Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

LiquidFun Tutorial with Metal and Swift – Part 1

$
0
0
Learn how to simulate water with LiquidFun and Metal!

Learn how to simulate water with LiquidFun and Metal!

Fluid dynamics and liquid simulation in games can be extremely difficult topics to digest. Without a good handle on physics and math concepts, it could take the average programmer months to accomplish what games like Where’s My Water and Sprinkle have done.

Thankfully, a group of talented engineers at Google have decided to lend us all a hand by giving us LiquidFun, an open-source multi-platform 2D physics engine. LiquidFun is built on top of Box2D, so while Box2D excels at 2D rigid body simulation, LiquidFun adds both particle-based fluid simulation and soft body simulation to the mix.

In this LiquidFun tutorial series, you’re going to learn the basics of making procedurally animated 2D water. You’ll use LiquidFun to simulate the motion of water particles, and Apple’s new GPU-accelerated 3D graphics API, called Metal, to render the water particles onscreen.

Note: Although this tutorial is designed to be as simple as possible, it’s still an intermediate-level tutorial because it combines several different technologies. By the time you complete the tutorial, you’ll have used the following programming languages: C, C++, Objective-C, Swift and the Metal shading language.

If you’re new to:

Getting Started

Note: This section is optional and covers how to integrate LiquidFun into your project. If you’d like, you can skip this section and begin with a starter project that has LiquidFun pre-integrated – just scroll down to the “Creating a LiquidFun Wrapper” section.

The first step is to create a Swift project. Launch Xcode (6 or higher), go to File\New\Project…, choose the iOS\Application\Single View Application template and click Next.

Fill out the options as follows:

  • Product Name: LiquidMetal
  • Language: Swift
  • Devices: Universal

Click Next, choose a folder for your project and click Create.

This is a portrait-only application, so open the Target Settings screen and in the General tab, make sure only Portrait is checked in the Device Orientation section:

Device orientation

To add LiquidFun to your project, download the latest stable version of LiquidFun (version 1.1.0 or later) and unarchive the package to a directory of your choice. For this example, I chose my Downloads directory.

Drag the liquidfun-1.1.0/liquidfun/Box2D/Box2D folder into your Xcode project. Make sure that Destination: Copy items if needed is checked, Added folders: Create groups is selected and Add to targets: LiquidMetal is checked, as shown below:

add_liquidfun

Because LiquidFun supports multiple platforms, it includes a lot of files that you won’t need for this tutorial. You need to remove the non-Xcode supported files so your project can build properly.

In your Xcode project directory, command-click the following unneeded files:

  1. Box2D/Box2D.vxcproj
  2. Box2D/Box2D.vcxproj.filters
  3. Box2D/Box2DConfig.cmake.in
  4. Box2D/CMakeLists.txt
  5. Box2D/UseBox2D.cmake
  6. Box2D/Documentation
  7. Box2D/Particle/b2ParticleAssembly.h
  8. Box2D/Particle/b2ParticleAssembly.neon.s
  9. Box2D/Particle/b2ParticleAssembly.cpp

Once you’ve selected them all, right-click on the group and choose Delete. Choose Remove References when prompted. Xcode will keep the files in your project directory but exclude them from your project. The GIF below demonstrates this process:

remove_liquidfun_extras

Build and run. You’ll encounter multiple “file not found” issues like the ones shown in the following image:

box2d_headerissue

You’re getting these errors because of how LiquidFun refers to its files internally. To fix them, you need to tell the compiler to look for header files inside the LiquidMetal folder you added to your project. Open the Target Settings screen, and in the Build Settings tab, add the following line to the Header Search Paths field:

$(SRCROOT)/LiquidMetal

This simply includes your project’s root directory to the header search paths – this way when you include a file with a path like Box2D/Particle/b2Particle.h it will resolve correctly.

Your target’s Header Search Paths should look like the following:

box2d_headerpath

That’s it! You’ve added LiquidFun to your Swift project. Build and run on any supported device or the iOS Simulator, and you should see the default splash screen transitioning to an empty view.

WhiteScreen

Creating a LiquidFun Wrapper

Note: If you skipped ahead to this section, download this starter project so you can jump straight into the action.

As a C++ library, LiquidFun can’t be used directly with Swift. This is because you can’t invoke C++ code from a Swift source file, nor, for that matter, can you invoke Swift code from a C++ source file. However, there’s a common link between the two languages: Objective-C.

Apple designed Swift to be interoperable with Objective-C; you can use Objective-C APIs in Swift and vice versa. Objective-C is also interoperable with C++, and you can use both languages in Objective-C++ source files.

To use LiquidFun in Swift, you have to create a wrapper class. This wrapper class will have a public interface written in Objective-C and an underlying implementation using Objective-C++.

wrapper_structure

The Objective-C layer is like a black box: Swift will only interact with Objective-C methods, without caring about how Objective-C does its business.

Let’s get started. Right-click the LiquidMetal group in the Project Navigator and select New File…, then select the iOS\Source\Cocoa Touch Class template and click Next. Call the class LiquidFun, enter NSObject into the Subclass of field and select Objective-C for the Language field. Click Next and then Create.

Select LiquidFun.m in your Project Navigator and left-click on it once more to rename the file. Change its name to LiquidFun.mm.

Changing the file’s extension to mm instructs Xcode to treat that file’s contents as Objective-C++ instead of Objective-C. If you hadn’t done that, you’d get errors later when you tried to compile this class.

With the LiquidFun class, you now have a place to mix Objective-C and C++ code together. The next step is to import this file into your Swift code. To do this, you need to create an Objective-C bridging header.

Right-click the LiquidMetal group in the Project Navigator and select New File…, then select the iOS\Source\Header File template and click Next. Name the file Bridging-Header and click Create.

Open Bridging-Header.h and add this line right below #define LiquidMetal_Bridging_Header_h:

#import "LiquidFun.h"

You can use all Objective-C headers exposed in this bridging header directly in any Swift source file in your project. In addition, you can interact with your Objective-C code using Swift syntax. Neat!

You have to tell the compiler about your Objective-C bridging header. Open the Target Settings screen, and in the Build Settings tab, add the following line to the Objective-C Bridging Header field:

LiquidMetal/Bridging-Header.h

The line you’ve added, shown in the following image, specifies the header file complete with its path from the root of the project.

bridging_header

You can now work seamlessly between C++, Objective-C and Swift. Build and run to confirm that your project is still working as expected.

WhiteScreen2

Note: From here on, I’ll refer to the Box2D-based physics engine as simply LiquidFun, while I’ll refer to LiquidFun.h/.mm, the wrapper class, as the LiquidFun wrapper class.

Creating a Physics World

To work with LiquidFun, you first need to create a world object. The world object manages and contains a collection of physics objects that interact with each other.

Note: Remember, LiquidFun is built on top of Box2D, so if you’re familiar with Box2D (or Sprite Kit’s physics engine, which is also built on top of Box2D) then you should be familiar with this concept, and many other concepts referenced in this tutorial.

Go to LiquidFun.h and add the following code above the @interface line:

#ifndef LiquidFun_Definitions
#define LiquidFun_Definitions
 
typedef struct Vector2D {
  float x;
  float y;
} Vector2D;
 
#endif

Then declare the following method inside the LiquidFun interface:

+ (void)createWorldWithGravity:(Vector2D)gravity;

You’ve declared your very first wrapper method and created a structure called Vector2D that holds x- and y-coordinates. LiquidFun has its own structure for holding the same information, called b2Vec2, but since you can’t expose any C++ code to Swift, you’ve created a new Swift-compatible structure in the wrapper’s public interface.

Note: Objective-C Cocoa has a similar structure named CGPoint. However, CGPoint contains CGFloat x- and y-coordinates instead of regular floats. A CGFloat is a regular float type in a 32-bit architecture but becomes a double type in a 64-bit architecture. LiquidFun and Swift are similar in that they both deal with float explicitly, so having this kind of auto-conversion may lead to unpredictable results.

Next, switch to LiquidFun.mm and add the following lines just above the @implementation line:

#import "Box2D.h"
 
static b2World *world;

Then add the following method to the LiquidFun class implementation:

+ (void)createWorldWithGravity:(Vector2D)gravity {
  world = new b2World(b2Vec2(gravity.x, gravity.y));
}

You import the main header of LiquidFun so you can use its classes and methods inside the wrapper. Then, you create a static b2World variable to keep a global reference to the world object you’ll create later. Since you won’t be creating any instances of the LiquidFun wrapper class, a static variable is as close as you can get to a class variable.

createWorldWithGravity: is a pass-through method to LiquidFun’s b2World constructor. This method creates a new b2World instance with the supplied gravity. Since the constructor method expects a b2Vec2 coordinate structure, you use the Vector2D type as input and create a counterpart b2Vec2 structure out of it.

All right, it’s time to create your world in Swift!

WholeNewWorld

Switch to ViewController.swift and add the following constant to the ViewController class:

let gravity: Float = 9.80665

Here you create a gravity constant to match the value of Earth’s standard gravity.

Next, add the following line to viewDidLoad:

LiquidFun.createWorldWithGravity(Vector2D(x: 0, y: -gravity))

This uses the LiquidFun wrapper class to create the world object with a negative y-gravity so that every object inside this world will fall down vertically.

Build and run to again see none of your work reflected on the screen. :]

WhiteScreen3

Simulating Water

You now have an empty world to play with, so it’s time to get to the main topic—water simulation. Ironically, water simulation is the easiest part of this tutorial due to the fact that LiquidFun makes it so simple.

Open LiquidFun.h and add the following structure after #define LiquidFun_Definitions:

typedef struct Size2D {
  float width;
  float height;
} Size2D;

Then add the following method declarations to the LiquidFun interface:

+ (void *)createParticleSystemWithRadius:(float)radius dampingStrength:(float)dampingStrength 
                            gravityScale:(float)gravityScale density:(float)density;
+ (void)createParticleBoxForSystem:(void *)particleSystem 
                          position:(Vector2D)position size:(Size2D)size;

You define another convenience structure named Size2D that contains width and height information. Next, you declare the two methods needed to create water in LiquidFun.

The foundation of LiquidFun’s water simulation is the particle system, defined by a b2ParticleSystem object. If each particle represents a water droplet, the particle system is their parent and enforces properties common to them all.

Each particle system is a subsystem within the larger simulation defined by the world object it belongs to. That is, a world object could contain multiple different particle systems, and once created, each particle system can generate particles discretely or in groups.

Switch to LiquidFun.mm and add this method:

+ (void *)createParticleSystemWithRadius:(float)radius dampingStrength:(float)dampingStrength
                            gravityScale:(float)gravityScale density:(float)density {
  b2ParticleSystemDef particleSystemDef;
  particleSystemDef.radius = radius;
  particleSystemDef.dampingStrength = dampingStrength;
  particleSystemDef.gravityScale = gravityScale;
  particleSystemDef.density = density;
 
  b2ParticleSystem *particleSystem = world->CreateParticleSystem(&particleSystemDef);
 
  return particleSystem;
}

This method creates a particle system with an initial set of properties defined by a b2ParticleSystemDef:

  • A particle is round, so you need to define a radius for each particle in the system.
  • You’ll use dampingStrength to reduce the velocity of particles over time.
  • A particle system doesn’t need to strictly follow the physics world’s gravity, so it uses gravityScale to adjust the effect of the physics world’s gravity on its particles.
  • density affects the mass of the particles, and this affects how the particles interact with other physics bodies in the simulation. However, density doesn’t change how particles interact with each other.

b2World is the overall manager of the simulation, so you use it to create a new b2ParticleSystem with the properties you defined. After the particle system is created, this method returns a reference to it so you can access the particle system again later. Note that this method returns a pointer of type void *, a generic pointer to an address in memory, because Swift doesn’t know about the b2ParticleSystem type.

Still in LiquidFun.mm, add this method:

+ (void)createParticleBoxForSystem:(void *)particleSystem 
                          position:(Vector2D)position size:(Size2D)size {
  b2PolygonShape shape;
  shape.SetAsBox(size.width * 0.5f, size.height * 0.5f);
 
  b2ParticleGroupDef particleGroupDef;
  particleGroupDef.flags = b2_waterParticle;
  particleGroupDef.position.Set(position.x, position.y);
  particleGroupDef.shape = &shape;
 
  ((b2ParticleSystem *)particleSystem)->CreateParticleGroup(particleGroupDef);
}

To create a group of particles, you first need to define a shape for it (i.e. the shape of the container for the particles). This method uses a box shape defined by a size parameter for the group – later you will pass in a 50×50 point box at the bottom of the screen for this container box.

Next, you create a b2ParticleGroupDef to define properties for the group of particles to be produced. Since you want to simulate water, you specify the b2_waterParticle flag as the type of particle and place the group in a starting position.

Note: You can specify multiple flags for the type of particle you want. Check out the b2ParticleFlag documentation to learn more about the different types of flags available.

Finally, you ask the particle system to create a group of particles with the defined properties. This method expects to be given a reference to a previously created particle system, so you must use it in conjunction with createParticleSystemWithRadius:dampingStrength:gravityScale:density:, which you defined earlier.

Open ViewController.swift and add the following properties:

let ptmRatio: Float = 32.0
let particleRadius: Float = 9
var particleSystem: UnsafeMutablePointer<Void>!

Here’s a breakdown of the properties you’ve just added:

  • ptmRatio is the points-to-meters conversion ratio. LiquidFun is optimized to work with objects sized from as small as 0.1 meters to as big as 10 meters. Since a 0.1-point sized object won’t be visible on your device, you need a ratio to convert LiquidFun’s units—meters—to screen coordinates in points. With this ratio, a 1-meter object in LiquidFun’s physics simulation would occupy 32 points onscreen.
  • particleRadius defines the radius you’ll use, in points, for your water particles.
  • particleSystem will hold a reference to the particle system you’ll create later. The UnsafeMutablePointer<Void> type is Swift’s way of representing the void * type you used in your wrapper class earlier.

Still in ViewController.swift, add the following to the end of viewDidLoad:

particleSystem = LiquidFun.createParticleSystemWithRadius(
  particleRadius / ptmRatio, dampingStrength: 0.2, gravityScale: 1, density: 1.2)
 
let screenSize: CGSize = UIScreen.mainScreen().bounds.size
let screenWidth = Float(screenSize.width)
let screenHeight = Float(screenSize.height)
 
LiquidFun.createParticleBoxForSystem(particleSystem, 
  position: Vector2D(x: screenWidth * 0.5 / ptmRatio, y: screenHeight * 0.5 / ptmRatio), 
  size: Size2D(width: 50 / ptmRatio, height: 50 / ptmRatio))

First, you create a particle system and store a reference to it. You divide the particle radius by the points-to-meters ratio you defined earlier so that LiquidFun doesn’t produce huge water particles. Your particles will still show up as 9 points onscreen, but in LiquidFun coordinates it will be 9.0 points / 32.0 points per meter = 0.28 meters.

Next, using the particleSystem you just created, you add a group of particles in a 50×50-point box at the center of the screen. To compute for the center, you get the screen bounds from UIScreen and convert the retrieved width and height values to Swift Floats from CGFloat. As before, you divide all values using ptmRatio.

Build and run to make sure everything still compiles correctly.

WhiteScreen4

Where’s My Water?

Congratulations! You’ve just added an invisible particle system to your invisible physics world.

Of course, invisible particle systems, while potentially useful to create certain effects, are pretty anti-climatic. Unfortunately, you still have a ways to go before you can draw your water particles onscreen, but in the meantime you can at least print out the positions of the particles in your system to confirm they exist.

Go to LiquidFun.h and add the following method declarations:

+ (int)particleCountForSystem:(void *)particleSystem;
+ (void *)particlePositionsForSystem:(void *)particleSystem;

Quickly switch to LiquidFun.mm and add the following implementations of those methods:

+ (int)particleCountForSystem:(void *)particleSystem {
  return ((b2ParticleSystem *)particleSystem)->GetParticleCount();
}
 
+ (void *)particlePositionsForSystem:(void *)particleSystem {
  return ((b2ParticleSystem *)particleSystem)->GetPositionBuffer();
}

These are both Objective-C pass-through methods for their C++ counterparts. particleCountForSystem: returns the number of particles currently alive in a particle system, while particlePositionsForSystem: returns a pointer to the array of b2Vec2 positions of these particles. Once again, you return void * because Swift doesn’t know about the b2Vec2 type.

Now open ViewController.swift and add the following method:

func printParticleInfo() {
  let count = Int(LiquidFun.particleCountForSystem(particleSystem))
  println("There are \(count) particles present")
 
  let positions = UnsafePointer<Vector2D>(LiquidFun.particlePositionsForSystem(particleSystem))
 
  for i in 0..<count {
    let position = positions[i]
    println("particle: \(i) position: (\(position.x), \(position.y))")
  }
}

And add a call to this method at the end of viewDidLoad:

printParticleInfo()

You call printParticleInfo to log how many particles were created and their positions in your physics world. particlePositionsForSystem returns a pointer to an array of type void * (originally b2Vec2), so you typecast it to a Vector2D array pointer instead, allowing you to access each element’s properties.

Note that directly converting one structure to another like this is a dangerous thing to do, but in this case, Vector2D and b2Vec2 are similar enough that it works.

Build and run, and look at the developer console.

log_particles

Hello, particles!

Note: When creating a group of particles in a box, LiquidFun creates the particles (.75 * particle diameter) units apart from one another by default. It produces particles from the center outward, and creates any particle whose center falls within the defined shape.

Where to Go From Here?

So far, you’ve learned how to integrate LiquidFun with Swift, and in the process, you’ve created an invisible liquid particle system. Now it’s time to take a breather.

Here’s the sample project with all of the code from this LiquidFun tutorial.

From this point on, you have multiple options for how to render your LiquidFun particles onscreen. You could use a Sprite Kit particle system and manually map the positions of your LiquidFun particles to it, or you could roll your own particle system using OpenGL ES. For this tutorial series, you’ll use Apple’s Metal graphics API for the task.

When you’re ready, move on to Part 2 of this series, where you’ll render a LiquidFun particle system using Metal.

In the meantime, if you have any questions or comments about this part, please join the forum discussion below!

LiquidFun Tutorial with Metal and Swift – Part 1 is a post from: Ray Wenderlich

The post LiquidFun Tutorial with Metal and Swift – Part 1 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>