Getting started part 4: Dragon Eggs - Scripting and getting a controllable player
If you didn't complete tutorial 3 but still want to play along, grab this: DragonEggs_Tutorial4Start.zip
We're now going to continue working on the "Dragon Eggs " game we started in the last part.
Unfortunately, to allow flexibility and power, we can't just run around pasting icons to make a game, you've also gotta program a bit.
Lua is the scripting language that Novashell uses. It's been used in a ton of games, including Grim Fandango, Escape From Monkey Island, World Of Warcraft, FarCry, and Baldur's Gate. So instead of needing to learn the idiosyncrasies of yet another language for just one game like Dink C was in Dink Smallwood, you can apply the knowledge to hundreds of others too!
If you run into something that you just don't want to do by script, don't forget, Novashell's entire C++ source code is also available and license free, so you're never at a 'dead end' when Seth is hit by a bus.
Some useful links for learning Lua, although really, no Lua knowledge is required for these tutorials.
Tech Tip: Novashell uses V5.13 of Lua as a base and has been enhancemenced with extra things like C++ style // comment support.
Tools of the trade
When editing script, any text editor, even notepad, will do. However, if you want to do it in style you need something with these features:
- Project tree navigation (not totally important, but I like it)
- Syntax coloring
- Easy to navigate even with many files open
- Powerful text search that scans all your scripts (You can't live without this!)
Here is a screenshot of what I use, it's called UltraEdit. In the screenshot, I'm doing a "Find In Files", this means you can type in any command you aren't sure of how to use and get a list of all the files it's used in to see as an example - very useful.
A program that supports similar features that is free is conTEXT. I haven't tried it...
If you double click a .lua text file, it should open in your favorite editor. If it doesn't, you'll need to associate it by right clicking the file from Windows Explorer, choosing "Open With->Choose Program..." and selecting your favorite text editor. Check the "always use the selected program to open this kind of file" box before clicking OK.
If I only had a brain
When we last left our hero in the DragonEggs game, he was a picture that couldn't move. Start the world and click Edit.
Select the hero/player entity (remember, we upgraded it from being a normal tile pic) and open its properties.
In the LUA script field enter "player.lua".
Now click Edit. Because this script doesn't exist you should see this prompt:
Click Continue, and you should see the default script that was created for you. It should pop up in your associated text editor. (There is no internal editor in the engine itself.)
function OnInit() //run upon initialization end function OnPostInit() //run after being placed on a map end function OnKill() //run when removed end
These are functions that are automatically run in an entity's script.
The OnInit() function is run as soon as the entity is made, before he is actually placed on the map. It's the job of the OnInit() to setup what it looks like and any special properties, so the engine knows how big it is. Note, every time you copy and paste an entity, copies are made and this is run on the entities in the copy buffer too.
The OnPostInit() function is run by the entity after he has been placed on the map. Most initialization work should be done here.
OnKill() is run when the entity is removed from the map and deleted.
Script tip: There are other 'automatically called if they exist' also: OnSave() is a useful one, called before the map is saved.
Note that the file player.lua was created in the DragonEggs/script directory.
Add a line inside the OnPostInit() as shown below: (bolded lines mean that's the part we added/changed)
function OnPostInit() //run after being placed on a map LogMsg("Hi World! My name is " .. this:GetName() .. " and I like long romantic walks on the beach"); end
Save the file.
By pressing OK on the player's properties, all entities in the current map are reinitialized. Do that now.
Open the console. (Use the tilde/backtick key to toggle it.)
Great, it ran the line! Toggle the console back off.
Tip: The standard lua output of "print" also routes to the console. If you'd used LogError, it would have been in red and forced the console to pop up as well.
Tip: Opening the editor pauses the game. Game->Game Paused can be used to unpause it, but this is kind of not recommended, it's hard to edit things that are moving.
Make four more copies of the player. Check the console again.
What the heck? The five entities all ran the player.lua script as expected but the engine took it upon itself to rename the copies.
The reason is there CANNOT be two entities with the same name, the engine will not allow it, it tacks on a letter at the end until it is unique when there is a conflict.
The name is more than a name - it's a unique text "tag" that will always link to this specific entity with lightning fast look-ups. If you're thinking "But I want 50 monsters named Ogre!", don't worry, you can just add a custom data field to hold your monsters name, they don't even need an "entity name" unless they need to be kept track of in special ways.
Engine Tip: Information about named entities is kept in the "tagcache.dat" file in each map file directory. This data is scanned when the world is started and added to a global database that is updated when needed. Even when entities move through doors between maps, tagcache data is still synchronized correctly so the engine can find any entity without actually loading its map.
Ok, erase all the copies you made. Double check that the one left is called Player, if it isn't, fix it.
What the heck is "This"
In the LogMsg() script command we used earlier, we got the name of the entity with this:GetName(). "this" is the entity that ran the script. An "Entity" object has many useful functions. (Check the scripting command reference for all of them.)
The ":" after the "this" is kind of a weird lua thing, similar to this->GetName() in C++.
Tech Tip: Lua script is case sensitive If you typed This instead of this, it won't work. Be careful!
Using the console interactively, how scope works
First, click the player entity, then Edit so we can make some more changes to its script.
First, let's create a variable called health and assign a random number to it at the top of the script. We could also do it in a function, but we want to make sure it happens right away.
Let's also add a new function called ShowHealth that will display our health.
The script should now look like this:
health = random(1,100); //create this variable by assigning a random number between 1 and 100 to it function OnInit() //run upon initialization end function OnPostInit() //run after being placed on a map LogMsg("Hi World! My name is " .. this:GetName() .. " and I like long romantic walks on the beach"); end function OnKill() //run when removed end function ShowHealth() //our own function LogMsg(this:GetName() .. " has " .. health .. " health right now."); end
Go back into the editor and click OK on the properties so the player entity reinitializes.
Ok, now we assume this script was run and the variable called "health" we created was set to a random number.
But what was it set to? I'll show you a few ways to interactively check it.
Enable the editor. Make sure no entities are highlighted and bring up the console.
In the input box, cut and paste the following line in: GetEntityByName("Player"):RunFunction("ShowHealth");
and press ENTER.
You should see a result similar to the about image.
What's happening is GetEntityByName() returns an Entity object. An entity object has a function called RunFunction that allows us run a function from ITS namespace. If we cut and paste 100 players, each one will have a different amount of health because they each have their own namespace. If a variable or function doesn't exist in its namespace, it checks the global one too. If a variable or function doesn't exist in either one, an error will occur. This helps you catch typos.
Now, in the editor, click on the Player entity to highlight it, then open the console again. Something has changed! Now the input is run inside the Entity's namespace instead of the global one. The input bar is RED to indicate this.
Enter the following and press ENTER after each line:
health = 50;
Weee, we changed it!
Tip: You don't really have to enter the ; at the end of each line. But by using it, you can enter more than one command on the same line. Because I'm mostly a C++ programmer I'm just used to it.
With the Entity namespace still active, enter this:
(btw, "this" is always the Entity object that owns this script, there is no "this" when run in the global namespace)
What's all this junk? It's every function and variable in this Entity's namespace, this can be useful to see sometimes.
Tip: You can cut and paste text from this tutorial directly into the console.
Let's create a copy of the player entity 40 pixels to the right of it. Sure we could cut and paste, but what if you wanted to do this as someone is playing the game, like for creating bullets or something? Here is the script way to do it, enter (or paste) this while the player entity is highlighed in the console.
this:Clone(nil, this:GetPos() + Vector2(40,0));
Another player appears. If you highlight him and run this:DumpScriptInfo() on him, you'll see his health is different. If you type LogMsg(this:GetName()); you'll see his name is slightly different too. (It's also shown on the tile edit floating palette when he's selected)
Tip: Press the UP and DOWN arrow while the console is active to scroll back through previously entered commands.
Delete that extra copy we made. Highlight it and in the console enter:
Poof, it will be gone.
Giving the player a brain
Ok, that was a fun tangent but we're itching to actually control the player. Shoot, we probably could have written a finished game in assembler by now.
Open the player's script by opening his properties and clicking the Edit button.
Make the following changes to the OnInit function:
function OnInit() //run upon initialization this:GetBrainManager():Add("StandardBase",""); //add this //hint to the path-finding system that it can ignore this while computing paths if it needs to this:SetIsCreature(true); end
Uh oh, if you bring up the console, you'll see a warning like "StandardBase of 115 (Player): Entity needs collision information to use StandardBase."
What was that about? What's this "StandardBase" thing? Who killed Mr. Burns?
First, this:GetBrainManager() is a sub system that handles brains. Not just one but plural. A brain is something you kind of 'plug-in' to an entity to give it some smarts.
In the line of code we added, we request that the "StandardBase" brain is added. This brain helps characters to move around and is required for the Entity's GoalManager to function. (GoalManager handles pathfinding)
If you request a brain that doesn't exist, the list of available brains will be shown. Some are very simple, like "Shake" and "ColorFlash". The things they do could be done in script, but it's easier and more intuitive to use brains I think.
That was another nice diversion. The warning stems from the brain trying to move the player around and the physics system refusing to because it doesn't have collision information. I think you know what we have to do.
The collision edit palette
This is pretty painless, honest. Select the player and hit Ctrl-W (or use Modify Selected->Edit Collision Data) to toggle the collision editor on. Click five or six points around the players feet. Change to "adjust" mode to move them around or delete them.
Tip: Zoom in close for this.
You may want to disable snap for finer adjustments.
When you're satisfied, click Save Changes or hit Ctrl-W again.
Tip: Collision shapes must be convex. The engine will detect any problems and simplify the shape if needed. For complex shapes, you can create more than one shape to get around this limitation. Moving things should only use one shape.
Now, you didn't explicity save or load the collision data, but it's retained. Where exactly is the engine putting this stuff? If you look in the directory that contains your my_image.png, you'll see another file named my_image.dat. This contains extra information such as the collision data and transparent key color if set. If you cut and paste the image and it's .dat to a new project, that info will be retained and automatically applied if applicable. There is also a way to directly load and save collision shapes from script for more control, we'll get into that later.
The Image Alignment Editor
Ok, I've got to explain something. It's vital to understand this! When you were playing with the collision stuff above, you may have noticed a little yellow X in the middle of the character bitmap. That is the exact point of the entity's X/Y position. So if you move an entity to 0,0 on the map, this little X will be what is actually lined up with 0,0.
Now, it's sort of a bad thing that we don't have the X/Y point inside of the area with the collision shape. Why? Because, if you say "Move to such and such a position", it's going to be calculating it by when his chest arrives, rather than when his feet arrive, which is what we want here.
Don't worry, fixing this is pretty easy. Highlight the character and choose Modify Selectic->Edit Image Alignment (or the shortcut, Ctrl-Shift-E).
You can use the arrow keys to move around the little crosshairs, or change the centering mode of the image or use a combination of both.
If we were using a visual profile .xml (we'll get into that later), the changes would be saved directly to it. But because this is a simple "tilepic" image, the centering and offset data is saved inside this entity itself.
Ok, great, we moved the true X/Y into where his foot is. But if you toggle on Show Map Collision (Ctrl-Q) you'll see that the collision shape we made earlier is now in the wrong place. The collision data is not connected to the visual data at all.
To fix this, go back into the collision editor and use the arrow keys to move the whole shape at once. Move it back over the foot as shown below.
Seeing something actually move
Well, he's standing there. Let me give you a little example of how the goal manager works.
Enter the command:
into the console while the player entity is highlighted.
Press F1 to turn off the editor so the game will unpause.
You should see the entity move to 0,0 on the map. Uh, you should move him back, that was just a demonstration.
Because this is player human-controlled by pressing and holding down keys we'll have to do some custom scripting to get the effect we want.
Make the following changes to your player.lua file:
(Note, you can delete that health stuff we did earlier, we don't really need it)
RunScript("system/player_utils.lua"); //has some useful global functions, this is in base
function OnInit() //run upon initialization this:GetBrainManager():Add("StandardBase",""); //hint to the path-finding system that it can ignore this while computing paths if it needs to this:SetIsCreature(true); end
function OnPostInit() //run after being placed on a map LogMsg("Hi World! My name is " .. this:GetName() .. " and I like long romantic walks on the beach"); this:SetTurnSpeed(6); //turn instantly. Since we only have 1 graphic frame, it's weird //if we don't do this //Note: the "always" keyword means the key will always react, regardless of what else is //also pressed. Without it, you couldn't shoot and walk at the same time GetInputManager:AddBinding("left,always", "OnLeft", this:GetID()); GetInputManager:AddBinding("right,always", "OnRight", this:GetID()); GetInputManager:AddBinding("up,always", "OnUp", this:GetID()); GetInputManager:AddBinding("down,always", "OnDown", this:GetID()); ResetKeys(); //our function to reset our local key input vars //let's have a function that gets run every logic tick this:SetRunUpdateEveryFrame(true); end
function OnKill() //run when removed RemoveActivePlayerIfNeeded(this); //let the engine know we shouldn't be the active player any more end
//this is run every logic tick, normally we don't need to do this, but the player is a special case function Update(step) AssignPlayerToCameraIfNeeded(this); //make us the official player and have the camera track us, //if we're not already, and if another entity doesn't already have this role //this function is in player_utils.lua local facing = ConvertKeysToFacing(m_bLeft, m_bRight, m_bUp, m_bDown); if (facing != C_FACING_NONE) then //they are pressing towards a direction this:SetFacingTarget(facing); //turn towards the direction of the keys we pressed this:GetBrainManager():SetStateByName("Walk"); else this:GetBrainManager():SetStateByName("Idle"); end end function ResetKeys() m_bLeft = false; m_bRight = false; m_bUp = false m_bDown = false; end function OnLeft(bKeyDown) m_bLeft = bKeyDown; return true; //continue to process key callbacks for this key stroke end function OnRight(bKeyDown) m_bRight = bKeyDown; return true; //continue to process key callbacks for this key stroke end function OnUp(bKeyDown) m_bUp = bKeyDown; return true; //continue to process key callbacks for this key stroke end function OnDown(bKeyDown) m_bDown = bKeyDown; return true; //continue to process key callbacks for this key stroke end
Try him out now.
Use your arrow keys to move. If all went well, you can sort of "float him around". Yay. We can fine tune the physics and speed through other Entity functions later.
Tip: The keyboard shortcuts of F1, Control+Shift+R to restart, escape, and tab for double speed are set in base/scripts/system_start.lua and can be overridden to disable these features or change the keys.
© Copyright Seth A. Robinson and Robinson Technologies