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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Create Your Own Level Editor: Part 2/3
65 mins
- Getting Started
- Creating The LevelEditor Class
- Adding the Editor Menu
- Drawing Ropes On-screen
- Drawing the Game Objects On-screen
- Detecting User Inputs: Touch, Movement and Long Press
- Adding the Popup Editor Menu
- Positioning the Menu
- Implementing Dynamic Popup Menu Positioning
- Adding New Game Objects - Pineapples
- Adding New Game Objects - Ropes
- Where To Go From Here?
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:
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:
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:
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 CCSprite
s 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:
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.