How to Make an RPG
In this tutorial, you’ll learn how to use pre-built 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! By .
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
How to Make an RPG
45 mins
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:
- Click on the npc layer to ensure it’s selected.
- Select the soldier tile to which you added the name property.
- 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.
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:
- Download Lua 5.1 here and extract it. Newer versions of Lua don’t work with the bridge.
- 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).
- Delete the following files: lua.c, luac.c, print.c, Makefile . Leaving these in will cause issues.
- Download LuaObjCBridge from here.
- 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:
- These are all of the includes you need to make the manager work. Don’t worry about the nerdy C headers.
- You’ll use the luaState property to interact with the Lua system; gameLayer is your familiar GameLayer class.
- Initialize the Lua system.
- 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!