## Intermediate Box2D Physics: Forces, Ray Casts, and Sensors

Expand your Box2D knowledge with ray casting and more!

If you’ve gone through the Box2D tutorials in this site or in our Learning Cocos2D Book and can’t get enough, this tutorial is for you!

This tutorial will cover some intermediate Box2D techniques: applying forces to objects, using ray casts, and using sensors for collision detection.

In this tutorial, we’ll be adding some new features to a simple physics platformer that we created in the How To Use SpriteHelper and LevelHelper Tutorial. Don’t worry if you don’t have SpriteHelper or LevelHelper – this tutorial doesn’t require them.

So if you don’t have it already, download the sample project and fire it up to see where we’re at, and let’s start having fun with Box2D!

## 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!

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   @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:

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).
• 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!

## 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!

## 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 #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:

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.

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!

## 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.

[ 1 , 2 , 3 ]
• Hi there,

can someone give me a clue what's the right way to show the sprites ?

1. method
Code: Select all
``` LHSprite* laserSprite = [_lhelper createSpriteWithUniqueName:@"laserbeam_red"]; ```

or
2. method
Code: Select all
``` LHSprite* laserSprite = [_lhelper spriteWithUniqueName:@"laserbeam_red"]; ```

As i said, when i'am using the 1. method i dont see any lasers.

thx
hoschman
hoschman
• @hoschman - I ran into the same issue. I believe you want to use "createPhysicalBatchSpriteWithUniqueName" instead (create a sprite with physics body), otherwise I believe you are just reusing the 1 sprite that was placed offscreen in LevelHelper. With your method, you won't be able to get shot at by more than 1 monster.

Great tutorials Ray and I enjoyed your book as well!
jaycj
• m currently having a problem with lasers... i have posted this also on the box2d forum as its probably a box2d error but i will share my resolve on here if i can get one.

Basically i have a space shooter game ( think geo wars) and move the playership with the sneaky joystick. upon taping a button a laser shoots at the angle of the ship. However about 10-20% of the time the laser body/sprite is shot at an angle to which the playership is not facing and the laser body/sprite itself is at an angle.

code below

Code: Select all
``` - (void)movePlayer {     //move the player with joypad     CGPoint velocity = ccpMult(_hud->leftJoystick.velocity, 5);     if (velocity.x != 0)     {         b2Vec2 force = b2Vec2(0,0);         force = b2Vec2(velocity.x,velocity.y);         playerBody->SetLinearVelocity(force);         float bodyAngle = playerBody->GetAngle();         b2Vec2 toTarget = playerBody->GetPosition();         float desiredAngle = atan2f( -velocity.x, velocity.y );         float totalRotation = desiredAngle - bodyAngle;         while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;         while ( totalRotation >  180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;         playerBody->SetTransform( playerBody->GetPosition(), desiredAngle );     }     if (velocity.x == 0)     {         playerBody->SetAwake(false) ;             } } - (void)LaserGun:(ccTime)dt {     for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())    {     NSArray *players = [lh spritesWithTag:PLAYERSHIP];     for (playerShip in players) {                 playerBody = [playerShip body];         b2Vec2 eyeOffset = b2Vec2(0, 0.5);         b2Vec2 eye = playerBody->GetWorldPoint(eyeOffset);         b2Vec2 target = eye - playerBody->GetWorldCenter();         target.Normalize();         target *= 20.0;         target = eye + target;                         playerData * playerData = [playerShip customValueWithKey:@"data"];                    playerData.eye = ccp(eye.x * [LevelHelperLoader pixelsToMeterRatio], eye.y * [LevelHelperLoader pixelsToMeterRatio]);         playerData.target = ccp(target.x * [LevelHelperLoader pixelsToMeterRatio], target.y * [LevelHelperLoader pixelsToMeterRatio]);         playerData.canSeePlayer = NO;                     RaysCastCallback callback;         world->RayCast(&callback, eye, target);                 if (callback.m_fixture)         {             playerData.target = ccp(callback.m_point.x * [LevelHelperLoader pixelsToMeterRatio],                                     callback.m_point.y * [LevelHelperLoader pixelsToMeterRatio]);            if (callback.m_fixture->GetBody() == monster1body)            { playerData.canSeePlayer = TRUE;}         }         if(_hud->jumpButton.active)         {                     if (CACurrentMediaTime() - playerData.lastShot > 0.1)             {             playerData.lastShot = CACurrentMediaTime();                         // Create and position laser             b2Body *laserBody = [lh newBodyWithUniqueName:@"LaserBeam" world:world];             laserSprite = (LHSprite *)laserBody->GetUserData();             laserSprite.position = playerData.eye;                 laserSprite.rotation = (playerShip.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 *= 10.0;             laserBody->SetLinearVelocity(laserVel);                                 if(!world->IsLocked())             {                 b2Body* laserBody = [laserSprite body];                                 if(laserBody)                 {                     laserBody->SetLinearVelocity(laserVel);                               }               }}}}}} ```

has anybody encountered this problem before and what did you do to resolve this?
tr736
• Try to use

Code: Select all
``` LHSprite* laserSprite = [_lhelper createPhysicalBatchSpriteWithUniqueName:@"laserbeam_red"]; ```

createPhysicalBatchSpriteWithUniqueName maybe work better than other...
Acquarious81
• I am not very into c++, but what does this line mean:

"RaysCastCallback() : m_fixture(NULL) {}"

??
JHNeves
• Really fascinated by this kind of stuff, Nice tutorial.

What would be the best way to implement a field of view rather than ray cast?

Would it be a matter of calculating a circular parameter, and finding the 1/4 that would be what the sprite sees?

or would it be calculating two angled sensors and getting the area between?

Hope this makes sense, does in my head.
MichaelMacartney
• Hello! I've been following along with this tutorial (and the previous one before it) with LevelHelper 1.4.957. As of today, thats the latest version on the app store. I've been able to work through a lot of the code that doesn't match up to this version of LevelHelper and been fixing on the fly. I'm stuck on one part that i cant seem to adjust without making some serious changes.

[LevelHelperLoader setCustomValue:data withKey:@"data" onSprite:monster]; -- setCustomValue does not exist in LHSprite.h or LHSpriteEXT.h with this version of level helper. The closest thing i can find is possibly they wanted to go a separate route in the newer version with a Custom Sprite Class with its own methods to handle the dictionary which used to be handled by customUserData in LHSprite.h

It just seems that if i implement this way of doing it that i'll have to rework a lot of the code in the tutorial. Am i over thinking this? Is this a big change or a minor one?
Tmoriello
• Any updates for LH 149 ? cant make a ray...
falconpewpew
• Hello

Any update for LevelHelper 1.4.9?

THX
Balillas
[ 1 , 2 , 3 ]

# Unity Starter Kit!

We are considering writing a new starter kit on making a game in C# with Unity.

Would this be something you'd be interested in, and if so which option would you prefer?

... 12 total!

... 13 total!

... 52 total!

... 9 total!

... 11 total!

... 10 total!

... 9 total!

... 10 total!