We've found out about the tools and we've learned the basics of scripting and it’s the time to see how this knowledge applies to the game development. Before we start this tour, let’s first make a brief overview of the file types you’ll be using in WME (and I strongly recommend using this naming convention):
*.script – are the files which contains the actual program
*.inc – are the include files which you can include in the script files. Their purpose is to save you hundreds of lines of the code, which would otherwise repeat in more files. Prime example of this is the file data\scripts\base.inc. You’ll be including this file in almost every script.
*.scene – is the file which contains the definition of the scene as set in the Scene Editor. Although you could define this file by hand, it’s way more comfortable to use scene edit to do the dirty work.
*.sprite – product of the sprite editor
*.actor – is the file which defines actor and his animations.
*.entity – is the file which stores the sprite entity definition.
*.font – is the font definition file. It’s an interface between the actual font and a WME. Currently there are two types of font supported – bitmap and true type.
*.window – is a definition file for GUI window elements.
*.image – is a special container to store an image (with the ability of tiling etc.). This is useful for gui elements.
*.button – is a container for button definitions.
Also WME has preset a couple of special files which you can also change, but those are defaults:
default.game and startup.settings are files created by Project Manager and stored in the first (main) game package.
string.tab located at the same place contains the localization table for the texts.
items.items contains the definition of inventory items used in the game (data\items)
responses.def is the file which defines how the dialogue window would look like.
inventory.def is the file which defines how the inventory box looks like (data\interface)
But enough of that and let’s start with the most simple thing – the scene tweaking. If we explore the contents of any of created scenes in the explorer, we’ll see that there is always a scene file and a folder scr which contains the file scene_init.script
Wme reads the scene file for the current scene, prepares all nodes as defined in the scene file, attach all scripts which are present in the scene file and run the file called scene_init.script. This is the file which we’ll examine now and I’ll add my comments to it:
#include "scripts\base.inc" // here comes the stuff which initializes the scene actor.SkipTo(733, 562); // We position actor to a certain position actor.Direction = DI_DOWN; // we set the direction she faces actor.Active = true; // we make it visible for the scene //////////////////////////////////////////////////////////////////////////////// // scene state global Statewarehouse; // We create a global variable which has the same name as // the scene prepended by State // default values if(Statewarehouse==null) //We run the game for the very first time so the value was { // not initialized yet Statewarehouse.Visited = false; //We’ve never been in the scene yet } //////////////////////////////////////////////////////////////////////////////// // setup scene according to state variables //////////////////////////////////////////////////////////////////////////////// if(!Statewarehouse.Visited) //If we’ve never been in the scene { Statewarehouse.Visited = true; //Set that we’ve been there // this is our first visit in this scene... }
Now this may look to you as an overkill script for something as similar as checking if we were in some scene or not but trust me, there’s more to it than meets the eye. This is actually very clever script which counts with many different possibilities. It counts with the game which was newly started, it counts with a scene revisiting, it counts with the fact that you can have on enter special events. Remember those cinematic moments when you played an adventure, walked in some room only to see the bridge collapsing? You wouldn’t want to see it collapse every time you visit that scene. Once is enough for sure.
So now when we uncovered what’s going on in this script, it’s actually very simple. As there are thousands of different methods, I’ve decided to choose the non violent way how to present them when needed, although Max was very unhappy with me last time, I've consulted nonviolent approaches.
Let’s look at one example from the beginning of the script.
actor.SkipTo(733, 562);
We know that objects have their methods which are identified by .name(); notation. So plain and simple we’re calling method SkipTo() of our actor object. “actor” is a global variable which contains an actor object and is declared in different script. Now someone stands up and shouts at me: “You liar, you told us, that every global variable must be declared explicitly in the script by a keyword global.” Ha, I would reply, but it’s of course true. It’s there, but neatly hidden in the included file base.inc See the power of includes? You don’t have to declare an actor, whom you would use for almost every script. You simply put it in the include file and that’s it. Template does the rest.
So let’s get back to our actor object and look at the very first methods we saw + add two more very useful methods:
Actor object methods and attributes
actor.SkipTo(x,y); - places an actor to a specified screen position. Always double check, that this position is inside of a region.
actor.Direction = ?; - is an attribute which sets the direction actor is looking. We have 8 constants (DI_UP, DI_DOWN, DI_LEFT, DI_RIGHT, DI_UPRIGHT, DI_UPLEFT, DI_DOWNRIGHT, DI_DOWNLEFT)
actor.Active = true / false; - is an attribute common to all nodes and actors which makes them active or inactive (visible or invisible).
actor.GoTo(x,y); - instead of direct placing of the actor, actor walks to specified coordinates
actor.Talk(string); - actor says a text line.
So I bet you’re eager to try the new methods in action. Let’s work on our scene_init.script then! Open the scene_init.script file located in data→scenes→warehouse→scr folder. Note that from now on, when I’ll refer to the scene_init.script you’ll find it easily without my assistance. Locate the line actor.Active = true; and add some more lines behind this one so you’ll get the following result:
actor.Active = true; actor.GoTo(365,561); actor.Direction = DI_DOWN; actor.Talk("What a beautiful warehouse!");
Save your script and run the game from the Project Manager. Before we go on, let’s speak about one of the most important aspects of the game programming in WME. WME produces a file called wme.log which is placed in the project root directory. This is a critical file where all problems / script errors etc. are stored. So if you for example make a typo in the script, the game just simply states Script compiler error, but this file tells you what file and on which line contains this error. So learn to open it often or your work will be very slow.
Oh-key. Let’s move on and try some interaction with the Scene. Open the warehouse scene in the scene edit again and select the door region entity we’ve created earlier. Click on the scripts button. And here click on “New script” button.
In the next dialogue select template “empty.script” on the left side and press ok.
This operation will create an empty file called door.script and place it in the scr folder. It also attaches the file to the entity so from now on you’re able to catch interaction with our door. Press OK again and save your scene. Now open the newly created file which contains the following:
#include "scripts\base.inc" //////////////////////////////////////////////////////////////////////////////// //on "event" //{ // ... //}
As you can see it’s not very useful. It has include of our old friend base.inc but apart from that it doesn’t do anything. We need to expand it a bit. I was briefly explaining events in the script chapter and now it’s time to use them. Change the script so it will read:
#include "scripts\base.inc" on "LeftClick" { actor.Talk("I've clicked on the door"); }
Save the script, run the game and click left mouse button on the door. Voila! We’ve defined our first interaction with the game world. We’ll expand more on that but before I’ll offer you a set of most useful events which are available from the WME side.
on "LeftClick" – Left mouse button was clicked over the node.
on "RightClick" – Right mouse button was clicked over the node.
on "MiddleClick" – Middle mouse button was clicked over the node.
on "LeftDoubleClick" – Left mouse button was double-clicked over the node.
on "RightDoubleClick" – Right mouse button was double-clicked over the node.
There are more events to use, but for the start we’ll be ok with these.
The scripts attached to the node have another interesting feature, they are parts of the object so you can access their properties by the identifier this. Let’s experiment with this concept a bit. Change the actor.Talk to read:
actor.Talk(this.Name);
If you save the script and run the game, upon clicking on the door actor says “Door”. Does it ring a bell? But of course, it’s a Name we’ve set in the scene edit. As you can see, what we set in the Scene Editor is easily accessible from the script which brings us to yet another actor method:
actor.GoToObject(Node); - Goes to coordinates specified in the Scene Editor under “Walk to” and then turns to the direction specified in the same place.
But we also know that the entity is represented by a keyword this. Can’t it be simpler than that? Try to rewrite your event handler to:
on "LeftClick" { actor.GoToObject(this); actor.Talk(this.Name); }
Provided you’ve set correctly the coordinates in the scene editor, actor should go to the door and says the word “Door”.
Last experiment with our infamous door for now is setting their Active property to false and thus disabling the door hotspot after we click it.
on "LeftClick" { actor.GoToObject(this); actor.Talk(this.Name); this.Active = false; }
We’ve scratched surface of two objects so far. Actor object and Region Entity object, which we referenced by keyword this and hid it through setting its attribute Active to false.
Let me return back to the image we’ve seen at the beginning and look at this very image from a developers point of view.
Now it won’t probably surprise you that the main game object is called Game. You can also see that everything in this diagram is derived from the Game object. If we look at our door entity from this point of view to see where it’s hidden, we’d go this way:
Game → Scene → Door.
We know, that we can reference Scene as Scene, but don’t let this fool you.1) The scene is in demo actually defined in the game.script which is the master brain of the game as follows:
Scene = Game.Scene;
In the base.inc our Scene is defined as global Scene; and because we include base.inc into all our files, we can readily reference Scene object from everywhere.
As we can guess, Game object would serve for the global purposes of the game while Scene object would be useful for per Scene operations. Let me introduce you to the most used functions which represent very well each object.
Game.ChangeScene(filename); is a function which changes the scene. It takes as an argument path to the .scene file
Scene.GetNode(NodeName); is a function which returns the object named NodeName from the scene.
We’ll do something, which would demonstrate the power of WME now. Return to your warehouse scene in scene edit and let’s tweak it a bit. First we should create a new region and call it Exit. Position it to the left bottom corner as on the following image (you should immediately discover by now that it’s the red shape). Also note the position in the scene tree. If you put it above the floor, the mechanism presented wouldn’t work because the floor region would cover the Exit region and this way the Exit region would never get his voice. Last thing which remains to do is to attach a script to this region. Use an empty template and save the scene.
Before we go on, let’s discuss two more events (we already know Mouse related events).
on "ActorEntry" occurs when actor enters the current region. This is the basic logic behind so called trap regions. Trap regions mean that actor triggers an event upon entering certain portion of the screen.
on "ActorLeave" occurs when actor leaves the current region.
Last thing we’d need for this example is taking the game out of player’s control. This is the vital part of successful game design. We want to trigger a scene and in the middle player clicks somewhere else, character didn’t finish the whole sequence and you’re stuck in an unsolvable loop. This kind of nightmare met many designers so let’s avoid it while we still can.
Game.Interactive = true / false; Takes the game out of players control or returns it back. Always remember to return the game back to Interactive mode or your game will be stuck and players will get angry. So always think of the noninteractive mode as of a block.
We’ll try to combine those ideas into our first more complex game logic.
First we’ll adapt the door.script to look like this:
on "LeftClick" { Game.Interactive = false; // We want our player to make more things at once and we don't want to be interrupted. actor.GoToObject(this); this.Active = false; // We disable the door so the hotspot is not visible or active anymore. actor.Talk("Oh no. The door is welded shut."); Game.Interactive = true; // Allow player to play some more. }
Then we open the exit.script and add a new event there. The file should look like this:
#include "scripts\base.inc" on "ActorEntry" //Actor entered the Exit region { Game.Interactive = false; var door = Scene.GetNode("Door"); //We get the object corresponding to the scene name door into a variable named door if (door.Active) // If door was not clicked, it was not disabled, so this path will be used { actor.Talk("I don't want to return to my flat before I am sure I can't enter the warehouse!"); } else //we know that the door is welded shut so we can leave the scene { actor.Talk("Oh well, there really is no way how could I enter that damn warehouse"); Game.ChangeScene("scenes\room\room.scene"); } Game.Interactive = true; }
Okay. Time to explain what’s going on in here. Our thought process as a game designer was that player needs to enter the warehouse but the warehouse is welded shut. We for some reason need our player to discover the fact that the door is welded shut so the game could proceed. So we decided to allow him to change the scene only after he discovered that the warehouse was welded shut.
Code-wise we have the region which would be normally used as an exit region, but we’ve added the condition that until the door region was active (which means that player didn’t click on it because if he did, the door region turns inactive), player can’t leave the screen.
Save all scripts and run the game. Test the game thoroughly so you’re sure that you really understand the coding behind this logic. It’s one of the critical aspects of adventure game designing and if you get this right, you’ll be in no time creating your own adventures.
Important:
Critical thing to know is how path in wme works in general. We’ve seen in Game.ChangeScene("scenes\room\room.scene"); that we didn’t start with the data path. That’s totally correct! Write it thousand times – I’ll never include the root package name in the referenced path. Why? Because this would work only in debug mode, and when you compile your game in the packages and send it around, it won’t work. The same applies to all files (images, sounds etc.). Never reference files outside of packages. They’ll work in debug mode, but they won’t get compiled and you’ll be in it for the worst nightmares. Think of package as of a virtual root directory and all references should be starting from that point. More about that in the chapter about packages.
Let’s move on to the last part of this chapter, which will cover the daemon issue. We’ll start right off with our first daemon. Before we explain how it works, we’ll have to take a look at a few new methods:
Game.AttachScript(filename); - Attaches a new script file to the Game object, making it global for the whole game. This script is attached until it’s detached or ends.
Game.DetachScript(filename); - Detaches script from the Game object.
Scene.AttachScript(filename); - Attaches a new script file to the current scene, making it active until player doesn’t change the current scene, or script is detached or ends.
Scene.DetachScript(filename); - Detaches script from the Scene object.
We can of course attach script to the scene object in the Scene Editor or script to Game object in the Project Manager, but we want our script to be attached in the middle of the gameplay.
Next command we’d learn is totally necessary for the daemon writing. It’s command Sleep.
Sleep(time); - Pauses the script for certain amount of milliseconds.
Sleep serves two purposes. First - it lets the script wait for certain amount of time and second - it hands the program flow to other threads. Here’s the typical daemon pitfall:
var a = 0; while (1) // infinite loop, the same as while(true) { a = a + 1; if (a > 100) a = 0; }
Never do this. It’ll halt your computer. Script takes over the whole processing power and the rest of the game never get control back. This way also Windows never gets control back which results in hung up computer. So how to fix this?
var a = 0; while (1) { a = a + 1; if (a > 100) a = 0; Sleep(1); }
This daemon is correct (remember our previous chapter), because it hands over the control from the script. One millisecond is here only formally. The important fact is that you DO hand over the control.
So enough theory and let’s make some daemon fun with the commands, we’ve just learned. Oh wait. We’d need one more command:
Game.Msg(text); - although we’ve seen it before let’s recap that it displays an onscreen debug message. Very useful command.
What we’re going to do is a funny little timed sequence, which occurs in our warehouse scene. The scenario is as follows. Player examines the door to discover that there’s a time bomb attached to it. If he can’t exit quickly enough, the Scene is reloaded (it’s the hardcore-adventure-fan-way how to tell the player, that he has died).
First we’ll create the new script in the scr folder (from the empty template) and name it bomb.script.
Tip: if you get more skilled, you can add the new files manually by creating a text file with a script extension created in the corresponding folder.
Then we modify the door.script so it’ll read the following:
#include "scripts\base.inc" on "LeftClick" { Game.Interactive = false; // We want our player to make more things at once and we don't want to be interrupted. actor.GoToObject(this); this.Active = false; // We disable the door so the hotspot is not visible or active anymore. actor.Talk("Oh no. There's a timed bomb attached to the door! I have to find an exit before it explodes."); Scene.AttachScript("scenes\warehouse\scr\bomb.script"); Game.Interactive = true; // Allow player to play some more. }
I’ve hilighted the line which attaches our new script and thus starting the countdown.
Now let’s look at the bomb.script contents.
#include "scripts\base.inc" for (var timer=6;timer>-1;timer = timer -1) // let's set up a loop which would go down from 6 to 0. { Game.Msg("Countdown: " + timer); //Let's display how much time do we have left. Sleep(1000); // Sleep one second (and also hand the game over to other threads) } Game.Interactive = false; //Death cutsene actor.Talk("I've just died. Let's try again"); Game.ChangeScene("scenes\warehouse\warehouse.scene"); var door = Scene.GetNode("Door"); door.Active = true; // If we die, we need to return the door to its active state or we'll never discover the bomb again. Game.Interactive = true;
And the last change we’ll make is to the exit.script. If we found an exit in time, actor will be able to escape, but what if he arrives there in the nick of time and while talking his line, the time runs away and he dies? So let’s prevent this from happening by detaching the bomb script and letting him say something more reasonable.
So the contents of the exit.script would look like this:
#include "scripts\base.inc" on "ActorEntry" { var door = Scene.GetNode("Door"); Game.Interactive = false; if (door.Active) { actor.Talk("I don't want to return to my flat before I am sure I can't enter the warehouse!"); } else { Scene.DetachScript("scenes\warehouse\scr\bomb.script"); actor.Talk("Phew. That was close"); Game.ChangeScene("scenes\room\room.scene"); } Game.Interactive = true; }
Save everything and test our little scheme to see some more of the effects.
On closing of this chapter I’ll introduce a little change to our scheme. This change is not necessary in our example, but it will be necessary in different case. For now we’re testing, if the entity region Door is Active for another decision. But what if we need to apply the similar logic to more than one scene? If we stand in different scene, we logically can’t use entity this way because it’s not anymore part of the current scene.
So what we can do is using a global variable. Let’s make a change to our scripts then!
Last code listing for this script triplet is then (changes in bold):
bomb.script
#include "scripts\base.inc" for (var timer=6;timer>-1;timer = timer -1) // let's set up a loop which would go down from 6 to 0. { Game.Msg("Countdown: " + timer); //Let's display how much time do we have left. Sleep(1000); // Sleep one second (and also hand the game over to other threads) } Game.Interactive = false; //Death cutsene actor.Talk("I've just died. Let's try again"); var door = Scene.GetNode("Door"); door.Active = true; // If we die, we need to return the door to its active state or we'll never discover the bomb again. global doorClicked = false; Game.ChangeScene("scenes\warehouse\warehouse.scene"); Game.Interactive = true;
door.script
#include "scripts\base.inc" on "LeftClick" { Game.Interactive = false; // We want our player to make more things at once and we don't want to be interrupted. global doorClicked = true; actor.GoToObject(this); this.Active = false; // We disable the door so the hotspot is not visible or active anymore. actor.Talk("Oh no. There's a timed bomb attached to the door! I have to find an exit before it explodes."); Scene.AttachScript("scenes\warehouse\scr\bomb.script"); Game.Interactive = true; // Allow player to play some more. }
exit.script
#include "scripts\base.inc" on "ActorEntry" { var door = Scene.GetNode("Door"); Game.Interactive = false; global doorClicked; if (!doorClicked) { actor.Talk("I don't want to return to my flat before I am sure I can't enter the warehouse!"); } else { Scene.DetachScript("scenes\warehouse\scr\bomb.script"); actor.Talk("Phew. That was close"); Game.ChangeScene("scenes\room\room.scene"); } Game.Interactive = true; }
We’ve seen in this chapter some neat tricks with the way how we can handle the game logic. We’ve also learned some of the important concepts for the tying scene to the code and we are able to use a couple of very basic commands and object methods. But for now we all the time used a lot of prebuilt demo code. Next chapter is all about starting the project from scratch. We’ll build upon this blank project until we create a little feature packed game. So stay tuned, we’re getting into it.