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 3 of 7 of this article. Click here to view the first page.

Drawing the Game Objects On-screen

After putting so much work into the rope drawing methods, you are nearly ready to draw the level.

There’s a new variable to add to LevelEditor.mm, an array that stores all of the rope sprites in the level.

First add the following import to the top of LevelEditor.mm:

#import "RopeSprite.h"

Now add the declaration of the new ropes array to the class extension (the @interface block) at the top of LevelEditor.mm:

    NSMutableArray* ropes;

Okay, now you have everything you need to draw the current level on the screen. Time to pull everything together and see the results of your hard work!

Add the following lines of code to drawLoadedLevel in LevelEditor.mm, replacing the existing TODO lines:

    // Draw pineapple
    for (PineappleModel* pineapple in fileHandler.pineapples) {
        [self createPineappleSpriteFromModel:pineapple];
    }
    // Draw ropes
    ropes = [NSMutableArray arrayWithCapacity:5];
    for (RopeModel* ropeModel in fileHandler.ropes) {
        [self createRopeSpriteFromModel:ropeModel];
    }

The above code simply iterates over all pineapples and ropes stored in the fileHandler. For each of the models you then create a visual representation.

Now implement the method that creates the pineapple sprites by adding the following method to LevelEditor.mm:

-(void)createPineappleSpriteFromModel:(PineappleModel*) pineappleModel {
    CCSprite* pineappleSprite = [CCSprite spriteWithSpriteFrameName:@"pineapple.png"];
    pineappleSprite.tag = pineappleModel.id;
    CGPoint position = [CoordinateHelper levelPositionToScreenPosition:pineappleModel.position];
    pineappleSprite.position = position;
    [pineapplesSpriteSheet addChild:pineappleSprite];
}

The above method creates a sprite that contains the pineapple graphic. It then retrieves the ID and position of the pineapple from the pineappleModel variable and assigns them to the pineappleSprite accordingly. Finally, it adds the pineapple sprite to the pineapplesSpriteSheet.

The method for creating the rope sprites follows the same logic.

Add the method below to LevelEditor.mm:

-(void)createRopeSpriteFromModel:(RopeModel*)ropeModel {
    CGPoint anchorA;
    if (ropeModel.bodyAID == -1) {
        anchorA = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorA];
    } else {
        PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyAID];
        anchorA = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
    }
    
    CGPoint anchorB;
    if (ropeModel.bodyBID == -1) {
        anchorB = [CoordinateHelper levelPositionToScreenPosition:ropeModel.anchorB];
    } else {
        PineappleModel* pineappleWithID = [fileHandler getPineappleWithID:ropeModel.bodyBID];
        anchorB = [CoordinateHelper levelPositionToScreenPosition:pineappleWithID.position];
    }
    
    RopeSprite* ropeSprite = [[RopeSprite alloc] initWithParent:ropeSpriteSheet andRopeModel:ropeModel];
    [ropes addObject:ropeSprite];
}

The method first determines the anchor position for the rope. If the bodyID is -1 it takes the anchorPosition value stored in the ropeModel. Otherwise, it uses the position of the pineapple with the given bodyID. Then it creates a RopeSprite instance using the information and adds it to the ropes array.

Build and run your game, and switch to the level editor. You should finally see something on the screen for all your hard work, as demonstrated in the screenshot below:

Sure looks finished, doesn’t it?

Detecting User Inputs: Touch, Movement and Long Press

Seeing things on screen is great and all, but you need some action too! Currently, you can’t actually do any level editing. You need to enable the level editor to handle user input.

The user interactions that you’ll handle in your editor are normal touches, drag & drop and long press.

First, add an instance variable to LevelEditor.mm to store the gesture recognizer that recognizes a long press:

    UILongPressGestureRecognizer* longPressRecognizer;

Now add the following code to LevelEditor.mm:

-(void)onEnter {
    [super onEnter];
    [[CCDirector sharedDirector].touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:NO];
    longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    UIView* openGLView = [CCDirector sharedDirector].view;
    [openGLView addGestureRecognizer: longPressRecognizer];
}

onEnter is called whenever the view switches to the LevelEditor layer. Here, you register the LevelEditor instance as a touch handler so that it will receive calls to input methods like ccTouchBegan and ccTouchEnded. Also create longPressRecognizer and add it to the openGLView as a gesture recognizer.

Since the level editor is the delegate for touch events, you need to add the relevant delegate methods that will later handle touch input.

Add the following code to LevelEditor.mm:

-(void)longPress:(UILongPressGestureRecognizer*)longPressGestureRecognizer {
    if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) {
        NSLog(@"longpress began");
    }
}

-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch began");
    return YES;
}

-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch moved");
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"touch ended");
}

longPress first checks the current state of the gesture recognizer. A gesture recognizer can have one of several different states. However, you only need to know when the long press began, so you just handle the UIGestureRecognizerStateBegan state. For the moment, this consists of a simple log message.

Only one more thing is missing: cleaning up after the user leaves the level editor.

Add the following code to LevelEditor.mm:

-(void)onExit {
    [super onExit];
    [[CCDirector sharedDirector].touchDispatcher removeDelegate:self];
    UIView* openGLView = [CCDirector sharedDirector].view;
    [openGLView removeGestureRecognizer: longPressRecognizer];
}

The above simply removes the layer as a touch dispatcher and also removes the gesture recognizer.

Build and run your project, and again switch to the editor mode. Take a look at the list of log messages produced in Xcode’s output window when you either tap, drag, or long press on the screen, as shown in the example below:

IOS Simulator

Adding the Popup Editor Menu

It feels pretty good to see all this in action, doesn’t it? However, log messages alone do not make an editor! Time to allow the user to interact with the objects on the screen.

The first problem to be solved is how to add new objects. You already determined that you need to be conservative with screen space. This is why you won’t have another menu on screen to select new items to be added. Instead, the editor will open a popup menu, which will allow the user to select between adding ropes or pineapples.

When the player taps the screen you want a popup menu to appear that allows the user to select between creating a pineapple or a rope. Here’s how it will look:

The popup menu

Create a new Objective-C class named PopupMenu with CCLayer as the super class.

Now switch to PopupMenu.h and replace its contents with the following:

#import "cocos2d.h"

@protocol PopupMenuDelegate

-(void)createPineappleAt:(CGPoint)position;
-(void)createRopeAt:(CGPoint)position;

@end

@interface PopupMenu : CCLayer

@property id<PopupMenuDelegate> delegate;

-(id)initWithParent:(CCNode*)parent;

-(void)setPopupPosition:(CGPoint)position;
-(void)setMenuEnabled:(BOOL)enable;
-(void)setRopeItemEnabled:(BOOL)enabled;

-(BOOL)isEnabled;

@end

The above code declares a new protocol. A protocol defines an interface between the PopupMenu and any other classes that want to be notified whenever the user selects an item in the menu.

This protocol defines two methods, createPineappleAt: and createRopeAt:, which will be called when an instance of the respective object is created.

The PopupMenu class definition adds a reference to an instance of PopupMenuDelegate. This will be the concrete instance that will be called when the user does something in your menu.

Open up PopupMenu.m and replace its contents with the following:

#import "PopupMenu.h"
#import "RopeSprite.h"

@interface PopupMenu () {
    CCSprite* background;
    CCMenu* menu;
    CCMenuItem* ropeItem;
    CGPoint tapPosition;
    BOOL isEnabled;
}

@end

@implementation PopupMenu

@end

As usual, this is simply a skeleton with the relevant imports and private variables. The variables add references to the background and to the menu. Additionally, there’s a pointer to the rope menu item which allows you to change the menu item state. You need this because creating a rope should only be possible if there is at least one pineapple you can tie it to.

Then there’s tapPosition, which will store the screen position where the player tapped to open the popup menu. This is the location where the arrow of the popup menu will point. isEnabled indicates whether the popup menu is currently visible on the screen for the player to tap.

Now add the following code to PopupMenu.m:

-(id)initWithParent:(CCNode*) parent {
    self = [super init];
    if (self) {
        CCSprite* pineappleSprite = [CCSprite spriteWithFile:@"pineappleitem.png"];
        CCSprite* pineappleSpriteSelected = [CCSprite spriteWithFile:@"pineappleitem.png"];
        pineappleSpriteSelected.color = ccc3(100, 0, 0);
        CCMenuItemImage* pineappleItem = [CCMenuItemImage itemWithNormalSprite:pineappleSprite selectedSprite:pineappleSpriteSelected target:self selector:@selector(createPineapple:)];
        
        CCSprite* ropeSprite = [CCSprite spriteWithFile:@"ropeitem.png"];
        CCSprite* ropeSpriteSelected = [CCSprite spriteWithFile:@"ropeitem.png"];
        CCSprite* ropeSprite3 = [CCSprite spriteWithFile:@"ropeitem.png"];
        ropeSpriteSelected.color = ccc3(100, 0, 0);
        ropeSprite3.color = ccc3(100, 100, 100);
        ropeItem = [CCMenuItemImage itemWithNormalSprite:ropeSprite selectedSprite:ropeSpriteSelected disabledSprite:ropeSprite3 target:self selector:@selector(createRope:)];
        
        menu = [CCMenu menuWithItems: pineappleItem, ropeItem, nil];
        background = [CCSprite spriteWithFile:@"menu.png"];
        [background addChild:menu z:150];
        [self addChild:background];
        [parent addChild:self z:1000];
        [self setMenuEnabled:NO];
    }
    return self;
}

This new method takes a CCNode as parameter. This node will be the parent of the popup menu. The rest of the method implementation is relatively straightforward; it creates some CCSprites and a CCMenu and adds them to the parent node. The method also disables the menu since it should only appear and be enabled when requested by the user.

The image below shows the parts that combine to make the popup menu:

Parts of the popup menu.

To start, you have the background image. The background image consists of a bubble (containing the menu) and an arrow (the anchor point for the menu should be set to tip of this arrow). The menu contains two menu items: one for the pineapple, the other for the rope.

Barbara Reichart

Contributors

Barbara Reichart

Author

Over 300 content creators. Join our team.