In the last post we created our hero. Now lets make him move in four directions using the arrow keys. We will also implement collision detection so he doesn’t run off the map or on top of rocks.
Split Map State and View
Before starting, the current Map
class holds too many responsibilties. It is responsible for holding the map state (current tiles, entities) and holding the map view (what you see on screen). It is important to keep all view logic in its own class. A class with multiple responsibilities is harder to use, reuse and contains logic for two purposes which won’t always go hand in hand. In the case of Map
its interface is diverged into one set of methods for handling view logic and an other for game logic. This creates a monster with two sets of logic which cannot be untangled. What if you want to implement split screen? It’s not possible with the current Map
class.
Lets create a MapView
class, it will be our visual representation of the current map and always keep in sync with it. Right now it only contains sprites for all the layers and an entity layer for entities of course. All of our map state is left in the original Map
class.
With our view logic out of the Map
class how will the mapView keep in sync with map state? The simpliest solution is to add some glue in our Game
class to add and remove views on map changes.
The add entity and remove entity triggers of course when a new entity is added or removed from the map. The callback function will then add or remove it from the mapView.
This is one of the event callbacks to keep the mapView in sync. It adds new entities to the mapView. The SimpleEvent<T>
is a general purpose event which holds a generic e.data
property defined as T
, in this case it is Entity
although it can be any class choosen.
Setting up collision
Before letting our hero move, it’ll first be easiest to define which tiles are walkable. The TileData
class needs a new attribute to identify which is walkable or unwalkable. In our Tilesheet
class we can then set the values per-tile. The rock and sign tiles will be marked as unwalkable and by default all other tiles will be walkable.
Getting collision info from the map
Next we need to add two methods to our Map
, one to find out if the given coordinate is off map and an other to find out if the wanted tile is walkable.
The tile from bottomLayer
and middleLayer
are both taken and the walkable value is computed for both. A rock will usually be on the middle layer while something like lava would be on the bottom. Checking both will ensure both cases are handled. The topLayer
is special because it is always above the hero so it wouldn’t make sense for it to prevent movement. Remember every game object has a reference to map when it is added to the instance map. This will make it easy to use these methods from inside the game object like you will see.
Main loop
In order to move the hero a main loop has to be created. This is a continuous process that calls all registered objects every interval, based on the framerate. Just about every game has one and is needed for anything which has to be updated every frame, like animations, tweening, artificial intelligence and physics. Since only one process is currently in our game the main loop will only update our hero.
Before a new screen redraw takes place the onEnterFrame
method will be called allowing the game to update one step. It will call the _game.update
method which will then call update
all update on all registered game objects active in the game.
Movement
Entities in our RPG can only move in one of four directions at a time. Up, down, right and left, so it would make sense to place these values in a global object called Direction
so the directions available are clear and it can be used anywhere.
Where should the logic for movement live? The logic for deciding can the entity move in this direction and the logic for continuing movement until reaching the end destination. The logic can be placed inside the Entity
class but entities know nothing of movement and should know nothing. If the logic is placed in there the Entity
will have multiple responsbilities and not every entity will move the same way.
The best solution is the break out movement into its own component. A component defines functionality for an entity, it is a place for defining input, movement and whatever else. Each entity will have an update
method which then calls update
on all of its components.
The components are passed into the constructor of entity.
The InputComponent
will handle the key inputs and the MoveComponent
will supply an interface for requesting movements and moving the actual entity. Once the components are inside the constructor each are then set with a reference to gameObject
which is the owner of the component. The components will then know the entity it is controlling. It doesn’t know it is an entity but the component does need to know things like position and needs a reference to map for it to function.
The MoveComponent
does two things, it has methods for requesting movement in all four directions and it will move the entity to the requested tile.
The moveUp
will first ensure the entity currently isn’t moving. It will then ensure the next tile above the entity is walkable. If the tile is walkable the component will then set the wanted direction and trigger a movement phase by setting isMoving
to true.
This update
method is called by the hero’s update
method every frame. If the entity is moving this method will move the entity a little bit in the wanted direction every frame. After moving the entity it will then check if it is perfectly on top of a tile. If that is true it means the entity arrived at its wanted destination and can stop moving.
This method is defined on the base gameObject
class. It is a simple way of finding out if the currently x and y positions can evenly divide by the tile size. Which if true means the game object is on a tile and not between two tiles. This is an easy way to figure out if the game object is in transition to an other tile.
There is one bug to be aware of, if the speed of entities does not perfectly multiply into the tile size entities will over shoot the target tile and continue moving, potentially forever.
Listen for key presses
We got movement figured out, we got collisions and we have a main loop for running it. The last piece is key input. A new input component is needed for listening to key presses and converting that into movement requests. First we have to listen to KeyboardEvent.KEY_DOWN
events and save the current key states in a global class called KeyState
. Putting key states inside a global class is great because now components don’t need to add keyboard events and add all this boilerplate code to get the wanted key info. All of that is already taken care of in the KeyState
class.
This listens to every key on your keyboard and tracks which ones are currently pressed down.
A public static method isDown
is available for use anywhere in the code base. Chances are we will have multiple components and objects listening for key downs. This is a great way of reducing code duplication. You might be thinking, global methods are bad. Everything should be kept as local as possible to reduce coupling and increase reusability. That is true but not every global method or singleton is wrong. The KeyState
is fine because its global method isDown
can only be read, it is a one-way global object. Now if it had a global method which modified key states that would be different. Lets just pretend the keysDown
map is private.
The InputComponent
checks for key downs on every update and moves the entity in the wanted direction if it is not currently moving. An instance of move component has to be passed into the input component so it has access to the move methods. The Keyboard.RIGHT
constants are helper constants from flash.ui.Keyboard
. With all that the hero should be controllable with the arrow keys.