Polymorphism And Game Engines
From Compsci.ca Wiki
Contents |
Introduction
For this tutorial I will be explaining how to build your own simple reusable and self managing game engine using polymorphism and classes in Turing. In case you're new to the idea of a "game engine", a game engine is essentially a centralized system which handles all input, output, updating and management for a game. The game engine is responsible for loading, displaying and processing all of a game's content, be it levels, sprites, textures, shaders etc. These resources are not part of the game engine itself, but stored in external files. As well as loading content, the game engine is in charge of interfacing (although not usually directly) with various hardware, such as the video card and sound card. Essentially, a game engine is the skeleton of a game, which supports all of the content of a game.
There are many advantages to using a game engine over hard coding everything. Having a game engine makes updating content and adding functionality a lot easier, as it is possible to change the content of a game without having to rebuild everything or do additional programming. This allows more time to be spent on actually designing a game instead of worrying about the loading of resources and the updating of game objects. As well, game engines can be reused again and again for different projects, making it worth the effort it takes to create a good engine.
Although game engines are a must for large and complex games such as RPGs and shooters, most small and simple games don't need to be made using a game engine. For example, a simple game of pong doesn't need to load many resources or need to worry about many complicated things, so it's alright, and probably a good idea to hard code it. However, when you begin to have more complex object interactions and needs, like 20 objects which collide with each other, having a game engine makes it possible to have this automatically managed and handled for you without any sort of direct or specific or programming.
Now that you (hopefully) understand what a game engine is, let's talk about how a game engine works programmatically. A game engine is usually represented as a class which contains an array or collection of game objects, and methods (procedures and functions) which control the game. The game engine is either told to start the game (it updates itself) or a loop in the main program calls an update function. For the sake of convenience, the game engine I will be building will use the first method. When the start method is called, the game engine enters an infinite loop where it calls each object's draw, and update procedure. This is achieved through polymorphism; each game entity inherits from a master "object" class which provides overrideable methods which perform certain tasks for the entity.
Anyways, enough talk, let's actually start building something.
Setting up your ?Object? Class
When you first build your generic object class, you want it to encompass all the features that your game entities will possess. Usually in larger engines you have multiple of these generic classes which are specific to certain types of objects, for example, if you had an object that needed only to draw itself and was never going to move, it might be in your interest to create a "detail" type class, which contains only the parts it needs to draw itself and nothing more. This way you can conserve memory, and be able to target specific entities of your game (you would store "detail" objects in a different array or collection than say, all your moving objects). This gives your game engine more flexibility in what it can do. However, for this tutorial I'm going to keep it simple and only use one type of class.
So let's first start off by defining our class. It will contain variables for a position, an image, speed and direction as well as a creation and destruction procedure and two "deferred" procedures which will serve as the game engine's means of "communication" with the object. As well, the class will contain a few other "internal" procedures such as a procedure for moving (not using speed and direction). Remember this is only the blueprint for an entity in the game, so you don't want to make anything specific to the actual game you are creating, as that is defined elsewhere.
Turing:
% A basic abstract class, all game entities will inherit from this. % Note: All position variables should use the "real" type and % be rounded, so when drawing the movement looks smoother. % But for simplicity I will use the "int" type. class Object export Create, DrawObject, Update, PosX, PosY, Speed, Direction var PicId : int % The picture Id of the object var ImageName : string % The image file name, in case we want to use it later var PosX : int % The x position of the object var PosY : int % The y position of the object var Speed : int := 0% The speed of the object var Direction : int := 0 % The direction of the object, 0 = east, 90 = north, 180 = west, 270 = south % Call this procedure when you first intialize the child class % img - The filename of the image for the object % posx - The x position of the object % posy - The y position of the object procedure Create (img : string, posx, posy : int) ImageName := img PosX := posx PosY := posy PicId := Pic.FileNew (ImageName) end Create % Moves the object based on set x and y values % x - Movement in the x axis (or the x position) % y - Movement in the y axis (or the y position) % relative - If true, x and y are based on the previous x and y positions eg. (10 + 2,10 + 3) % If false, the objects position becomes x and y eg. (2,3) procedure Move (x, y : int, relative : boolean) if (relative) then PosX += x PosY += y else PosX := x PosY := y end if end Move % Moves the object based on its speed and direction procedure MoveDir () Move (round (Speed * cosd (Direction)), round (Speed * sind (Direction)), true) end MoveDir % Draws the object; Abstract deferred procedure DrawObject () % Alows the object to update itself; Abstract % Keys - The currently pressed keys, returned by Input.Keydown() deferred procedure Update (Keys : array char of boolean) end Object
Ok, so there's the code for our basic game entity. It's fairly straight forward, just some basic variable and method declarations. The only "not straight forward" part of the class is the declaration of the deferred or abstract procedures at the bottom. Basically what's happening, if you're not sure, is that we're telling Turing that we will write the actual code for the procedures in another class that inherits this one. Overriding these functions, as it is called, gives us the ability to make our objects perform specific things, like having one entity move left when its update procedure is called, and another entity move right when its update procedure is called.
Now that we have our basic game entity class, we will move on to making the actual game engine itself.
Setting up your Game Manager
The game engine class is probably going to be the most complex part of the tutorial, but I'll try to keep it relatively simple for this tutorial. It will consist of a dynamic array of game entities, variables for controlling the frames per second and a few procedures which control the game engine. These procedures are responsible for adding and deleting objects and updating the game. So let's start this off with a couple code snippets.
Turing:
import Object, Input export Create, Run, AddObject var Entities : flexible array 0 .. 1 of pointer to Object % The entities list var Keys : array char of boolean % The currently pressed keys var Fps : int % The number of frames per second var FpsMilli : int % The number of milliseconds between steps var lastTime : int % The last time the the game was updated var thisTime : int % The current time
Firstly we have the main variable declarations, it should be pretty straight forward but I want to take a moment to explain how the "Entities" array works. The array begins by having two empty elements of the generic game entity type "Object". It?s basically like having a list of people, every person is different, but still has the same basic features (a name, eyes, ears, etc.) which you can ask for, e.g. everyone would (hopefully) have a response for "what is your name?". The Entities list works the same way, but the "same basic features" are the ones inherited from the "Object" class.
Turing:
% Call this procedure when you first % fps - The number of frames per second the game will update at procedure Create (fps : int) Fps := fps FpsMilli := 1000 div Fps lastTime := Time.Elapsed end Create
Next, we have the Create procedure, which should be called before the Run procedure. It assigns the Fps variable which is used to assign the "FpsMilli" variable. The "FpsMilli" variable is used to tell how often (in milliseconds) the game should be updated based on the "Fps" variable. For instance, at 1000 fps the game is updated every millisecond (or it attempts to), and at 30 fps the game updates every 33 milliseconds (1000 div 30).
Turing:
% The main game engine procedure, call this to start the game procedure Run () loop thisTime := Time.Elapsed if (thisTime - lastTime >= FpsMilli) then lastTime := thisTime cls Input.KeyDown (Keys) if (Keys (KEY_ESC)) then return end if if (upper (Entities) - 1 > 0) then for i : 0 .. upper (Entities) - 2 Entities (i) -> Update (Keys) Entities (i) -> DrawObject () end for end if View.Update () end if end loop end Run
Next, we have the Run procedure, which is called when you want to run the game (what a shock). First the "thisTime" variable is assigned the current time since the program began, this is used next to find the time since the last update. If the time is greater than or equal to the "FpsMilli" variable, the game updates.
Each time the game updates, the "lastTime" variable is assigned the current time which will reset the timer. Then the screen is cleared, and if the escape key is pressed the Run function exits, ending the game. Next we iterate through the Entities array. This is where the deferred procedures come into play, for each Object in the array we call its overridden procedure for drawing and updating. You should always draw each object last or after everything has been handled, otherwise whatever is on the screen will always be one step behind everything else. After all the objects have been updated we call "View.Update()" to swap the back buffer.
Turing:
% Adds an object to the entities list % obj - A pointer to the object function AddObject (obj : pointer to Object) : pointer to Object Entities (upper (Entities) - 1) := obj new Entities, upper (Entities) + 1 result obj end AddObject
Finally, we have the AddObject function, which simply adds an object pointer to the Entities array and reserves another spot in the array for the next call. You'll notice this is a function and not a procedure. This is so you're able to both add an object to the game engine and call its Create procedure in the same line.
Well, there it is, a really simple game engine. But wait! We're not done; we still have to make it do something! So let's move on and make the game engine do something. Everything up to this point has been written in the "Engine.t" Turing file in the accompanying zip archive. However, the next two sections will refer to "Objects.t" and "Game.t", spreading code across files makes things easier to edit and provides further encapsulation.
Creating Game Elements
Ok, it's all pretty straight forward from here; to define a new game entity you simply inherit Object then overwrite the abstract procedures by marking them as a "body procedure". When you inherit from the Object class you now have access to all of its variables regardless of their protection level (whether they are exported or not) and you can freely manipulate them. As well you can add new variables to the class, but be aware that you won't be able to access them externally.
Turing:
class Player inherit Object Speed := 4 body procedure DrawObject () Pic.Draw (PicId, PosX, PosY, picMerge) end DrawObject body procedure Update (Keys : array char of boolean) if (Keys (KEY_UP_ARROW)) then Move (0, Speed, true) end if if (Keys (KEY_DOWN_ARROW)) then Move (0, -Speed, true) end if if (Keys (KEY_LEFT_ARROW)) then Move (-Speed, 0, true) end if if (Keys (KEY_RIGHT_ARROW)) then Move (Speed, 0, true) end if end Update end Player
The one I wrote for this tutorial simply draws a red block (loaded from an image, you'll see in the next section) and moves the block based on keyboard input. Pretty straight forward and there's not really much to explain. And there shouldn't be much to explain, making a new type of entity is as easy as that, the game engine handles everything else. All you have to do is add instances of the objects to the GameEngine class, which I will talk about next.
Adding Game Elements
Well, we've reached the final part of this long tutorial, adding instances of our game entities to the GameEngine class. This is just a matter of defining a pointer and initializing new instances with "new". Take a look at the "Game.t" file in the zip archive.
Turing:
setscreen ("graphics:640;480,offscreenonly") include "Engine.t" include "Objects.t" var Engine : pointer to GameEngine new Engine Engine -> Create (30) var Player_ : pointer to Player new Player_ Engine -> AddObject (Player_) -> Create ("Resources/Player.gif", 320, 60) new Player_ Engine -> AddObject (Player_) -> Create ("Resources/Player.gif", 0, 60) Engine -> Run ()
Once again, pretty straight forward. Here I show how the AddObject function works; you can use the value it returns to call the Create procedure on the same line. This about wraps up the tutorial.
Conclusion
I hope you've learned something from this tutorial, and I encourage you to play around with the code, not that it really does much. The generic object class we built in this tutorial doesn't really provide much functionality but I encourage you to add to it. Normally, the game engine would contain other things like collision checking, particles, other entities etc. I may write a second part to the tutorial which addresses making and loading levels, although I?m not sure if it can be done in Turing, at least to the level of automation I have achieved in other languages. Anyways, thanks for reading my tutorial and happy coding.
Credits
Author: Stove
Added to Wiki by: TheFerret