-
Notifications
You must be signed in to change notification settings - Fork 83
A Platformer
All of the code snippets used in this tutorial have complete versions available in oak/examples/platformer-tutorial.
At a low level, entities are any type that get registered to be an Entity with the oak/v4/event
package, but for the purposes of this tutorial we'll be using the oak/v4/entities
helper package to construct the elements of our game world.
We'll start with a player character, which for this is just going to be a box that moves around and can jump. To initialize a moving character using the entities
package, we want to call entities.New
:
package main
import (
"image/color"
oak "github.com/oakmound/oak/v4"
"github.com/oakmound/oak/v4/alg/floatgeom"
"github.com/oakmound/oak/v4/entities"
"github.com/oakmound/oak/v4/scene"
)
func main() {
oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) {
entities.New(ctx,
entities.WithRect(floatgeom.NewRect2WH(100, 100, 16, 32)),
entities.WithColor(color.RGBA{255, 0, 0, 255}),
)
}})
oak.Init("platformer")
}
New
takes in a scene context and any number of additional options which in this case makes a red rectangle with the WithRect
and WithColor
options. The size of the character is determined by the size of the Rectangle's options.
Running this file will get us a screen like this:
We'll now get the character to move left and right when we press A or D. To do this, one approach would be to respond to keyboard events, track when A and D are pressed down, then move the character so long as they aren't released. We aren't going to do that because what we'll end up doing is a lot simpler, and that approach can present concurrency problems down the line.
We're going to set a default speed on the character via WithSpeed
and then we're going to bind a function that will get called at the start of every logical frame, binding it to event.Enter
. In this function we'll check if A or D is pressed, and if they are, we'll move the character appropriately:
import (
"image/color"
oak "github.com/oakmound/oak/v4"
"github.com/oakmound/oak/v4/alg/floatgeom"
"github.com/oakmound/oak/v4/entities"
"github.com/oakmound/oak/v4/event"
"github.com/oakmound/oak/v4/key"
"github.com/oakmound/oak/v4/scene"
)
func main() {
oak.AddScene("platformer", scene.Scene{Start: func(ctx *scene.Context) {
char := entities.New(ctx,
entities.WithRect(floatgeom.NewRect2WH(100, 100, 16, 32)),
entities.WithColor(color.RGBA{255, 0, 0, 255}),
entities.WithSpeed(floatgeom.Point2{3, 3}),
)
fmt.Printf("Creating character at %f, %f", char.X(), char.Y())
event.Bind(ctx, event.Enter, char, func(c *entities.Entity, ev event.EnterPayload) event.Response {
// Move left and right with A and D
if oak.IsDown(key.A) {
char.Delta[0] = -char.Speed.X()
} else if oak.IsDown(key.D) {
char.Delta[0] = char.Speed.X()
} else {
char.Delta[0] = (0)
}
char.ShiftDelta()
return 0
})
}})
oak.Init("platformer")
}
Our bind call, which we can call on the character because as an entity it is composed of an event.CallerID
, works directly on the character itself.
Technically we don't need to bind directly to the character but this is cleaner for larger programs where bindings are reusable functions for multiple entities.
Now if we run the program and press A and D, we'll get something like this:
To add gravity we just need to keep track of a delta over time for what the character's current falling speed is. If there isn't ground below the character, we increase it, and if there is, then we set it back to zero.
In order to actually check whether there is ground, we'll create a collision.HitLabel
for ground and apply it to a new entity. Then each frame we'll have the character check if it's touching the ground or not.
//... at the top of the file
const Ground collision.Label = 1
//... In the event.Enter binding before the ShiftDelta
fallspeed := .1
hit := collision.HitLabel(char.Space, Ground)
if hit == nil {
char.Delta[1] += fallSpeed
} else {
char.Delta[1] = 0
}
//... In the scene start function
entities.New(ctx,
entities.WithRect(floatgeom.NewRect2WH(0, 400, 300, 20)),
entities.WithColor(color.RGBA{0, 0, 255, 255}),
entities.WithLabel(Ground),
)
The end result of this will have the same character, now falling onto some blue ground. You can walk the character off the side of the ground to fall off the screen.
The last thing we need for a basic platformer is adding in jumping. This is as easy as, in our gravity check, if we are touching ground, then if spacebar is down we should add a lot to our Y delta in the upwards direction. Because we already have gravity, this will get us jumping in arcs.
// ... after char.Delta[1] = 0
// Jump with Space
if oak.IsDown(key.Spacebar) {
char.Delta.ShiftY(-char.Speed.Y())
}
If we run this, we can jump but we can notice some immediate issues. One: we can also jump if we're stuck inside the ground, if we walk off the right and then back to the left. Two: We aren't flush with the ground when we land. We can apply a couple tweaks to our physics to correct this:
oldY := char.Y()
char.ShiftDelta()
hit := collision.HitLabel(char.Space, Ground)
// If we've moved in y value this frame and in the last frame,
// we were below what we're trying to hit, we are still falling
if hit != nil && !(oldY != char.Y() && oldY+char.H > hit.Y()) {
// Correct our y if we started falling into the ground
char.SetY(hit.Y() - char.H())
// Stop falling
char.Delta[1] = 0
// Jump with Space when on the ground
if oak.IsDown(key.Spacebar) {
char.Delta[1] -= char.Speed.Y()
}
aboveGround = true
} else {
// Fall if there's no ground
char.Delta[1] += fallSpeed
}
Feel free to read through the code and comments on the complete code, which has additional, relatively simple logic to correct cases like bumping into platforms from below or walking into the side of a platform. It isn't perfect, and I recommend playing around with it to see if you can find ways to improve it.
One particular challenge I'd present is to stop the player from shaking back and forth when you are jumping and you walk into a wall. Doing this will require changing what most of the Set
and Shift
functions operate on.
You might want to walk through the Top Down Shooter tutorial, to start working with the viewport, raycasting, and sprites.