OpenFL - Movement
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
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
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
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.
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.
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.
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.
MoveComponent does two things, it has methods for requesting movement in all four directions and it will move the entity to the requested tile.
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.
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
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.
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.