Create Your Own Level Editor: Part 2/3

In this second part of the tutorial, you will implement a portion of the editing capabilities of your level editor. You’ll work through adding popup menus, dynamically positioning and sizing your objects on screen, and much more. By Barbara Reichart.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 7 of this article. Click here to view the first page.

Drawing Ropes On-screen

You now need to implement the code that draws the level elements on the screen. For the pineapple, this is relatively easy. However, the ropes are slightly more complex.

You might as well tackle the harder problem first — start with the ropes! :]

The ropes can have variable length and orientation. The easiest way to draw them would be to use one sprite and scale and rotate it accordingly. However, this can end up looking rather ugly, as shown in the example below:

Rotating and stretching the rope sprite to any orientation: Looking ugly.

What you want is to have the ropes sized consistently. The easiest way to do this is to use a small rope segment graphic that is chained or tiled so as to create a rope of any desired length.

In order to encapsulate this drawing code, create a new Objective-C class in the LevelEditor group. Name the class RopeSprite and make it a sub-class of NSObject.

Switch to RopeSprite.h and replace its contents with the following:

#import "cocos2d.h"
#import "Constants.h"
#import "RopeModel.h"

@interface RopeSprite : NSObject

@property (readonly, getter = getID) int id;

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel;

-(int)getID;
-(CGPoint)getAnchorA;
-(CGPoint)getAnchorB;
-(int)getBodyAID;
-(int)getBodyBID;

-(void)setAnchorA:(CGPoint)anchorA;
-(void)setAnchorB:(CGPoint)anchorB;

@end

This defines some essential methods and properties. Most of the methods are simple getters and setters.

You might ask, “Why aren’t we simply using properties?”. Properties are easy ways to implement getters and setters; however, in this case you want to add custom code to the setter so that it redraws the rope when a property is changed.

Note: You could have used properties and then overridden the getter and setter in your implementation. However in this tutorial the getter and setter approach used above does a better job of explaining each implementation step in detail.

Switch to RopeSprite.m and replace its contents with the following code:

#import "RopeSprite.h"
#import "CoordinateHelper.h"

@interface RopeSprite () {
    CCSprite* ropeSprite;
    RopeModel* ropeModel;
}

@end

@implementation RopeSprite

@end

The above adds some private variables to RopeSprite. The CCSprite is required to draw the rope. Additionally, you need a RopeModel, which gives you all the placement information for the rope.

Now add the following to RopeSprite.m:

-(id)initWithParent:(CCNode*)parent andRopeModel:(RopeModel*)aRopeModel {
    self = [super init];
    if (self) {
        ropeModel = aRopeModel;
        
        ropeSprite = [CCSprite spriteWithFile:@"rope_texture.png"];
        ccTexParams params = {GL_LINEAR,GL_LINEAR,GL_REPEAT,GL_CLAMP_TO_EDGE};
        [ropeSprite.texture setTexParameters:&params];
        
        [self updateRope];
        [parent addChild:ropeSprite];
    }
    return self;
}

The above method takes two parameters. The parent parameter is the reference to the node on which the rope will be drawn. This parameter is followed by the model, which contains all information required to draw the rope. The code then stores the rope model in an instance variable.

Next, the code creates a CCSprite from the file “rope_texture.png”. This image file contains only one small segment of the rope. The whole rope is then drawn by repeating this sprite over and over.

You could accomplish the same thing by drawing the same sprite several times, but this would require a large amount of code. Instead, it’s much more efficient to set up a rope texture to handle the drawing.

Textures in OpenGL have a few parameters that you might not be familiar with. The ccTexParams struct has the following fields:

OpenGL texture parameters

OpenGL texture parameters

minFilter is used when the surface the texture is drawn upon is smaller than the texture itself, whereas magFilter is used when the texture is smaller than the surface.

The level editor uses GL_LINEAR for both minFilter and magFilter. GL_LINEAR returns the weighted average of the four texture elements that are closest to the center of the pixel being textured. In other words, with this setting OpenGL uses linear interpolation to calculate the values of the pixels.

The wrapS and wrapT parameters let you set the wrapping behavior of the texture in the s and t coordinate.

Note: If you haven’t worked with OpenGL directly, you’re probably wondering what s and t stand for. In OpenGL the x, y, and z coordinate are used to define the position of an object in 3D space. As it would be confusing to reuse the x and y coordinate names for the texture coordinates, they’re simply re-labeled s and t.

You want the rope to repeat itself along the s-axis, which is done using the GL_REPEAT value. Along the t-axis, you want it to display just once without any scaling. For this, you use the GL_CLAMP_TO_EDGE value.

Note: There are several other filter values for texture parameters. You can look them up in the OpenGL Documentation.

You now have a CCSprite with a texture which will repeat along one axis while it stays unchanged along the other axis. That’s pretty neat! :]

The only thing you need to do now in order to display the rope properly on the screen is to update its length and rotation. This magic takes place in updateRope, which you’ll implement below.

Add the updateRope implementation to RopeSprite.m as shown below:

-(void)updateRope {
    CGPoint anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
    CGPoint anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
    float distance = ccpDistance(anchorA, anchorB);
    CGPoint stickVector = ccpSub(ccp(anchorA.x,anchorA.y),ccp(anchorB.x,anchorB.y));
    float angle = ccpToAngle(stickVector);
    ropeSprite.textureRect = CGRectMake(0, 0, distance, ropeSprite.texture.pixelsHigh);
    ropeSprite.position = ccpMidpoint(anchorA, anchorB);
    ropeSprite.rotation = -1 * CC_RADIANS_TO_DEGREES(angle);
}

The code above uses ccpDistance to calculate the distance between the two anchor points, which equals the length of the rope. It then calculates the rotation angle of the rope with ccpToAngle, which takes a vector and translates it into an angle in radians.

Next, the code changes the rope texture’s rectangle using the rope length calculated above. Then it updates the rope’s position. Keep in mind that the anchor point is at the middle of the ropeSprite, so its position is exactly in the middle of the two anchor points.

Finally, the code sets the angle of the rope sprite. As the angle is currently in radians, you need to transform it into degree notation with CC_RADIANS_TO_DEGREES.

This is how you draw a rope having arbitrary length without using a texture that is stretched in ugly ways. While it requires more code than simple scaling, it looks far, far better. As an added bonus, you’ve probably learned something along the way about OpenGL that you can use in your other projects!

The RopeSprite class is almost done. All that’s left is to add the getter and setter calls.

Add the following code to RopeSprite.m:

-(void)setAnchorA:(CGPoint)theAnchorA {
    ropeModel.anchorA = [CoordinateHelper screenPositionToLevelPosition:theAnchorA];
    [self updateRope];
}

-(void)setAnchorB:(CGPoint)theAnchorB {
    ropeModel.anchorB = [CoordinateHelper screenPositionToLevelPosition:theAnchorB];
    [self updateRope];
}

-(int)getID {
    return ropeModel.id;
}

-(CGPoint)getAnchorA {
    return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
}

-(CGPoint)getAnchorB {
    return [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
}

-(int)getBodyAID {
    return ropeModel.bodyAID;
}

-(int)getBodyBID {
    return ropeModel.bodyBID;
}

The getters and setters simply call updateRope whenever the position of one of the rope anchors is changed. This will redraw the rope to reflect the changes.

Build and run your app! You should now see…oh, wait. That’s just the same empty screen as before, isn’t it?

iOS-Simulator.png

Why don’t you see anything new?

You’ve implemented the RopeSprite class — but you’re not using it yet to draw anything on screen! That comes next. :]

Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.