How to Make an RPG

Brandon Trebitowski Brandon Trebitowski

This post is also available in: Spanish

Learn how to make a RPG game for iOS!

Learn how to make a RPG game for iOS!

For as long as I can remember, I’ve been in love with role-playing games. There’s something so magical about an epic storyline set in a vast, intricate world.

But when it comes to actually programming a RPG, it’s easy to get intimidated by that same epic complexity!

Well, there’s some good news. There are now some killer tools, frameworks and tutorials to make this a lot easier than it used to be.

In this tutorial, you’ll learn how to use these frameworks to create your own reusable RPG engine. By the time you’re done, you’ll have the groundwork to create the RPG of your dreams!

This tutorial uses the Cocos2D game framework for iOS. Although you can follow along with this tutorial even if you’ve never used Cocos2D before, to get the most of this tutorial it helps to have some background knowledge. I recommend these three tutorials:

  1. How To Make a Simple iPhone Game with Cocos2D 2.X
  2. How To Make a Tile-Based Game with Cocos2D 2.X
  3. How To Use Animations and Sprite Sheets in Cocos2D

And that’s it – are you ready to roll the dice?

Getting Started

Your first step is an easy one: download the starter project.

Unzip the file and open MiniRPG.xcodeproj in Xcode. You’ll see something like this:

You can tap the screen and the hero will move in the direction you tap with some basic animation – but that’s about all it does for now. The rest is up to you!

Before you begin, let’s take a quick tour of the starter project so you understand how it works so far.

Resources\Characters

Character Animations Sprite Sheet

Character Animations Sprite Sheet

The Resources\Characters directory contains the sprite sheet for the hero’s animations (character.plist and character.pvr). The artwork was made for the Liberated Pixel Cup by various artists. The individual attributions can be found on each tileset’s respected page here.

This sprite sheet was made with a tool called TexturePacker.

Note: If you want to learn more about sprite sheets and Texture Packer, check our our How To Use Animations and Sprite Sheets in Cocos2D tutorial.

Resource\Tilemaps

The Resource\Tilemaps directory contains the tiles for the tile map (terrain_atlas.png, meta_tiles.png, obj_misk_atlas.png, and build_atlast.png). As mentioned above, the license for each tileset can be found here.

This directory also contains the tile map files themselves (town.tmx and room.tmx) that were created with the Tiled map editor. Download Tiled if you don’t have it already and open room.tmx. You will see the following:

RPG map made with Tiled Map Editor

You will see there are four layers in the map:

  • meta: If a tile is collidable, a special “transparent red” tile is drawn in this spot. It will not appear in the game – it’s only used as an easy way of tracking what’s collidable or not.
  • exit: Contains the exit to the room.
  • items: Decorations in the room like the bookshelf, stove, and bed.
  • walls: The walls of the room.
  • floor: The floor of the room.

Most of the layers are laid out in such a way to organize the tiles for layering. For example, the items layer is above the floor layer because you want to see the items on top of the floor. You will see more complex examples of this by opening up the Town.tmx file where I have things like ground and ground-helper to aid in drawing the environment.

Note: If you want to learn more about Tiled and making tile-map games in general, check out our How To Make A Tile Based Game with Cocos2D 2.X tutorial.

GameLayer.m

GameLayer.m contains the main game logic for the starter project so far. It does a few things so far:

  • init and loadMapNamed add the tile map and the hero to the layer.
  • update and setViewpointCenter keep the hero focused in the middle of the screen.
  • ccTouchEnded figures out where to move the player based on the tap, and calls setPlayerPosition appropriately.
  • setPlayerPosition checks the tile map’s “meta” layer to see if a given tile is collidable or not. If it’s not, it uses a move action to move the hero toward that tile and runs the appropriate animation based on the direction the sprite is moving in (by calling playerHeroMoveAnimationFromPosition).

Browse through the code until you have a good understanding of how it works. If there is anything you don’t understand, I strongly encourage you to read the tutorials mentioned in the introduction to make things a bit clearer.

That’s it for the starter project. At this point, your hero is safe and warm in their little room. But where should he go from here?

Your Quest, Should You Choose to Accept It

Over the course of this tutorial, not only will you learn how to make an RPG, you’ll also make a reusable RPG game engine that’s easy to expand and even rebrand for completely different games.

You will be adding the following components to this project to turn it into a full-fledged game:

  • Room navigation: Your hero doesn’t want to stay in this little room forever – he yearns for adventure! You’ll learn how to add doors to the map and change the character’s location when they cross the threshold.
  • NPC scripting: The game would be lonely (and boring) without other characters. You’ll learn how to create non-player characters (NPCs) that will drive the story and change their speech based on the player’s actions in the game. This will be done entirely in an easy-to-use scripting language called Lua that is ideal for scripting logic like this.
  • Questing: You’ll learn how to develop a reusable questing system that you can use in your own games. The system will also make it easy to add more areas and quests to your existing games in future updates.

Just like in an RPG, you can see how your quest to implement these features will lead to many other side quests – all for fun, fortune and glory, of course!

From Room to Room

Since the game uses a base tile map, switching between rooms is a simple matter of swapping out the tile map from under the character when they have walked over a certain area such as a door or a staircase.

Before you can switch to another area, though, you need to add two things:

  1. You need to define the location of the exit on the tile map;
  2. You need to build another area for your player to enter.

First, create an exit. Open room.tmx in the Tiled map editor. Below the Layers section, click the button to make a new Layer, and choose Add Object Layer, as shown in the screenshot below:

Note that this is different than the existing exit layer (a normal Tile Layer), because this new layer is an Object Layer. Object Layers allow you to define areas in the map with key/value properties that you can then easily retrieve in code. This is ideal for defining the area of the map where the exit is and defining where it should warp the player to.

Select the Insert Rectangle tool and draw a square over the stairs at the bottom of the room.

Now right-click on the square and select Object Properties. Set both the name and type to exit and add the following name/value pairs to the list of properties below:

  • destination: town
  • startx: 3
  • starty: 15

The destination specifies the name of the tile map that you want to load when the user steps onto this exit. The startx and starty properties specify the tile into which the user will spawn when switching between rooms. This is important to show that the user is coming from a door and not just randomly spawning on the map.

Save room.tmx. Now to step two – building another area for your player to enter.

Lucky for you, the town.tmx file is already included in the project. Open it up in Tiled and have a look around, but don’t make any changes to it yet!

Town RPG Map made with Tiled Map Editor

Remember that tile coordinate (0,0) in Tiled is the bottom left of the map. So coordinate (3, 15) is the area right below the door – count it out for yourself to see.

Warp Speed Ahead!

Now that you have an exit defined and another room for the user to enter, you need to add some code to check when the hero hits this spot, and transition to a new map.

First, since the exits layer is an object group, you need to define a CCTMXObjectGroup in order to access it. Back in Xcode, open GameLayer.m and in the private interface at the top of the file, add the following line:

@property (nonatomic, strong) CCTMXObjectGroup *exitGroup;

Next add the following line to the bottom of the loadMapNamed method:

self.exitGroup = [self.tileMap objectGroupNamed:@"exits"];

This gives you a handy reference to the new exits object layer that you created in the Tile Map. Now you just need to detect when the user steps on those exits.

Add the following code to the heroIsDoneWalking method:

// 1
NSArray *exits = self.exitGroup.objects;
for(NSDictionary *exit in exits)
{
    // 2
    CGRect exitRect = CGRectMake([exit[@"x"] floatValue], [exit[@"y"] floatValue],
                                 [exit[@"width"] floatValue], [exit[@"height"] floatValue]);
    // 3
    if(CGRectContainsPoint(exitRect, self.hero.position))
    {
        // 4
        NSString *name = exit[@"destination"];
        CGPoint heroPoint = CGPointMake([exit[@"startx"] floatValue] * self.tileSize + (self.tileSize/2), [exit[@"starty"] floatValue] * self.tileSize + (self.tileSize/2));
 
        self.hero.position = heroPoint;
        [self loadMapNamed:name];
        return;
    }
}

Here’s what you are doing:

  1. Enumerate through the exits in the exit group.
  2. Convert the exit’s frame (the rectangle you drew) to a CGRect.
  3. Check to see if the player is standing in the exit.
  4. If so, fetch the name of the room and the new position for the player. Load the new room and update the player’s position.

Build and run the project. You should now be able to walk out of the door and into the town. Congratulations, you’re no longer a recluse!

Forever Alone No More!

When I first started developing games, the idea of NPCs boggled my mind. How did developers build so many characters capable of such complex interactions, particularly when it came to questing?

Well, I’ve come up with a rather simple and robust method for doing just that.

You see, when you are writing gameplay logic like this, it’s often easier to do so in a lightweight scripting language than a strongly typed language like Objective-C. It makes your code much faster to write and tweak, which is critical when you’re changing things a lot like you will when creating NPCs, quests, and so on.

So in this part I am going to show you how to integrate a scripting language called Lua into your game. As I mentioned earlier, Lua is ideal for this purpose, and is really easy to integrate into Xcode projects.

Here’s how it will work. You’ll add NPCs to the tile map inside of Tiled and give each of them a name. When your character approaches an NPC, the game will invoke the NPC’s Lua code. You’ll be building a simple bridge so that your Lua code can call upon your Objective-C code to do things like store key/value pairs, display chat boxes, and so on.

With this system in place, you can easily script an entire game with minimal changes to your engine.

All About Lua

Before you begin, let’s take a moment to discuss how the Lua integration will work at a high level.

Integrating LUA into an Xcode project

Lua is a very high level scriping language made to run in just about any environment. It only has one complex type (called a table) which can be used to build high level data structures such as arrays, hashes, objects, etc. You can learn more about Lua on their official website.

Lua ships as a bunch of .c and .h files for it’s runtime that get compiled directly into your project. In order to get Objective-C and Lua talking, you are going to be using an open source class called LuaObjBridge. Although it’s no longer maintained, it will work perfectly for your purposes.

What this class does is makes use of Objective-C’s reflection ability to dynamically “link up” objective-c classes/methods/properties to the Lua runtime. This gives you the ability to call upon them using a specific syntax (to be discussed later).

For your game, you are going to be scripting the entire storyline (which is pretty shallow) and all of the interaction between the player and the NPCs.

Bring On the NPCs

Now that you have a basic understanding of how the Lua integration will work and why you’re using Lua, let’s start adding some NPCs into the game!

To do this, first open room.tmx inside of Tiled.

Find the soldier tileset and right-click on one of the soldier tiles. Select Tile Properties.

Add a new key/value pair with “name” and “soldier”:

Create a new tile layer called npc. Now you can add the soldier to the layer like a stamp in three steps:

  1. Click on the npc layer to ensure it’s selected.
  2. Select the soldier tile to which you added the name property.
  3. Then click somewhere in the room to place the soldier. Make sure you put him somewhere the player will be able to access!

Having the NPC in the game is a good first step, but there are still some issues. For one, the user can pass right through them. Start by preventing the user from standing on top of the NPC.

Head back to Xcode and open GameLayer.m. Add the following line to the private class interface at the top of the file:

@property (nonatomic, strong) CCTMXLayer *npcLayer;

Then add the following line at the bottom of the loadMapNamed method:

self.npcLayer = [self.tileMap layerNamed:@"npc"];

Finally, add the following code after the //Check walls section in the setPlayerPosition method:

tileGid = [self.npcLayer tileGIDAt:tileCoord];
if(tileGid)
{
    NSDictionary *properties = [self.tileMap propertiesForGID:tileGid];
    NSString *name = [properties objectForKey:@"name"];
    // TODO: Interact with NPC
    return;
}

This code simply prevents the user from walking into the NPC. Build and run and you should see that the player stops when approaching the soldier.

You shall not pass!

Managing NPCs With Lua

Now that the NPCs are in place, they need to do more than stand around and block the player character. You’ll handle all the interaction through a new class called NPCManager. The interactivity is where Lua scripting comes into play.

The starter project already includes Lua and a Lua-ObjC bridge that allows Lua scripts to use your native Objective-C objects and call methods on them. Here are the steps I took to add them to the project in case you are adventurous and want to add Lua to your existing project:

  1. Download Lua 5.1 here and extract it. Newer versions of Lua don’t work with the bridge.
  2. Copy the src folder into your project’s folder and drag all of the files into your project (don’t check to have XCode copy them in).
  3. Delete the following files: lua.c, luac.c, print.c, Makefile . Leaving these in will cause issues.
  4. Download LuaObjCBridge from here.
  5. Drag LuaObjCBridge.h and LuaObjCBridge.m (in the trunk folder) into your project and let XCode copy them in.

Aren’t you glad I did all that for you? ;)

Add a new class to the project called NPCManager and make it a subclass of NSObject. Replace the contents of NPCManager.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
 
@class GameLayer;
 
@interface NPCManager : NSObject
@property(nonatomic, strong) NSMutableDictionary *npcs;
 
- (id) initWithGameLayer:(GameLayer *)layer;
- (void) interactWithNPCNamed:(NSString *) npcName;
- (void)loadNPCsForTileMap:(CCTMXTiledMap *) map named:(NSString *) name;
@end

I will explain each of these methods as you implement them. Now open up NPCManager.m and replace the contents with the following code:

// 1
#import "cocos2d.h"
#import "NPCManager.h"
#import "LuaObjCBridge.h"
#import "GameLayer.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
 
// 2
@interface NPCManager()
@property(nonatomic) lua_State *luaState;
@property(nonatomic, strong) GameLayer *gameLayer;
@end
 
@implementation NPCManager
 
- (id)initWithGameLayer:(GameLayer *)layer
{
    if (self = [super init]) {
        self.npcs = [@{} mutableCopy];
        self.gameLayer = layer;
 
        // 3
        self.luaState = lua_objc_init();
 
        // 4
        lua_pushstring(self.luaState, "game");
        lua_objc_pushid(self.luaState, self.gameLayer);
        lua_settable(self.luaState, LUA_GLOBALSINDEX);
    }
    return self;
}
 
@end

Here’s what’s going on in the code above:

  1. These are all of the includes you need to make the manager work. Don’t worry about the nerdy C headers.
  2. You’ll use the luaState property to interact with the Lua system; gameLayer is your familiar GameLayer class.
  3. Initialize the Lua system.
  4. Send the Game object into Lua by adding it to the top of the Lua stack. The lua runtime uses a stack to manage variables, objects, and method calls. To run something, it must first get pushed on to the stack. So first, you push a string called “game” telling Lua that “game” is what you are going to call the object you are about to put on the stack. Then, you push the game object itself. Remember, the bridge handles all of the heavy lifting behind the scenes. The last call just tells Lua to make the game object global so you can access it from any Lua file.

Now comes the tricky part. Before you can interact with the Lua system, you need a way to call upon it. Add the following two methods to your NPCManager class:

/**
 * Executes Lua code and prints results to the console.
 */
- (void) runLua:(NSString *) luaCode
{
    char buffer[256] = {0};
    int out_pipe[2];
    int saved_stdout;
 
    // Set up pipes for output
    saved_stdout = dup(STDOUT_FILENO);
    pipe(out_pipe);
    fcntl(out_pipe[0], F_SETFL, O_NONBLOCK);
    dup2(out_pipe[1], STDOUT_FILENO);
    close(out_pipe[1]);
 
    // Run Lua
    luaL_loadstring(self.luaState, [luaCode UTF8String]);
    int status = lua_pcall(self.luaState, 0, LUA_MULTRET, 0);
 
    // Report errors if there are any
    report_errors(self.luaState, status);
 
    // Grab the output
    read(out_pipe[0], buffer, 255);
    dup2(saved_stdout, STDOUT_FILENO);
 
    // Print the output to the log
    NSString *output = [NSString stringWithFormat:@"%@",
      [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding]];
    if(output && [output length] > 2)
    {
        NSLog(@"Lua: %@",output);
    }
}
 
/**
 * Reports Lua errors to the console
 */
void report_errors(lua_State *L, int status)
{
    if ( status!=0 ) {
        const char *error = lua_tostring(L, -1);
        NSLog(@"Lua Error: %s",error);
        lua_pop(L, 1); // remove error message
    }
}

This tutorial won’t go over this code in too much detail. If you’re dying to know more about it, you can reach out to me on Twitter.

Just know and trust that it will run a string of Lua code that is passed in to the runLua: method. For example:

[self runLua:@"print(\"Hello Lua\")"];

The code above will print the text “Hello Lua” to the console.

Now that Lua is hooked up, you’ve got three more methods to implement in this class. Add the following methods to NPCManager.m:

/**
 * Loads all NPCs on a given tile map.  Initialized empty Lua table to hold
 * NPCs in Lua.
 */
- (void)loadNPCsForTileMap:(CCTMXTiledMap *) map named:(NSString *) name
{
    // Reset NPCs for the current map
    [self runLua:@"npcs = {}"];
 
    [self loadLuaFilesForMap:map layerName:@"npc" named:name];    
}
 
/**
 * For a given layer on a tile map, this method tries to load files of the format:
 * [MapName]-[NPCName].lua
 *
 * Lua files are responsible for initializing themselves and adding themselves to the
 * global npcs table.
 *
 * All Lua objects in the npcs table must have an interact method that will be invoked when
 * the player interacts with them.
 */
- (void) loadLuaFilesForMap:(CCTMXTiledMap *) map layerName:(NSString *) layerName named:(NSString *) name
{
    NSFileManager *manager = [NSFileManager defaultManager];
    CCTMXLayer *layer = [map layerNamed:layerName];
 
    // Enumerate the layer
    for(int i = 0; i < layer.layerSize.width; i++)
    {
        for(int j = 0; j < layer.layerSize.height; j++)
        {
            CGPoint tileCoord = CGPointMake(j,i);
            int tileGid = [layer tileGIDAt:tileCoord];
 
            // Check to see if there is an NPC at this location
            if(tileGid)
            {             
                // Fetch the name of the NPC
                NSDictionary *properties = [map propertiesForGID:tileGid];
                NSString *npcName = [properties objectForKey:@"name"];
 
                // Resolve the path to the NPCs Lua file
                NSString *roomName = [name stringByReplacingOccurrencesOfString:@".tmx" withString:@""];
                NSString *npcFilename = [NSString stringWithFormat:@"%@-%@.lua",roomName,npcName];
                NSString *path = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"npc"] stringByAppendingPathComponent:npcFilename];
 
                // If the NPC has a Lua file, initialize it.
                if([manager fileExistsAtPath:path])
                {
                    NSError *error = nil;
                    NSString *lua = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
                    if(!error)
                    {
                        [self runLua:lua];
                    }
                    else
                    {
                        NSLog(@"Error loading NPC: %@",error);
                    }
                }
                else
                {
                    NSLog(@"Warning: No Lua file for npc %@ at path %@",npcName,path);
                }
            }
        }
    }
 
}

The code above is pretty well commented, so I won’t go into further explanation. The gist of it is, you pass a tile map and the name of the tile map into loadNPCsForTileMap:named:, which loads Lua files inside a folder called npc in the bundle path.

Note that all NPCs must follow the naming convention [:map_name]-[:npc_name].lua. You’ll return to this idea in a bit.

Also note the line “[self runLua:@"npcs = {}"];” . This sets up a global table in the Lua system to hold all of the NPC objects that will be loaded from files.

The final method you need to implement is the one that your GameLayer class will call to interact with the NPC. Add this method to NPCManager.m:

- (void) interactWithNPCNamed:(NSString *) npcName
{
    NSString *luaCode = [NSString stringWithFormat:@"npcs[\"%@\"]:interact()",npcName];
    [self runLua:luaCode];
}

Now back up a minute before considering what this method does. When an NPC is loaded into the system, they get added to a global Lua table called (you guessed it) npc.

Lua tables are like dictionaries. So if you have an NPC named soldier, it would look like this to Lua:

    npc["soldier"]:interact()

This code looks up a soldier Lua object in the global NPC table and calls the interact method on it. Of course, this assumes that your soldier object has an interact method – rest assured you’ll get to that soon!

With that explanation in mind, you can see what the interactWithNPCNamed: method does. It looks up the NPC by name in the Lua table and calls the NPC’s interact method.

You’ll have to wait just a bit longer to see the results of all this work, but don’t lose heart!

Start Your Script Engine!

Well, you have made it through that section: you’ve earned enough experience points for a level-up! Even better, how about a scriptable RPG engine? ;]

It’s time to learn how to use the NPCManager in the GameLayer. Open GameLayer.m and add the following import to the top of the file:

#import "NPCManager.h"

Add the following property to the private class declaration:

@property(nonatomic, strong) NPCManager *npcManager;

Add the following line to the init method, at the top of the if block:

self.npcManager = [[NPCManager alloc] initWithGameLayer:self];

Here’s where the connection takes place. At the end of the loadMapNamed method, add the following line:

[self.npcManager loadNPCsForTileMap:self.tileMap named:name];

This loads the set of NPCs for the newly loaded tile map. When the player changes rooms, the new NPCs get loaded with the new room.

You can start to see the need for the NPC naming convention here. You might have the soldier in your room as well as in the town, but they will interact with the player quite differently, so they would be scripted in separate files named room-soldier.lua and town-soldier.lua.

The final step is to invoke the Lua code of the NPC with which you wish the player to interact. In the setPlayerPosition: method, replace the //TODO Interact with NPC comment with the following line:

[self.npcManager interactWithNPCNamed:name];

When the player touches to move toward an NPC, the NPC’s name is looked up in its list of properties. This name is passed to the interactWithNPCNamed: method in order to invoke the NPC’s interact Lua method.

Writing Some Lua (Finally)

Now that you have full support for the Lua system, it’s time to actually write some Lua.

Right-click on the npc folder in the Xcode Project Navigator and click New File…. Choose the iOS\Other\Empty template. Name the file room-soldier.lua and make sure to save it into the npc folder inside of your project directory!

Add the following code to the new empty room-soldier.lua file:

-- 1
soldier = {}
-- 2
function soldier:new(game)  
    local object = { 
        game = game,
    }
    setmetatable(object, { __index = soldier })
 
    return object
end 
-- 3
function soldier:interact()
    print "Hello soldier!"
end
 
-- 4
soldier = soldier:new(game)	
npcs["soldier"] = soldier

Here’s what’s going on:

  1. Lua only has one complex data type and that’s a table. So it seems natural to use a table to create an “object”.
  2. This is the constructor for the soldier class. You can read up on metatables here to learn more about what’s going on. It’s just some boilerplate stuff.
  3. This is your interact method, the main way that the player interacts with the NPC.
  4. Finally, you instantiate a new soldier object and add it to the global NPC table you set up earlier. Note that the constructor takes a game variable, but where did that come from? Remember that when you initialized Lua, you put your GameLayer object on the Lua stack with the name of game. Since it’s global, you can just go ahead and pass it in here.

This is the moment you’ve been waiting for – a build and run! So do it! Then walk over to the soldier to interact with him. Once you do, you should see the following printed to the console:

Lua: Hello soldier!

Hooray! You have just interacted with your first NPC via Lua!

Getting Chatty

Your current game is great, as long as you don’t mind playing a game with Xcode’s debug window open. :]

You’re going to change that now by building a chat box that the NPC can call upon to “talk” to the player.

Here’s a screenshot of what you’re going to build:

Start by adding a new class called ChatBox and make it a subclass of CCNode. Replace the contents of ChatBox.h with the following code:

#import "cocos2d.h"
 
@interface ChatBox : CCNode
 
- (id) initWithNPC:(NSString *) npc text:(NSString *) text;
- (void) advanceTextOrHide;
 
@end

The first method takes an NPC name and the text to display. The second method either advances the text or hides it – more on that momentarily.

Next add the implementation by replacing the contents of ChatBox.m with the following code:

#import "ChatBox.h"
 
@interface ChatBox ()<CCTargetedTouchDelegate>
@property(nonatomic, strong) NSString *npc;
@property(nonatomic, strong) NSMutableArray *textArray;
@property(nonatomic, strong) CCLabelTTF *label;
@end
 
@implementation ChatBox
 
- (id) initWithNPC:(NSString *)npc text:(NSString *)text
{
    if (self = [super init])
    {
        self.npc = npc;
        // 1
        self.textArray = [[text componentsSeparatedByString:@""] mutableCopy];
        // 2
        CCSprite *backroundSprite = [CCSprite spriteWithFile:@"chat-box.png"];
        [backroundSprite.texture setAliasTexParameters];
        backroundSprite.scale = 8;
        backroundSprite.position = ccp(0,240);
        backroundSprite.anchorPoint = ccp(0,0);
        [self addChild:backroundSprite z:0];
        // 3
        self.label = [CCLabelTTF labelWithString:@"" dimensions:CGSizeMake(460, 60) hAlignment:UITextAlignmentLeft lineBreakMode:UILineBreakModeWordWrap fontName:@"Helvetica" fontSize:24];
        self.label.color = ccWHITE;
        self.label.anchorPoint = ccp(0, 0);
        self.label.position = ccp(10,250);
 
        [self addChild:self.label z:1];
    }
 
    return self;
}
@end

Here’s a breakdown of the above code:

  1. When NPCs in a game have something to say, the text usually takes up more than one line. Here you employ a strategy to solve this by splitting up the string on newline (\\n) characters. This way, the UI shows only one segment of the string at a time and advances the text to the next line when the user taps on the chat box. When the array becomes empty, the chat box hides itself.
  2. Draws the background of the chat box.
  3. Draws the text label.

Now add the following method to facilitate the text advancement process:

- (void) advanceTextOrHide
{
    // 1
    if(self.textArray.count == 0)
    {
        [self setVisible:NO];
        [self.parent removeChild:self cleanup:YES];
        return;
    }
 
    // 2
    NSString *text = self.textArray[0];
    [self.textArray removeObjectAtIndex:0];
 
    // 3
    NSString *message = [NSString stringWithFormat:@"%@: %@",[self.npc uppercaseString], text];
    [self.label setString:message];
}

The code above does the following:

  1. If the text array is empty, you hide the chat box.
  2. Here you pop the next chat string off of the stack.
  3. You display the new chat string.

Now you need to allow the NPC to create a new chat box and send it string messages. You can do this inside of the GameLayer class since your NPC has access to all of its methods.

Add an additional import at the top of GameLayer.m:

#import "ChatBox.h"

Add the following property to the private interface:

@property(nonatomic, strong) ChatBox *chatbox;

Add the following method:

- (void) npc: (NSString *)npc say:(NSString *) text
{
    self.chatbox = [[ChatBox alloc] initWithNPC:(NSString *)npc text:text];    
    [self.parent addChild:self.chatbox];
    [self.chatbox advanceTextOrHide];
}

An NPC can call this method to instantiate a chat box and display it. Here’s an example of how the NPC would call it:

self.game:npc_say("soldier","Please, save the princess!")
-- equivalent to [gameLayer npc:@"soldier" say:@"Please, save the princess!"];

Let’s break this down:

  • self: You are assuming the NPC is calling this from inside their interact method. This is just a self reference inside of the class.
  • game: A reference to the game that was passed in on initialization.
  • npc_say: This is where things get interesting. The Lua bridge uses Objective-C reflection to generate these sorts of Lua methods to map to your class methods. Anywhere there is a space and another argument in your Objective-C method, you replace it with an underscore in Lua. Finally, all parameters are passed in (in order) at the end.

Now you need to add touch interaction so that the player can advance the text or hide the chat box. Add the following lines to the beginning of ccTouchesEnded:withEvent:

if(self.chatbox && self.chatbox.visible)
{
    [self.chatbox advanceTextOrHide];
    return;
}

The final step is to have your soldier NPC call upon this method. Open room-soldier.lua and replace the print line in the interact method with the following:

self.game:npc_say("soldier","Please, save the princess!")

Build and run!

Your NPC can talk!

You can now repeat the process of adding NPCs to the map (inside any room/tilemap) and writing their corresponding Lua files.

One could argue that at this stage, you have enough knowledge to make a complete (though very basic) RPG game. But don’t head off to the tavern just yet – you’re about to go a few steps further to add more complex interactivity.

Quest-Making Power

What’s an RPG without questing? Since you have the groundwork for your game and interaction, the last phase is to build out an API with which your Lua characters can interact.

This way, you can make your NPCs say or do different things based on the stage of the quest the hero is on.

To build quests, you need to be able to keep track of quest progress. For this simple game, NSUserDefaults is a quick and easy way to do this. If you like, you can implement something more advanced with Core Data, sqlite or some network-backed storage system.

Think for a moment about what kind of data you’ll need to store. Say, for example, that you have a quest where the player is tasked to deliver a letter from “Kid” to “Old Man.”

Well, when the player speaks to the Old Man without having first received the quest from the Kid, the Old Man might respond with, “Nice weather we’re having.” He will know that the player doesn’t have a letter for him by checking a specific key in the NSUserDefaults (or database).

The Kid will give the player a quest by setting a value for a specific key. Then, when the player revisits the Old Man, the Old Man will see that value, clear it out and reward the player (perhaps by updating the value of a money key).

When you start to think about the game from this perspective, the possibilities are limited only by your ability to come up with stories! Let’s see how to implement such methods for storage that will aid you in your quest for quest-making power.

Add the following two methods to GameLayer.m:

- (void) setMeta:(NSString *)value forKey:(NSString *)key
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:value forKey:key];
    [defaults synchronize];
}
 
- (NSString *) getMetaValueForKey:(NSString *)key
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    return [defaults objectForKey:key];
}

These are just some helper methods to get and store some meta information. Think of it as a global dictionary for information shared across your LUA objects.

That’s it! You now have a basic storage mechanism for your game. These two methods provide you with a simple wrapper around NSUserDefaults. setMeta:forKey: stores a key/value pair and getMetaValueForKey: retrieves the value.

In theory, you could save the entire game state using only these two methods.

Now to make the player’s interaction with the soldier more interesting using these methods. Open up room-soldier.lua and replace the interact method with the following code:

function soldier:interact()
    if self.game:getMetaValueForKey("room_soldier_greeting") == "true" then
        self.game:npc_say("soldier","Please return her home safely.")
    else
        self.game:npc_say("soldier", "You must save the princess! She has been taken!")
        self.game:setMeta_forKey("true","room_soldier_greeting")
    end
end

The first time the player interacts with the soldier, the “room_soldier_greeting” key won’t be set and he will respond with “You must save the princess! She has been taken!” The key will then be set to “true” so that the game knows that the player has received the quest.

When the player interacts with the soldier after that, the soldier will recognize the key and respond with, “Please return her home safely.” If you wanted to, you could set another flag indicating that the quest is active, and so on.

What’s great about this method is that any NPC can access any key/value pair, even ones set by other NPCs. That’s how NPCs interact with each other “behind the scenes,” and that’s how you can set up your quests between multiple NPCs.

At this point, your creative juices should be flowing as you start to imagine all of the possibilities this brings!

Where To Go From Here?

And with that, you have the basic components for a full-fledged RPG. You can download the final project here.

If you would like to build up your API a bit more and add additional layers of interactivity, here are some suggestions:

  1. Dialog: Update the chat box to take parameters from which the the player can choose. When the player makes a choice, send that value back to the NPC. Once you’ve done this, you can begin to form entirely new systems – stores, monster battles, forks in stories, and so on!
  2. Move NPC: Perhaps an NPC blocks your character’s path and the only way to proceed is to move him out of the way. Create an API call that allows an NPC to move themselves based on some criteria.
  3. Animating NPCs: Build up your system even more to allow the NPCs to walk around. You could do this by exposing the animation methods for a given sprite sheet.
  4. Queue for NPC behaviors: Perhaps there is an epic dialog between characters. As of right now, you would have to fake it inside of a single NPC file, but it would be cool if there were a global event queue to which NPCs could add, so that all of the NPCs could queue events for themselves as well as for others.

Here are still more enhancements you could make to the overall game:

  1. An actual story with more characters: Your game is a little boring with only one talking soldier!
  2. Music: Perhaps use Cocos2D’s built in audio engines to dynamically load music based on the tile map. That way, when the player switches rooms, the music will switch, too.
  3. Random enemy encounters: If you implement #1 from the list above, this becomes pretty easy. Perhaps use the green meta tile to overlay areas where enemies could lurk. Then add some keys to the tile set that relate to the types of enemies that lurk there. Once the player moves over the highlighted area, do a random roll to see if an enemy is encountered, and if so, treat the enemy like an NPC and use the get/set meta methods to simulate health.
  4. Stats: Use the game state to keep track of a player’s inventory, equipment, health and other stats.

As you can see, building an RPG from scratch isn’t for the faint of heart. Luckily, you have incredible tools like Cocos2D, Lua and Tiled to assist you in your quest. You could easily reuse the engine you built in this tutorial to make countless games by swapping out the tile sets and Lua files.

If you have any questions or comments, feel free to reach out to me on Twitter or leave them in the forum for this post. Happy Coding!

Brandon Trebitowski
Brandon Trebitowski

Brandon Trebitowski is a software developer and author from Albuquerque, New Mexico. Brandon holds a BS in Computer Science from The University of New Mexico and has been developing software for the last 10 years. In 2010, he coauthored the book iPhone & iPad In Action published by Manning publishing.

Brandon is currently the Director of Mobile Engineering for ELC Technologies and a regularly blogs at http://brandontreb.com.

User Comments

29 Comments

[ 1 , 2 ]
  • For those having issues with the hero spawning in the wall. Make sure to use the Retina simulator. I did no optimize for non retina (sorry)
    Brandon Trebitowskibrandontreb
  • brandontreb wrote:For those having issues with the hero spawning in the wall. Make sure to use the Retina simulator. I did no optimize for non retina (sorry)

    Its dead so its probably ok :P
    shippo0708
  • Awesome tutorial! I bet many people have been looking for something like this, with Area transitions and text boxes and so on.

    But, is there no Download for the completed Project this time?
    falc410
  • The link is at the bottom of the post. Before the suggestions. In case you missed it: http://cdn1.raywenderlich.com/wp-conten ... -final.zip
    Brandon Trebitowskibrandontreb
  • Sorry I was just blind. :)

    I will inspect the project now but from the starter project it seems that the same method for movement is used as in the tile game tutorial. So you can only move from tile to tile.

    I tried to make it so that the character moves to the touch location but this is not so trivial since you need path finding and also you need to know the length so you can set the animation speed for the action correctly.

    But I guess that adding a virtual D-Pad and having continuous movement similar to the beat n up tutorial should be easier.
    falc410
  • Does anybody have a fix yet for the non-retina character positioning bug?

    Edit: No fix for the positioning bug yet, but I've made some improvements to the map transition code. This allows the use of named objects in a 'mapTransitions' object layer to be used to link a tile on one map to a tile on another map automagically, without needing to know the tile positions.

    Replace the exits layer with:
    Code: Select all

    @property(nonatomic, strong) CCTMXObjectGroup* mapTransitionGroup;


    Replace the loadMapNamed method with the following code:
    Code: Select all

    - (void) loadMapNamed:(NSString *) mapName
    {
       [self loadMapNamed:mapName andStartAtSpawnPoint:@"default"];
    }

    - (void) loadMapNamed:(NSString*) mapName andStartAtSpawnPoint:(NSString*) spawnPointId
    {
        if(self.tileMap)
        {
            [self.tileMap removeAllChildrenWithCleanup:YES];
            [self removeChild:self.tileMap cleanup:YES];
            self.tileMap = nil;
        }
        mapName = [mapName stringByAppendingString:@".tmx"];
        self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:mapName];
        self.tileMap.anchorPoint = ccp(0,0);
        [self.tileMap setScale:kGameScale];
        [self addChild:self.tileMap z:-1];
        self.tileSize = self.tileMap.tileSize.width;
       
       // Tile Layers
        self.metaLayer = [self.tileMap layerNamed:@"meta"];
        self.metaLayer.visible = NO;
       self.npcLayer = [self.tileMap layerNamed:@"npcs"];
       
       // Object Layers
       self.mapTransitionGroup = [self.tileMap objectGroupNamed:@"mapTransitions"];

       // Set the hero's position to the specified spawn point
       self.hero.position = [self getTransitionPointCoordinates:spawnPointId];
    }


    Add the following methods (I added them just below the 'loadMapNamed' methods):
    Code: Select all

    /**
    * Gets the coordinates of the specified map transition object
    *
    */
    - (CGPoint) getTransitionPointCoordinates:(NSString*) transitionPointId
    {
       return([self getCoordinatesOfObject:transitionPointId fromObjectGroup:self.mapTransitionGroup]);
    }

    /**
    * Gets the coordinates of the specified object from the supplied object group
    *
    */
    - (CGPoint) getCoordinatesOfObject:(NSString*) objectId fromObjectGroup:(CCTMXObjectGroup*) objectGroup
    {
       CGPoint point = CGPointMake(-1.0, -1.0);   // set to fall-back if no object is found
       objectId = (nil == objectId) ? @"default" : objectId;   // force to 'default' if nil

       if(nil != [objectGroup objectNamed:objectId])
       {
          // found it!
          NSDictionary* object = [objectGroup objectNamed:objectId];

          CGFloat x = [object[@"x"] floatValue];// / self.tileSize;
          CGFloat y = [object[@"y"] floatValue];// / self.tileSize;// - self.tileMap.mapSize.height);   // tile editor is 0,0 at top left of map, Cocos2D is 0,0 at bottom left of map
          return(CGPointMake(x + (self.tileSize / 2), y + (self.tileSize / 2)));
       }
       else if(![objectId isEqualToString:@"default"])   // look for the default transition point if we're not already doing so
       {
          point = [self getCoordinatesOfObject:@"default" fromObjectGroup:objectGroup];
       }

       // not found
       return(point);
    }


    Finally, replace the heroIsDoneWalking method with:
    Code: Select all

    /**
    * Called after the hero is done with his walk sequence
    */
    - (void) heroIsDoneWalking
    {
        self.canWalk = YES;
       NSArray* mapTransitions = self.mapTransitionGroup.objects;
       for(NSDictionary* transition in mapTransitions)
       {
          CGRect exitRect = CGRectMake([transition[@"x"] floatValue], [transition[@"y"] floatValue], [transition[@"width"] floatValue], [transition[@"height"] floatValue]);
          if(CGRectContainsPoint(exitRect, self.hero.position))
          {
             if([transition[@"type"] isEqualToString:@"map transition"] && nil != [transition objectForKey:@"destination"])
             {
                NSString* mapName = transition[@"destination"];
                NSString* spawnPointId = transition[@"spawnPointId"];
                [self loadMapNamed:mapName andStartAtSpawnPoint:spawnPointId];
                return;
             }
          }
       }
    }


    With these changes in place, you no longer need to specify the coordinates of the 'landing' point, for a map transition. Instead, you just need to specify the name of the map transition object in the new map. Note: If the specified object can't be found, it will fall back to the one named 'default', or drop you at -1,-1 if that isn't found either.

    Create an Object layer in your tile map called 'mapTransitions', this will replace your 'exits' layer.
    Add an object to it, being sure to place it with integer x & y values, with a width and height of 1. This ensures that the hero's location will be calculated correctly.
    Add the following properties:
    destination -> this is the name of the map to transition to
    spawnPointId -> this is the name of the map transition object in the target map

    Now on to the bug discovery: It turns out that you're not *actually* placed off the map, you're just *drawn* with an improper offset when in non-retina mode.

    If you carefully count out the tile placement, for your spawn point, and move from there to the doorway transition, you can actually load the town map, and you'll be drawn off target on that map as well, but you're obeying the meta layer's impassibility tiles just as if you had actually been spawned in the correct location.
    Voice
  • falc410 wrote:Sorry I was just blind. :)

    I will inspect the project now but from the starter project it seems that the same method for movement is used as in the tile game tutorial. So you can only move from tile to tile.

    I tried to make it so that the character moves to the touch location but this is not so trivial since you need path finding and also you need to know the length so you can set the animation speed for the action correctly.

    But I guess that adding a virtual D-Pad and having continuous movement similar to the beat n up tutorial should be easier.


    I haven't gotten far enough to try it yet, but couldn't you just keep the target destination, and at each tile step move along the longer of the two axes? (If it's longer horizontally, move that way, otherwise move vertically.) If you hit an impassible tile (or anything else that would prevent you from entering a tile, simply stop moving, and clear the target location. No pathfinding necessary, but it allows moving more than one tile at a time. Real pathfinding to the target tile would be a nicer option, but this should be an acceptable intermediate stage.
    Voice
  • has anybody implemented any NPC movement yet or anything with the NPCs?

    creating an inventory system and stats seems a bit complicated i have been messing around with it for a bit but i cannot seem to figure out an efficient solution
    Fabolous1
  • Hm... Im also trying to make npcs move. What I tought of is generating a random number and based on that number let the npc go left,right,up,down.
    BUT I have one problem. I check the coordinates of the tile my main character is on and I check the tile where my Npc is currently on.
    I have a method in the setPlayerPosition that is like this.

    Code: Select all
    if(tileGid == tileGidNpc){
         
           return;

    }



    But it dosen`t work. This code will let the main character not move, but in the console the location updates. I don`t get it....
    benedevil
  • Anyone else having an issue with calling functions from the lua file? I created the room-soldier.lua and eventually called the soldier:interact() function but now I can't seem to get the program to run any other lua procedure except for soldier:interact(). No matter how I change the function it will always print "Hello Soldier" and I can't get it to call any other functions I defined in the room-soldier file. I even deleted the file and folder all together but the program keeps performing the exact same print method every time I call soldier interact.

    This has pretty much made making chatboxes or any other npc interaction impossible. Anyone know what to do, is it perhaps an issue with luaobjc?

    EDIT: ok I found a solution to my issue. No it wasn't a problem with lua, it seems to have been caused by the way the tutorial suggested to capture the file.

    here is the way found in the tutorial. It gets the full path name XX-XX.lua and then appends that path with the prefix npc.
    For whatever reason that made it so no other lua file except for the first instance of room-soldier.lua (with the first 'version' of the interact() method) was loadable or callable in my program.
    Code: Select all
    //resolve the path to the npc's lua file
                    NSString *roomName = [name stringByReplacingOccurrencesOfString:@".tmx" withString:@""];

                    NSString *npcFileName = [NSString stringWithFormat:@"%@-%@.lua", roomName, npcName];

                    NSString *path = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"npc"] stringByAppendingPathComponent:npcFileName];


    This is the change I made to the program. Instead I just combined room and the npc name together (XX-XX without the lua suffix) . Then I called the nsbundle pathForResource and ofType methods which appended the lua suffix to my file name and I supposedly ignore the npc file prefix (I have no idea what problems this will create in the future).

    This change somehow allowed the program to load other files besides room-soldier and any changes to the soldier:interact() method were then recognized. All is well for now.
    Code: Select all
    //resolve the path to the npc's lua file
                    NSString *roomName = [name stringByReplacingOccurrencesOfString:@".tmx" withString:@""];

                    //Alternate version of npcFileName.
                    NSString *npcFileName = [NSString stringWithFormat:@"%@-%@", roomName, npcName];
                   
                    //Alternate version of path. No idea why this works better...
                    NSString *path = [[NSBundle mainBundle] pathForResource:npcFileName ofType:@"lua"];


    If anyone has any idea why this was the case please let me know, thanks.
    amr0828
  • Hey -

    I'm not sure I understand quite how the collision detection works.

    I made a texturepacker file to replace the character you have in the example, and all the collision is occasionally off...

    http://www.sendspace.com/file/8nf4d3

    What am I doing wrong? It's driving me nuts. :3
    loveat528hz
  • hello everyone
    first thx author ?this is a great tutorial?i am a amateur this days i try to make my own program according to this tutorial?i try to touch screen move sprites around in tilemap,sprites can do running ,idle action but can't move to forward ,its be trapped in one point, i can fixed it, this problem have been annoying me for a week ,i wish u guys can help me thx!
    this is code
    #import "GameLayer.h"
    #import "Robot.h"
    #import "GameScene.h"
    #import "SimpleAudioEngine.h"
    @interface GameLayer ()
    {

    BOOL heroMoving;
    }

    @property (nonatomic, strong) CCSprite *hero;
    @property (nonatomic, strong) CCAction *walkAction;
    @property (nonatomic, strong) CCAction *moveAction;

    @end

    @implementation GameLayer

    -(id)init
    {
    if ((self = [super init]))
    {
    [[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:@"latin_industries.aifc"];
    [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"latin_industries.aifc"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit0.caf"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit1.caf"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_herodeath.caf"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_botdeath.caf"];


    [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"pd_sprites.plist"];
    _actors = [CCSpriteBatchNode batchNodeWithFile:@"pd_sprites.pvr.ccz"];
    [_actors.texture setAliasTexParameters];
    [self addChild:_actors z:-5];
    [self initHero];
    [self addHp];
    [self initTileMap];
    [self initRobots];
    [self scheduleUpdate];

    }
    return self;
    }
    -(void)addHp{CCSprite *sprite=[CCSprite spriteWithFile:@"health_bar_red.png"];
    CCProgressTimer * timer = [CCProgressTimer progressWithSprite:sprite];
    timer.type=kCCProgressTimerTypeBar;
    timer.scale=0.2;
    timer.position = ccp(_hero.position.x,(_hero.position.y+50));
    timer.percentage += 100;

    [self addChild:timer z:-1 tag:90];
    }

    -(void)initTileMap
    {
    _tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"pd_tilemap.tmx"];
    for (CCTMXLayer *child in [_tileMap children]) {
    [[child texture] setAliasTexParameters];
    }
    [self addChild:_tileMap z:-6];
    }

    -(void)initHero
    {
    _hero = [Hero node];
    [_actors addChild:_hero];

    _hero.position = ccp(_hero.centerToSides, 80);
    _hero.desiredPosition = _hero.position;
    [_hero idle];

    self.isTouchEnabled = YES;
    }

    - (void)registerWithTouchDispatcher
    {
    [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:
    self priority:0 swallowsTouches:YES];
    }

    - (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
    {


    return YES;
    }

    - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
    {
    CGPoint location = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]];
    location = [[CCDirector sharedDirector] convertToGL:location];

    CGSize winSize = [[CCDirector sharedDirector] winSize];
    float heroVelocity = winSize.width / 3.0;
    CGPoint moveDifference = ccpSub( _hero.position, location);
    float distanceToMove = ccpLength(moveDifference);
    float moveDuration = distanceToMove / heroVelocity;
    if (moveDifference.x < 0) {
    _hero.flipX = NO;
    } else {
    _hero.flipX = YES;
    }
    [_hero stopAction:_hero.idleAction];

    if (!heroMoving) {
    [_hero runAction:_hero.walkAction];
    }

    _moveAction = [CCSequence actions:
    [CCMoveTo actionWithDuration:moveDuration position:location],
    [CCCallFunc actionWithTarget:self selector:@selector(heroMoveEnded)],

    nil];

    [_hero runAction:_moveAction];

    heroMoving = YES;
    }


    -(void)update:(ccTime)dt
    {
    [_hero update:dt];
    [self updateRobots:dt];
    [self updatePositions];
    [self reorderActors];
    [self setViewpointCenter:_hero.position];
    }

    -(void)reorderActors
    {
    ActionSprite *sprite;
    CCARRAY_FOREACH(_actors.children, sprite)
    {
    [_actors reorderChild:sprite z:(_tileMap.mapSize.height * _tileMap.tileSize.height) - sprite.position.y];
    }
    }

    -(void)updateRobots:(ccTime)dt
    {
    int alive = 0;
    Robot *robot;
    float distanceSQ;
    int randomChoice = 0;
    CCARRAY_FOREACH(_robots, robot)
    {
    [robot update:dt];
    if (robot.actionState != kActionStateKnockedOut)
    {
    //1
    alive++;

    //2
    if (CURTIME > robot.nextDecisionTime)
    {
    distanceSQ = ccpDistanceSQ(robot.position, _hero.position);

    //3
    if (distanceSQ <= 50 * 50)
    {
    robot.nextDecisionTime = CURTIME + frandom_range(0.1, 0.5);
    randomChoice = random_range(0, 1);

    if (randomChoice == 0)
    {
    if (_hero.position.x > robot.position.x)
    {
    robot.scaleX = 1.0;
    }
    else
    {
    robot.scaleX = -1.0;
    }

    //4
    [robot attack];
    if (robot.actionState == kActionStateAttack)
    {
    if (fabsf(_hero.position.y - robot.position.y) < 10)
    {
    if (CGRectIntersectsRect(_hero.hitBox.actual, robot.attackBox.actual))
    {
    [_hero hurtWithDamage:robot.damage];

    if (_hero.actionState == kActionStateKnockedOut && [_hud getChildByTag:5] == nil)
    {
    [self endGame];
    }
    }
    }
    }
    }
    else
    {
    [robot idle];
    }
    }
    else if (distanceSQ <= SCREEN.width * SCREEN.width)
    {
    //5
    robot.nextDecisionTime = CURTIME + frandom_range(0.5, 1.0);
    randomChoice = random_range(0, 2);
    if (randomChoice == 0)
    {
    CGPoint moveDirection = ccpNormalize(ccpSub(_hero.position, robot.position));
    [robot walkWithDirection:moveDirection];
    }
    else
    {
    [robot idle];
    }
    }
    }
    }
    }

    if (alive == 0 && [_hud getChildByTag:5] == nil)
    {
    [self endGame];
    }
    }

    -(void)endGame
    {
    CCLabelTTF *restartLabel = [CCLabelTTF labelWithString:@"RESTART" fontName:@"Arial" fontSize:30];
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartGame)];
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CENTER;
    menu.tag = 5;
    [_hud addChild:menu z:5];
    }

    -(void)restartGame
    {
    [[CCDirector sharedDirector] replaceScene:[GameScene node]];
    }

    -(void)updatePositions;
    {
    float posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - _hero.centerToSides, MAX(_hero.centerToSides, _hero.desiredPosition.x));
    float posY = MIN(3 * _tileMap.tileSize.height + _hero.centerToBottom, MAX(_hero.centerToBottom, _hero.desiredPosition.y));
    _hero.position = ccp(posX, posY);

    Robot *robot;
    CCARRAY_FOREACH(_robots, robot)
    {
    posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides, MAX(robot.centerToSides, robot.desiredPosition.x));
    posY = MIN(3 * _tileMap.tileSize.height + robot.centerToBottom, MAX(robot.centerToBottom, robot.desiredPosition.y));
    robot.position = ccp(posX, posY);
    }
    }

    -(void)setViewpointCenter:(CGPoint) position {

    CGSize winSize = [[CCDirector sharedDirector] winSize];

    int x = MAX(position.x, winSize.width / 2);
    int y = MAX(position.y, winSize.height / 2);
    x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width)
    - winSize.width / 2);
    y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height)
    - winSize.height/2);
    CGPoint actualPosition = ccp(x, y);

    CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
    CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
    self.position = viewPoint;
    }

    -(void)initRobots
    {
    int robotCount = 50;
    self.robots = [[CCArray alloc] initWithCapacity:robotCount];

    for (int i = 0; i < robotCount; i++)
    {
    Robot *robot = [Robot node];
    [_actors addChild:robot];
    [_robots addObject:robot];

    int minX = SCREEN.width + robot.centerToSides;
    int maxX = _tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides;
    int minY = robot.centerToBottom;
    int maxY = 3 * _tileMap.tileSize.height + robot.centerToBottom;
    robot.scaleX = -1;
    robot.position = ccp(random_range(minX, maxX), random_range(minY, maxY));
    robot.desiredPosition = robot.position;
    [robot idle];

    }
    }



    - (void)heroMoveEnded
    {
    [_hero stopAction:_hero.walkAction];
    heroMoving = NO;
    }



    @end
    85743790
  • Good call on using Lua. It'd be great if iOS could take full advantage of LuaJIT. Maybe in the future :D
    balaam
  • Any chance anyone has tried or could lead me in the right direction for adding a portrait sprit in the dialogue chat box that is maybe linked or pointed to in the assosciated NPC lua file?
    Kitlang
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Toby Stephens

... 50 total!

Update Team

  • Riccardo D'Antoni

... 14 total!

Editorial Team

  • Alexis Gallagher

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Miguel Angel
  • Victor Grushevskiy

... 33 total!

Subject Matter Experts

... 4 total!