-
Notifications
You must be signed in to change notification settings - Fork 0
Part 3 Movement
In this installment, we're going to be utilizing the framework we put in place last time, by creating a new component and system that will allow us to move our player character around on the screen. This should be a pretty short one, as since we put in so much work last time, there really isn't much to do, aside from implementing the actual logic of movement. Lets just right in!
First up, we're going to need a new component that will indicate if the entity can move or not. There will be no data attached to this component, it will simply be what I call a flag component
. Flag components
simply indicate that an entity has a state, or can take certain actions, based on their presence. For our movement component, the lack of such on an entity could indicate that the entity is paralyzed, and can't move, or that it is a statue that can't inherently move around on its own. We could also optionally attach the speed at which the entity can move to this component, making it easy to see how fast each entity that can move is, but we'll save that for a later tutorial. For now, we'll use the presence of this component to tell us if the entity can make a move on its turn.
Our movement component looks like:
type MovementComponent struct {}
func (mc MovementComponent) TypeOf() reflect.Type {
return reflect.TypeOf(mc)
}
Nothing fancy. We've omitted any meta data, and just have an empty struct, with the required TypeOf method. You can see a flag component
is nothing special, its just a component with no data.
Next, lets go register our new component, and add it to the player. First, registering:
// RegisterComponents attaches any components we've outlined to the global ECS controller
func registerComponents() {
controller.MapComponentClass("position", PositionComponent{})
controller.MapComponentClass("appearance", AppearanceComponent{})
controller.MapComponentClass("movement", MovementComponent{})
}
Next, in our init
function, just below where we added our AppearanceComponent
to our player entity, lets add a MovementComponent
:
func init() {
.
.
.
controller.AddComponent(player, playerAppearance)
controller.AddComponent(player, MovementComponent{})
.
.
.
}
Great, we can now indicate that our player can move around. Now, to actually facilitate the movement, we need to add a new system that will capture user input, and apply it to the player.
You may recall that we already have a way of capturing and processing user input, on each screen that we define, in the form of the process_input
method for the screen. Why are we not using this facility to get and process user input, and instead using a system in the main game loop? Well, the answer is a bit long, but it boils down to logical grouping of functionality. Gogue gives you the option to process input on a per screen basis, and this comes in very handy when you need each screen to have different meaning for controls than another screen. But, we also have the ability to toggle on ECS use for each screen as well, which gives us another layer of flexibility in how we can handle things, without mucking up the screen with a bunch of case specific logic. By leaving the process_input
method of the our GameScreen
empty, and using the ECS to process input and movement, we're keeping all of our core game logic in one place, rather than spreading it around between the screen and the ECS.
Lets see this in practice. First up, we need to add a movement system. In systems.go
:
type SystemInput struct {
ecsController *ecs.Controller
}
func (is SystemInput) Process() {
// Get all entities that can be controlled by the player. Most of the time, this will just be the player entity, but
// it may be possible to take control of other entities. This system will block until the player has taken an action
key := ui.ReadInput(false)
if key == ui.KeyEscape {
// Shift the screen back to the title screen
_ = screenManager.SetScreenByName("title")
// Render the screen
screenManager.CurrentScreen.Render()
return
}
// Fow now, just handle movement
if ecsController.HasComponent(player, PositionComponent{}.TypeOf()) && ecsController.HasComponent(player, AppearanceComponent{}.TypeOf()) {
// Check if the player is currently capable of movement (has a movement component)
if ecsController.HasComponent(player, MovementComponent{}.TypeOf()) {
pos := ecsController.GetComponent(player, PositionComponent{}.TypeOf()).(PositionComponent)
// Clear the existing appearance from the screen, since it will be moving. This will prevent artifact trails.
ui.PrintGlyph(pos.X, pos.Y, ui.EmptyGlyph, "", 1)
var dx, dy, newX, newY int
switch key {
case ui.KeyRight, ui.KeyL:
dx, dy = 1, 0
case ui.KeyLeft, ui.KeyH:
dx, dy = -1, 0
case ui.KeyUp, ui.KeyK:
dx, dy = 0, -1
case ui.KeyDown, ui.KeyJ:
dx, dy = 0, 1
}
newX = dx + pos.X
newY = dy + pos.Y
ui.PrintGlyph(pos.X, pos.Y, ui.EmptyGlyph, "", 1)
newPos := PositionComponent{X: newX, Y: newY}
ecsController.UpdateComponent(player, PositionComponent{}.TypeOf(), newPos)
}
}
}
Thats a lot of code, so lets walk through it.
First, we define our Gogue System
(just a struct with a special process
method, you'll recall). Then, in the systems process
method, we begin by waiting for input from the player. You'll recall that the line:
key := ui.ReadInput(false)
means that we will block the execution of the program until we receive a keypress event from the user. Once we have captured the key the user pressed, we handle a special case for the ESCAPE
key. If that key is pressed, terminate the program.
Next, we check to see if the player has both a PositionComponent
and a MovementComponent
. We need both of these on the component in order to move. If we are missing the MovementComponent
, it means the player cannot move, and if the PositionComponent
is missing, it means we don't actually know where the player is, and how can move something that we don't know the position of?
Once we've asserted the presence of these, we grab the PositionComponent
instance from the player, and draw a blank character in the players current position. If we did not do this, when we moved the player, we would leave a trail of @
symbols behind.
Next, we check if the player pressed one of the arrow keys, or one of [L, H, K J] (standard vim movement keys), and if one of those was pressed, set the direction X and direction Y accordingly. For movement, right means we increase the X axis, and the Y axis stays the same, opposite for left. Up means we decrease the Y axis, and the X axis remains the same, opposite for down.
Finally, we increase (or decrease, depending) the values of the players current position, create a new PositionComponent
with the new location values, and then update the PositionComponent
on the player entity. The old PositionComponent
will be garbage collected, and the next time we render, the player will appear at the location of the new PositionComponent
. Thats it, now we can go register and use our new SystemMovement
. Back in our main file, in the register_systems
function:
// RegisterSystems registers any needed systems to the global ECS controller
func registerSystems() {
render := SystemRender{entityController: controller}
input := SystemInput{entityController: controller}
controller.AddSystem(input, 0)
controller.AddSystem(render, 1)
}
We assign our new system a lower priority (which means it will get processed first) than our render system, as we need to move things around before we redraw them, or they won't immediately appear in the correct location.
That's it! If we run our game, our newly registered system will automatically be called prior to any rendering that happens on the GameScreen
. If you change to the game screen, you should now also be able to press an arrow or vim key, and see the player move around the screen. Cool.
However, before we wrap up this section, we've got a small bug. You may have noticed that, upon changing to the GameScreen
from the main menu screen, there is nothing drawn to the screen until you actually hit a movement key, at which point the player character appears, and works as intended. The issue here is that when we switch screens, the game loop immediately picks up that we've activated an ECS enabled screen, and moves to systems processing. The first system it hits is our new movement processing system, which blocks everything until the user hits a key. Rendering doesn't happen until after this, thus the delay. So, how do we fix it?
Thankfully, Gogue Screens allow us to do something when first enter a new screen, via the Enter
method. Lets add the following to our GameScreen
Enter
method:
func (gs GameScreen) Enter() {
// Call the ECS SystemRender upon entering the screen. This will force all entities to draw to the screen
// immediately
controller.ProcessSystem(reflect.TypeOf(SystemRender{}))
}
Our ECS controller allows us to selectively call certain systems, by themselves. This comes in immensely handy in this case, as we want to render all our of renderable entities once, before we do anything else. This will call our SystemRender
once immediately as soon as we switch the GameScreen
, meaning that as soon as the player sees it, all our entities are rendered and ready to move. Neat.
That's it! We can now move our player around the screen. This has been a lot of work, for seemingly not much functionality. But we've accomplished quite a bit, in that we have a dynamic system of adding behaviors to entities, and allowing them interact with the game world we're going to start building. Next time, we'll generate an interesting and random map for the player character to wander around in, and also implement field of view to ensure the player can't see everything at once.
The code for this part of the tutorial can be found here, tagged as v1.0.0-tutorial3