中级box2d物理特性;压力、射线、传感器

本文来自:http://www.raywenderlich.com/4653/intermediate-box2d-physics-forces-ray-casts-and-sensors

 

如果你把在这个网站上 Cocos2D 学习教程关于box2d的教程都学完了感觉还满足不了你,这篇文章就比较适合你。 这篇文章将涉及一些box2d中级技术:如何给对象生成压力、如何使用射线,如何在碰撞检测中使用传感器。

在这篇文章中,我们添加了一些新的特性到之前我们在如何使用 SpriteHelper和LevelHelper教程中创建的一个简单的物理特性程序. 如果你没学过也没关系,我们这里也用不到他。

如果你没有学过它,就从这里sample project下载下来之前做过的那个项目,然后就可以开始了。

Using Forces

If you try out the game, you’ll notice that the monsters fall from the sky and bounce on the ground.

That’s not very alien-like! It would be a lot better if the aliens floated menacingly in the air.

This brings us to a common question – how can we make some objects affected by gravity, but not others?

One technique you can use is to not use gravity at all, but just apply the gravity force manually on all sprites that are affected by gravity. Or alternatively, you can apply an anti-gravity force on things that are not affected by gravity.

We’ll try the latter. Switch to ActionLayer.mm and make the following changes:

// Add right after updateHero method
- (void)updateMonsters:(ccTime)dt {    
    NSArray *monsters = [_lhelper bodiesWithTag:MONSTER];
    for(NSValue* monsterValue in monsters) {
        b2Body* monsterBody = (b2Body*)[monsterValue pointerValue];
        monsterBody->ApplyForce(-1 * monsterBody->GetMass() * _world->GetGravity(), 
            monsterBody->GetWorldCenter());
        monsterBody->SetAngularVelocity(1.0);                       
    }    
}
 
// Inside update, add right after call to updateHero
[self updateMonsters:dt];

The first bit of code here gets the list of monsters in the game via a helper method that retrieves all of the Box2D bodies set up with the MONSTER tag in LevelHelper.

Next, we need to apply the force to the monster to get it to float.

Force is in newtons (N), which is the force required to accelerate one kilogram of mass at a rate of one meter per second squared. We know we want to accelerate our monster the opposite of gravity (set up to -10 meters per second sauared in LevelHelper), so we have to multiply that by the monster’s mass to get Newtons. We also multiply by negative one to reverse the gravity.

We also set the angular velocity to 1 radian/second here (which is a slowish turnaround), so the monster spins around as if he’s looking for an intruder.

Compile and run, and now you have floating and spinning monsters!

Using forces in Box2D to counteract gravity

By the way, you may wonder when you should use impulses and when you should use forces. A common rule of thumb is if you need something to move instantly (like our hero jumping), use an impulse. If you need an object to move over a period of time (like the permanent float effect here), use forces instead.

Ray Cast Visualization

Next we’ll move onto something I’m particular fond of (and it’s not just because of the name) – ray casting!

The idea behind Box2D ray casting is you specify a start point and an end point, and Box2D will trace along the line from start to end, and tell you every Box2D fixture that it collides with.

You can then do whatever you want with that information. We’re going to use this to simulate line of sight for our monsters. If we draw a line from the eyeball straight out and the first thing it hits is our hero (rather than a wall) – he’s been seen!

Before we can use Box2D for ray casting, we have to figure out the start point and end point of the lines to ray cast. It’s easy to make mistakes while doing this, so one thing I like to do is use Cocos2D drawing methods to visualize the points I’m using to make sure it’s working OK.

We’re going to need a class to keep track of the start and end points for each monster (along with a few other bits), so go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MonsterData.m, and click Save.

Replace MonsterData.h with the following:

#import <Foundation/Foundation.h>
 
@interface MonsterData : NSObject 
 
@property CGPoint eye;
@property CGPoint target;
@property BOOL canSeePlayer;
@property double lastShot;
 
@end

This creates a simple subclass of NSObject with four properties we’ll need. Notice this abbreviated format – it doesn’t need to specify the instance variables because the compiler will create those automatically for us now. Pretty cool eh?

Switch to MonsterData.m and replace it with the following:

#import "MonsterData.h"
 
@implementation MonsterData
 
@synthesize eye;
@synthesize target;
@synthesize canSeePlayer;
@synthesize lastShot;
 
@end

Now let’s make use of this. Make the following changes to ActionLayer.mm:

// Add to top of file
#import "MonsterData.h"
 
// Add to the bottom of setupLevelHelper
NSArray *monsters = [_lhelper spritesWithTag:MONSTER];
for (CCSprite *monster in monsters) {
    MonsterData *data = [[[MonsterData alloc] init] autorelease];
    [LevelHelperLoader setCustomValue:data withKey:@"data" onSprite:monster];
}

Here we loop through all of the monsters sprites, create an empty MonsterData for them, and store it on the sprite under the “data” key. This setCustomValue method is something LevelHelper lets you do to associate extra information like this with a sprite. If you weren’t using LevelHelper, you could subclass CCSprite instead or create a dictionary and set it on the sprite’s userData.

Next add the following inside updateMonsters, right after the call to SetAngularVelocity:

b2Vec2 eyeOffset = b2Vec2(0, -0.5);
b2Vec2 eye = monsterBody->GetWorldPoint(eyeOffset);
b2Vec2 target = eye - monsterBody->GetWorldCenter();
target.Normalize();
target *= 20.0;
target = eye + target;
 
CCSprite *monsterSprite = (CCSprite*)monsterBody->GetUserData();
MonsterData * monsterData = [LevelHelperLoader customValueWithKey:@"data" forSprite:monsterSprite];            
monsterData.eye = ccp(eye.x * [LevelHelperLoader pixelsToMeterRatio], eye.y * [LevelHelperLoader pixelsToMeterRatio]);
monsterData.target = ccp(target.x * [LevelHelperLoader pixelsToMeterRatio], 
    target.y * [LevelHelperLoader pixelsToMeterRatio]);
monsterData.canSeePlayer = NO;

The first bit figures out the start point (where the eye is) and the end point (a certain distance away from where the eye is looking). Let’s go over how that works.

The position of the eye is easy – it’s just 0.5 Box2D units down from the center of the sprite.

To get the target, we start by subtracting the center of the monster from the eye’s position. This gives us a vector pointing in the direction of where the eye is relative to the center of the monster.

We then call normalize on that vector to make it unit length (1). This makes it so that we can multiply it by the desired length we want, and we’ll have a vector pointing in the desired direction, at the desired length. Once we have that, we just add it to the start point to get the final target.

The rest of the code just converts these Box2D coordinates to Cocos2D coordinates and stores it in the sprite’s MonsterData.

As the final step, add the following inside draw right after the call to DrawDebugData:

NSArray *monsters = [_lhelper spritesWithTag:MONSTER];
for (CCSprite *monster in monsters) {
    MonsterData * data = [LevelHelperLoader customValueWithKey:@"data" forSprite:monster];
    if (!data.canSeePlayer) {
        glColor4ub(0, 255, 0, 255);
    } else {
        glColor4ub(255, 0, 0, 255);
    }
    ccDrawLine(data.eye, data.target);
}

Every time we draw the layer, we loop through the monsters, and look for the MonsterData key. We use the built-in ccDrawLine function to draw a line from the eye to the target. Note we color the line red if he can see the player, green otherwise. Right now it will always be green.

Compile and run, and you should now see lines drawn that indicate where the monsters are looking:

Drawing lines with Cocos2D

These will serve as the input we’ll send to Box2D to get it to do the ray casting.

Box2D Ray Casting

To use Box2D ray casting, you call a simple function on the world called RayCast, and give it the start and finish point (basically what we just figured out above).

You also pass the function an object that will receive a callback for each fixture the ray intersects. Usually you just squirrel away the information in the object, and retrieve the results from the object after the call to RayCast.

Let’s create a simple Raycast callback class. Go to File\New\New File, choose iOS\C and C++\Header File, and click Next. Name the new header RaysCastCallback.h, and click Save.

Replace the file with the following:

#import "Box2D.h"
 
class RaysCastCallback : public b2RayCastCallback
{
public:
    RaysCastCallback() : m_fixture(NULL) {
    }
 
    float32 ReportFixture(b2Fixture* fixture, const b2Vec2& point, const b2Vec2& normal, float32 fraction) {        
        m_fixture = fixture;        
        m_point = point;        
        m_normal = normal;        
        m_fraction = fraction;        
        return fraction;     
    }    
 
    b2Fixture* m_fixture;    
    b2Vec2 m_point;    
    b2Vec2 m_normal;    
    float32 m_fraction;
 
};

ReportFixture is the method that will get called whenever Box2D detects an intersection. We have a pretty simple implementation – we just squirrel everything away.

One thing about ReportFixture is you can’t make any assumption about the order of the calls (i.e. it’s not necessarily closest to farthest). However, you can do some interesting things with the return value.

  • If you return 0: The ray cast will be terminated immediately. So your ReportFixture will be called at most one time, with one random fixture it collides with.
  • If you return 1: The ray cast will continue. So your ReportFixture will be called for every fixture that collides along the ray. With this implementation, you’ll still be squirreling away one random set of information (whatever the last call gave you).
  • <EMIF you return fraction from the argument list:< em>The ray cast will be clipped to the current intersection point. This is what we do here. With this implementation, you’ll be guaranteed that each time ReportFixture is called, the intersection gets closer and closer toward the start point. So by the end the closest intersection will be squirreled away in the instance variables, which is what we want for line of sight!

You could modify this class to discard certain fixtures that might be transparent (like a piece of glass, etc). However this simple implementation is fine for our game.

Switch back to ActionLayer.mm and make the following changes:

// Add to top of file
#import "RaysCastCallback.h"
 
// Add inside updateMonsters, right after setting canSeePlayer to NO
RaysCastCallback callback;
_world->RayCast(&callback, eye, target);
 
if (callback.m_fixture) {
    monsterData.target = ccp(callback.m_point.x * [LevelHelperLoader pixelsToMeterRatio], 
        callback.m_point.y * [LevelHelperLoader pixelsToMeterRatio]);
    if (callback.m_fixture->GetBody() == _heroBody) {    
        monsterData.canSeePlayer = TRUE;
    }
}

Here we declare a RaysCastCallback class, and call the RayCast method, passing it as a parameter. It will call the ReportFixture zero or more times on the class, and by the end the closest fixture and contact point should be squirreled away.

We then look to see if it found an intersection, and if so we set the target to the actual point that was found. This will cause the line that is drawn to be truncated to a smaller range if it hits a cloud, etc., which is a cool way to visualize the first thing the monster is seeing since that’s the line we’re drawing.

We finally check to see if the fixture is the hero, and if it is set the flag to true. Remember, this will cause the line to be drawn red.

Compile and run, and move your player to the danger zone underneath the monster, and see if you’re spotted!

Box2D raycasting example

Lasers and Sensors

Obviously it is not a good thing if a scary monster like that spots you. So how are we going to punish our hero for not being careful? It’s obvious – shoot lasers at him!

Back in the previous tutorial, we added a laser to the scene to act as a “template” for future lasers. We can use LevelHelper to create a new laser based on how that laser was set up.

When we set up the laser, we set it up to be a sensor in Box2D. If you aren’t using LevelHelper, you can easily do this by setting the isSensor variable on your fixture to true.

When an object is a sensor, as long as you have the category, mask, and groupIndex properties set up right, you will receive callbacks to your contact listener, but it won’t cause physics reactions like bouncing off another object. This is perfect for our laser, since we want it to go through everything but zap the player if it hits him.

Let’s start just by shooting the lasers – we’ll add collision detection later. Add the following code inside updateMonsters, right after canSeePlayer is set to TRUE:

if (CACurrentMediaTime() - monsterData.lastShot > 1.0) {
    monsterData.lastShot = CACurrentMediaTime();
 
    // Create and position laser
    b2Body *laserBody = [_lhelper newBodyWithUniqueName:@"laserbeam_red" world:_world];
    CCSprite *laserSprite = (CCSprite *)laserBody->GetUserData();
    laserSprite.position = monsterData.eye;
    laserSprite.rotation = monsterSprite.rotation;                        
    laserBody->SetTransform(b2Vec2(laserSprite.position.x/[LevelHelperLoader pixelsToMeterRatio], 
                                   laserSprite.position.y/[LevelHelperLoader pixelsToMeterRatio]), 
                            CC_DEGREES_TO_RADIANS(-laserSprite.rotation));    
 
    // Make laser move
    b2Vec2 laserVel = callback.m_point - eye;
    laserVel.Normalize();
    laserVel *= 4.0;
    laserBody->SetLinearVelocity(laserVel);
 
    [[SimpleAudioEngine sharedEngine] playEffect:@"laser.wav"];
 
}

We first add some code to prevent the monster from shooting more often than every second.

Next we create a new sprite and body for the laser based on the template, using LevelHelper’s newBodyWithUniqueName method. We set the initial position and rotation of the laser to start at the eye, rotated the same way the monster is. We also manually update the laser body to be at the same position of where we just set the sprite.

Finally, we move the laser manually via SetLinearVelocity. To figure out where to go, we’re using the same technique of finding the vector the eye is looking at like we did earlier.

We also play a cool laser sound effect!

Compile and run, and prepare to be blasted!

Laser created from LevelHelper template

Finishing Touches

Right now the lasers just pass harmlessly by our hero, even if he’s foolishly stood in danger’s way. So let’s fix that by adding lives and collision detection!

If you remember from the Breakout Game Tutorial or our Learning Cocos2D Book, when you want to detect collisions you have to create a contact listener class.

So let’s create a simple ContactListener that simply directs the callbacks back to our action layer.

Go to File\New\New File, choose iOS\C and C++\Header File, and click Next. Name the new header SimpleContactListener.h, and click Save. Then replace SimpleContactListener.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "Box2D.h"
#import "ActionLayer.h"
 
class SimpleContactListener : public b2ContactListener {
public:
    ActionLayer *_layer;
 
    SimpleContactListener(ActionLayer *layer) : _layer(layer) { 
    }
 
    void BeginContact(b2Contact* contact) { 
        [_layer beginContact:contact];
    }
 
    void EndContact(b2Contact* contact) { 
        [_layer endContact:contact];
    }
 
    void PreSolve(b2Contact* contact, const b2Manifold* oldManifold) { 
    }
 
    void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) {  
    }
 
};

Pretty simple, eh? Next switch to ActionLayer.h and add a few more instance variables:

int _lives;
b2ContactListener * _contactListener;
BOOL _invincible;

And finally make the following changes to ActionLayer.mm:

// Add to top of file
#import "SimpleContactListener.h"
 
// Add at bottom of setupWorld
_contactListener = new SimpleContactListener(self);
_world->SetContactListener(_contactListener);
 
// Add above init
- (void)updateLives {
    // TODO: Next tutorial!  :D
}
 
// Add inside init, right after setting isTouchEnabled
_lives = 3;
[self updateLives];
 
// Add right before call to dealloc
- (void)beginContact:(b2Contact *)contact {
 
    if (_gameOver) return;
 
    b2Fixture *fixtureA = contact->GetFixtureA();
    b2Fixture *fixtureB = contact->GetFixtureB();
    b2Body *bodyA = fixtureA->GetBody();
    b2Body *bodyB = fixtureB->GetBody();
    CCSprite *spriteA = (CCSprite *) bodyA->GetUserData();
    CCSprite *spriteB = (CCSprite *) bodyB->GetUserData();
 
    if (!_invincible) {
        if ((spriteA == _hero && spriteB.tag == LASER) ||
            (spriteB == _hero && spriteA.tag == LASER)) {
            _lives--;
            [self updateLives];
            [[SimpleAudioEngine sharedEngine] playEffect:@"whine.wav"];
            if (_lives == 0) {
                [self loseGame];
                return;
            }
            _invincible = YES;
            [_hero runAction:
             [CCSequence actions:
              [CCBlink actionWithDuration:1.5 blinks:9],
              [CCCallBlock actionWithBlock:^(void) {
                 _invincible = NO;
             }],
              nil]];
        }
    }    
 
}
 
- (void)endContact:(b2Contact *)contact {
 
}
 
// Add at *beginning* of dealloc
_world->SetContactListener(NULL);
delete _contactListener;

In setupWorld we create and set the contact listener and in init we set the lives to 3.

The important part is in beginContact. We check to see if the hero is colliding with a laser, and if so subtract a life, play a sound effect, etc. We make the hero invincible for a second and a half after he’s hit to make things a bit easier for the poor dude (and avoid the problem of multiple collisions with the same laser).

Note this demonstrates the cool CCCallBlock actoin – cool!

Compile and run, and you should now be able to be blasted by lasers!

Gratuitous Animation

The following is an optional step if you have SpriteHelper and LevelHelper – feel free to skip if you don’t.

Our game is a little stupid at the moment because when you move it doesn’t look like you’re moving – it’s the same sprite frame no matter what.

Well, in LevelHelper you can easily define animations that you can then run in code. Let’s try it out!

Open up TestLevel.plhs in LevelHelper, select the fourth tab in the sidebar to bring up the Animation Editor, and click the New button:

LevelHelper Animation Panel

Select char_walk_1 and click the + button to bring it into the animation. Then repeat with char_walk_2. The dialog will show the animations running one after another. You can modify the speed if you want, but the default is actuallly OK for our game. Click Create Animation to finish.

Creating an animation with LevelHelper

Then double click inside the Animation Name area for the newly create animation and rename it to Walk.

Repeat this process to create an animation containing char_flap_1 and char_flap_2, and rename it to Flap.

Save your LevelHelper project, go back to Xcode, and add two new instance variables to ActionLayer.h:

double _lastGround;
int _numGroundContacts;

Then make the following changes to ActionLayer.mm:

// Replace updateHero with the following
- (void)updateHero:(ccTime)dt {
    if (_playerVelX != 0) {
        b2Vec2 b2Vel = _heroBody->GetLinearVelocity();
        b2Vel.x = _playerVelX / [LevelHelperLoader pixelsToMeterRatio];
        _heroBody->SetLinearVelocity(b2Vel);    
 
        if (_numGroundContacts > 0 && CACurrentMediaTime() - _lastGround > 0.25) {
            _lastGround = CACurrentMediaTime();
            [[SimpleAudioEngine sharedEngine] playEffect:@"ground.wav"];
            if ([_hero numberOfRunningActions] == 0) {
                [_lhelper startAnimationWithUniqueName:@"Walk" onSprite:_hero];
            }
        }
 
    } else if (_playerVelX == 0 && _numGroundContacts > 0) {
        [_lhelper stopAnimationWithUniqueName:@"Walk" onSprite:_hero];
    }
}
 
// Add to end of beginContact
if ((spriteA == _hero && spriteB.tag == GROUND) ||
    (spriteB == _hero && spriteA.tag == GROUND)) {
    if (_numGroundContacts == 0) {
        [_lhelper stopAnimationWithUniqueName:@"Flap" onSprite:_hero];
    }
    _numGroundContacts++;
}
 
// Replace endContact with the following
- (void)endContact:(b2Contact *)contact {
 
    b2Fixture *fixtureA = contact->GetFixtureA();
    b2Fixture *fixtureB = contact->GetFixtureB();
    b2Body *bodyA = fixtureA->GetBody();
    b2Body *bodyB = fixtureB->GetBody();
    CCSprite *spriteA = (CCSprite *) bodyA->GetUserData();
    CCSprite *spriteB = (CCSprite *) bodyB->GetUserData();
 
    if ((spriteA == _hero && spriteB.tag == GROUND) ||
        (spriteB == _hero && spriteA.tag == GROUND)) {
        _numGroundContacts--;
    }
}
 
// Add inside ccTouchesBegan, at end of touch.tapCount > 1 if statement
[_lhelper startAnimationWithUniqueName:@"Flap" onSprite:_hero];

All we’re doing here is keeping track of how many contacts the hero has with the ground, playing the walk animation if the number of contacts is greater than 0, and the flap action if the user double taps.

Compile and run, and now you can move in style!

Example Box2D game with ray casting, forces, and sensors

Where To Go From Here?

Here is the sample project we developed in the above tutorial.

Now you have even more techniques to add to your Box2D arsensal! I look forward to seeing some cool physics games from you guys!

There will be one more tutorial using this sample code, to demonstrate adding a HUD layer to a game, per popular request.

If you have any questions or comments on these Box2D techniques or about this tutorial, please join the forum discussion below!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值