Physical Simulation (timer-based animation 11.2), Timer 11.2
Physical Simulation
Even if the timer-based animation is used to copy the key frame behavior in chapter 1, there are some essential differences: in the implementation of the key frame, we calculated all frames in advance, but in the new solution, we actually calculate as needed. It means that we can modify the animation logic in real time based on user input, or integrate with other real-time animation systems, such as physical engines.
Chipmunk
We will create a realistic gravity simulation effect based on physics to replace the current buffer-based Elastic animation, but even if the simulation of 2D physical effects is nearly extremely complex, so don't try to implement it, just use the Open Source Physical engine library.
The physical engine we will use is called Chipmunk. The other 2D physical engine can also be used (for example, Box2D), but Chipmunk uses pure C writing instead of C ++. The advantage is that it is easier to integrate with Objective-C Projects. Chipmunk has many versions, including an "indie" version bound to Objective-C. The C language version is free of charge, so we can use it. When writing this book, 6.1.4 is the latest version; you can download it from the http://chipmunk-physics.net.
Chipmunk's complete physical engine is quite complex, but we only use the following classes:
cpSpace
-This is the container of all physical structures. It has a size and an optional Gravity Vector.
cpBody
-It is a solid, non-elastic rigid body. It has a coordinate and other physical attributes, such as mass, motion, and friction coefficient.
cpShape
-It is an abstract geometric shape used to detect collisions. You can add a polygon to the struct andcpShape
There are various subclass types to represent different shapes.
In this example, we model a wooden box and then fall under the influence of gravity. CreateCrate
Class, including the visual effect on the screen (UIImageView
) And a physical model (cpBody
And onecpPolyShape
, OnecpShape
To represent the rectangular wooden box ).
Using Chipmunk in C brings some challenges because it does not currently support the reference counting model of Objective-C, so we need to create and release objects accurately. To simplify the processcpShape
AndcpBody
Lifecycle andCrate
Class, and then in the wooden box-init
Created in-dealloc
. The configuration of the physical properties of the wooden box is very complicated, so reading the Chipmunk document will make sense.
View Controller for managementcpSpace
And the same timer logic as before. In each step, we updatecpSpace
(Used for physical computing and replacement of all struct) Then iterate the object, and then update the location of our wooden box view to match the wooden box model (here, there is actually only one struct, but we will add more ).
The Chipmunk uses a coordinate system that is reversed with the UIKit (the Y axis is in the upward and forward directions ). To make the synchronization between physical models and views easier, we need to usegeometryFlipped
The Set coordinates of the property flip container view (as mentioned in chapter 3rd), so both the model and view share the same coordinate system.
For specific code, see list 11.3. Note that we have not released it anywherecpSpace
Object. In this example, the memory space will always exist throughout the app lifecycle, so there is no problem. But in real-world scenarios, we need to manage our space like creating wooden boxes and shapes, encapsulate them in standard Cocoa objects, and then manage the lifecycle of Chipmunk objects. Figure 11.1 shows the dropped wooden case.
Listing 11.3 uses physics to model dropped wooden boxes
1 #import "ViewController.h" 2 #import 3 #import "chipmunk.h" 4 5 @interface Crate : UIImageView 6 7 @property (nonatomic, assign) cpBody *body; 8 @property (nonatomic, assign) cpShape *shape; 9 10 @end 11 12 @implementation Crate 13 14 #define MASS 100 15 16 - (id)initWithFrame:(CGRect)frame 17 { 18 if ((self = [super initWithFrame:frame])) { 19 //set image 20 self.image = [UIImage imageNamed:@"Crate.png"]; 21 self.contentMode = UIViewContentModeScaleAspectFill; 22 //create the body 23 self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height)); 24 //create the shape 25 cpVect corners[] = { 26 cpv(0, 0), 27 cpv(0, frame.size.height), 28 cpv(frame.size.width, frame.size.height), 29 cpv(frame.size.width, 0), 30 }; 31 self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2)); 32 //set shape friction & elasticity 33 cpShapeSetFriction(self.shape, 0.5); 34 cpShapeSetElasticity(self.shape, 0.8); 35 //link the crate to the shape 36 //so we can refer to crate from callback later on 37 self.shape->data = (__bridge void *)self; 38 //set the body position to match view 39 cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2)); 40 } 41 return self; 42 } 43 44 - (void)dealloc 45 { 46 //release shape and body 47 cpShapeFree(_shape); 48 cpBodyFree(_body); 49 } 50 51 @end 52 53 @interface ViewController () 54 55 @property (nonatomic, weak) IBOutlet UIView *containerView; 56 @property (nonatomic, assign) cpSpace *space; 57 @property (nonatomic, strong) CADisplayLink *timer; 58 @property (nonatomic, assign) CFTimeInterval lastStep; 59 60 @end 61 62 @implementation ViewController 63 64 #define GRAVITY 1000 65 66 - (void)viewDidLoad 67 { 68 //invert view coordinate system to match physics 69 self.containerView.layer.geometryFlipped = YES; 70 //set up physics space 71 self.space = cpSpaceNew(); 72 cpSpaceSetGravity(self.space, cpv(0, -GRAVITY)); 73 //add a crate 74 Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)]; 75 [self.containerView addSubview:crate]; 76 cpSpaceAddBody(self.space, crate.body); 77 cpSpaceAddShape(self.space, crate.shape); 78 //start the timer 79 self.lastStep = CACurrentMediaTime(); 80 self.timer = [CADisplayLink displayLinkWithTarget:self 81 selector:@selector(step:)]; 82 [self.timer addToRunLoop:[NSRunLoop mainRunLoop] 83 forMode:NSDefaultRunLoopMode]; 84 } 85 86 void updateShape(cpShape *shape, void *unused) 87 { 88 //get the crate object associated with the shape 89 Crate *crate = (__bridge Crate *)shape->data; 90 //update crate view position and angle to match physics shape 91 cpBody *body = shape->body; 92 crate.center = cpBodyGetPos(body); 93 crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body)); 94 } 95 96 - (void)step:(CADisplayLink *)timer 97 { 98 //calculate step duration 99 CFTimeInterval thisStep = CACurrentMediaTime();100 CFTimeInterval stepDuration = thisStep - self.lastStep;101 self.lastStep = thisStep;102 //update physics103 cpSpaceStep(self.space, stepDuration);104 //update all the shapes105 cpSpaceEachShape(self.space, &updateShape, NULL);106 }107 108 @end
View Code
Figure 11.1 a wooden box image, falling by the simulated gravity
Add User Interaction
The next step is to add an invisible wall around the view so that the wooden box will not drop out of the screen. Maybe you will use another rectanglecpPolyShape
But we need to check when the wooden box leaves the view, rather than when the view is collided, so we need a hollow rectangle instead of a solid rectangle.
We cancpSpace
Add fourcpSegmentShape
Object (cpSegmentShape
Represents a straight line, so the four are a rectangle ). ThestaticBody
Attribute (a struct not affected by gravity) instead of a new one like a wooden boxcpBody
Instance, because we do not want this border rectangle to slide out of the screen or be hit by a falling wooden box.
You can also add some wooden boxes for interaction. Add an accelerator so that you can adjust the Gravity Vector by tilting the phone (to test, you need to run the program on a real device because the simulator does not support accelerator events, even if you rotate the screen ). Listing 11.4 shows the updated code. The running result is shown in Figure 11.2.
Because the example only supports the Landscape mode, the x and y values of the accelerometer vector are exchanged. If you run the program in the portrait screen, please switch them back, otherwise the gravity direction will be lost. After a try, the wooden box will move along the horizontal direction.
Listing 11.4 updated code using walls and multiple wooden boxes
1 - (void)addCrateWithFrame:(CGRect)frame 2 { 3 Crate *crate = [[Crate alloc] initWithFrame:frame]; 4 [self.containerView addSubview:crate]; 5 cpSpaceAddBody(self.space, crate.body); 6 cpSpaceAddShape(self.space, crate.shape); 7 } 8 9 - (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end10 {11 cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);12 cpShapeSetCollisionType(wall, 2);13 cpShapeSetFriction(wall, 0.5);14 cpShapeSetElasticity(wall, 0.8);15 cpSpaceAddStaticShape(self.space, wall);16 }17 18 - (void)viewDidLoad19 {20 //invert view coordinate system to match physics21 self.containerView.layer.geometryFlipped = YES;22 //set up physics space23 self.space = cpSpaceNew();24 cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));25 //add wall around edge of view26 [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];27 [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];28 [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];29 [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];30 //add a crates31 [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];32 [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];33 [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];34 [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];35 [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];36 //start the timer37 self.lastStep = CACurrentMediaTime();38 self.timer = [CADisplayLink displayLinkWithTarget:self39 selector:@selector(step:)];40 [self.timer addToRunLoop:[NSRunLoop mainRunLoop]41 forMode:NSDefaultRunLoopMode];42 //update gravity using accelerometer43 [UIAccelerometer sharedAccelerometer].delegate = self;44 [UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;45 }46 47 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration48 {49 //update gravity50 cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY));51 }
View Code
Figure 11.1 wooden box interaction under the real gravitational field
Simulation time and fixed time step
Computing the duration of each frame is a good solution for implementing the animation buffering effect, but it is not ideal for simulating the physical effect. A variable time step has two drawbacks:
If the time step is not fixed and the exact value, the simulation of the physical effect will be uncertain. This means that even passing in the same input value may have different effects in different scenarios. Sometimes it does not have much impact, but in a physical engine-based game, players will be confused by the same operation behavior resulting in different results. It also makes the test troublesome.
Frame loss or call-in interruption may result in incorrect results due to performance. Consider moving an object as quickly as a bullet. Every frame update requires moving a bullet to detect collision. If the time between the two frames is longer, the bullet will move further in this step, passing through the wall or other obstacles, thus losing the collision.
The ideal result we want is to use a fixed time step to calculate the physical effect, however, the view can still be updated synchronously when the screen is re-painted (this may cause unpredictable effects beyond our control ).
Fortunately, because of our model (in this example, It is Chipmunk'scpSpace
IncpBody
View (the wooden box on the screen)UIView
Object), so it is very simple. We only need to track the time step based on the screen refresh time, and then calculate one or more simulated results based on each frame.
We can achieve this through a simple loop. Each timeCADisplayLink
To notify the screen to be refreshed, and then record the currentCACurrentMediaTime()
. We need to repeat the physical simulation in advance in a small increment (Here we use one second from 120) until we catch up with the display time. Then, update our view to match the display position of the current physical structure when the screen is refreshed.
Listing 11.5 shows the fixed-time step version code
Listing 11.5 wooden case simulation with fixed time step
Avoid death spiral
When using a fixed simulated time step, you must note that the real-world time used to calculate the physical effect does not accelerate the simulated time step. In our example, we randomly select one second to simulate the physical effect. Chipmunk is very fast, and our example is also very simple, socpSpaceStep()
It will be done well and will not delay frame update.
However, if the scenario is complex, for example, if there are hundreds of objects interacting with each other, physical computing will be complex,cpSpaceStep()
May also exceed 1/120 seconds. We didn't measure the time of the physical step, because we assume that the frame refresh is not important, but if the simulation step is longer, the frame rate will be delayed.
If the frame refresh time delay is poor, our simulation needs to execute more times to synchronize the real time. These additional steps will continue to delay frame update, and so on. This is the so-called death spiral, because the final result is that the frame rate is getting slower and slower until the last application gets stuck.
We can add some code to calculate the real-world time for physical steps on the device, and then adjust the fixed time step automatically, but it is actually not feasible. In fact, you only need to ensure that you leave enough edge length for fault tolerance, and then test on the slowest device that you expect to support. If physical computing exceeds 50% of the simulation time, you need to increase the simulation time step (or simplify the scenario ). If the simulation time step is increased to more than 1/60 seconds (a complete screen update time), you need to reduce the animation frame rate to 30 frames per second or increaseCADisplayLink
OfframeInterval
To ensure that frames are not randomly lost, or your animation will not look smooth.