Kha Shmup Tutorial Part 3
In part 2 we got the player’s ship on screen. This time we’re going to give the player some control over it.
Timer.hx
Something you’ve probably came across if you’ve read any tutorials on making simple games is frame independent movement, sometimes called delta timing. The basic idea is that if you have two computers - both of which are playing your game. Each computer has a player that holds down the “move right” button, but let’s say that once computer just updates slower than the other. If your movement code is based on number of updates called, you are going to have different experiences between these two computers - one player’s character will lag behind the other.
To combat this you can base your movement code on the amount of time in frames. This way when the slower computer updates inbetween frames, the movement will be scale accordingly, meaning the character on each computer will appear to move the exact same distance.
To achieve this we will be creating a file called Timer.hx:
package;
import kha.Scheduler;
class Timer {
public var deltaTime(default, null): Float;
public var lastTime(default, null): Float;
public function new() {
reset();
}
public function update() {
var currentTime = Scheduler.time();
deltaTime = currentTime - lastTime;
lastTime = currentTime;
}
public function reset() {
lastTime = Scheduler.time();
deltaTime = 0;
}
}
If you haven’t seen the notation on deltaTime and lastTime before, those are properties, they allow to specify additional contraints to your fields. The first in that tuple defines the read access, and the second specifies the write access. We are saying that deltaTime and lastTime have default read access, meaning anyone who has an instance of a Timer can read either of these fields. The null in the second position means that these fields are only set-able from within an instance of Timer itself. So this is a basic “read-only” property.
This class keeps track of the lastTime (that is, the previously seen timestamp provided by Kha), and everytime there is a call to update we’re calculating the deltaTime by finding the difference between the currentTime, and we are resetting lastTime.
Controls.hx
The next thing we need is a way to keep track of controls. Open up a file named Controls.hx:
package;
import kha.Key;
class Controls {
public var left: Bool;
public var right: Bool;
public var up: Bool;
public var down: Bool;
public function new() {}
public function keyDown(key: Key) {
switch (key) {
case LEFT:
left = true;
case RIGHT:
right = true;
case UP:
up = true;
case DOWN:
down = true;
default:
// no-op
}
}
public function keyUp(key: Key) {
switch (key) {
case LEFT:
left = false;
case RIGHT:
right = false;
case UP:
up = false;
case DOWN:
down = false;
default:
//no-op
}
}
}
This class is pretty much just two big switches to swap four different flags - each flag represents a possible button, and depending on the Key passed to either switch, we update the flag. We’ll see more about keys in a bit.
We will be using these classes in our main game class, but let’s update our Ship first to take into account controls and time, and we’ll add in some properties for fun:
Ship.hx
package;
import kha.Image;
import kha.graphics2.Graphics;
class Ship {
private var image: Image;
public var x: Int;
public var y: Int;
public var width(get, null): Int;
public var height(get, null): Int;
public var speed = 300.0;
public function new(x: Int, y: Int, image: Image) {
this.x = x;
this.y = y;
this.image = image;
}
private function get_width(): Int {
return image.width;
}
private function get_height(): Int {
return image.height;
}
public function render(g: Graphics) {
g.drawImage(image, x, y);
}
public function update(controls: Controls, deltaTime: Float) {
if (controls.left && !controls.right) {
x -= Math.round(speed * deltaTime);
} else if (controls.right && !controls.left) {
x += Math.round(speed * deltaTime);
}
if (controls.up && !controls.down) {
y -= Math.round(speed * deltaTime);
} else if (controls.down && !controls.up) {
y += Math.round(speed * deltaTime);
}
}
}
There are a few changes here - let’s start with the width and height properties. We see that we have specified null write-access, but the read-access is specified with a “get”. You will also notice to methods below - get_width, and get_height. These methods correspond to the getters for those fields. This allows us to read width or height given an instance of a ship as you would any other field, but we are returning the width and height of the image associated with the ship. This allows us to retrieve these properties from the private variable image, without having to change image’s visibility.
The second change is the update method: this is a method taking in controls and deltaTime. We are then updating the ship’s position based on this, and we are using deltaTime to scale this movement, giving us our frame-independent movement. Note that we are rounding this value - x and y are Ints, which was something we decided on because the image filtering may change when an image is on non-integer pixels. Now, this may be fine for your game and you may want to experiment with this, but for this game keeping positions as integers keeps the images smooth and crisp.
Now let’s go back to our KhaShmup game class and take into account the changes.
KhaShmup.hx
package;
import kha.Assets;
import kha.Color;
import kha.Framebuffer;
import kha.Image;
import kha.Key;
import kha.Scaler;
import kha.System;
import kha.input.Keyboard;
class KhaShmup {
private static var bgColor = Color.fromValue(0x26004d);
public static inline var screenWidth = 800;
public static inline var screenHeight = 600;
private var backbuffer: Image;
private var controls: Controls;
private var initialized = false;
private var ship: Ship;
private var timer: Timer;
public function new() {
Assets.loadEverything(loadingFinished);
}
private function loadingFinished(): Void {
initialized = true;
// create a buffer to draw to
backbuffer = Image.createRenderTarget(screenWidth, screenHeight);
// create our player
var shipImg = Assets.images.playerShip;
ship = new Ship(Std.int(screenWidth / 2) - Std.int(shipImg.width / 2),
Std.int(screenHeight / 2) - Std.int(shipImg.height / 2),
shipImg);
controls = new Controls();
timer = new Timer();
Keyboard.get().notify(keyDown, keyUp);
}
public function render(framebuffer: Framebuffer): Void {
if (!initialized) {
return;
}
var g = backbuffer.g2;
// clear and draw to our backbuffer
g.begin(bgColor);
ship.render(g);
g.end();
// draw our backbuffer onto the active framebuffer
framebuffer.g2.begin();
Scaler.scale(backbuffer, framebuffer, System.screenRotation);
framebuffer.g2.end();
update();
}
private function update() {
timer.update();
updateShip();
}
private function updateShip() {
ship.update(controls, timer.deltaTime);
// limit the ship to the width of the screen
if (ship.x < 0) {
ship.x = 0;
} else if (ship.x + ship.width > screenWidth) {
ship.x = screenWidth - ship.width;
}
// limit the ship to the height of the screen
if (ship.y < 0) {
ship.y = 0;
} else if (ship.y + ship.height > screenHeight) {
ship.y = screenHeight - ship.height;
}
}
private function keyDown(key: Key, value: String): Void {
controls.keyDown(key);
}
private function keyUp(key: Key, value: String): Void {
controls.keyUp(key);
}
}
There are a bunch of changes here. For one we instantiate our timer and our controls.
Secondly, in loadingFinished we make a call to Keyboard.get().notify, and we provide two methods we define at the bottom of this file - keyDown and keyUp. This is registering our methods to Kha’s input system, so that when the keyboard has a key up or key down, the appropriate method gets called. These are where we update our Controls instance. Note that both of these methods take a Key - which is an enum saying what kind of key is pressed - in this key we are caring only about specific directional keys, but this could also be something like the Shift key, or even a character key. This leads to the second parameter of these methods - the String value. Kha sends character keys such as the “A” key as a String. I think this is somewhat of a weird decision, as I would have much preferred the traditional keycode, but this is how Kha was built.
At the end of our render method we also make a call to update, and here is where we update both the timer as well as the ship. We pass our ship the controls, and we know it will update its position. We then limit the ship’s movement to be within our screen, disallowing the player from flying off camera.
Running the Application
Now when you run the application you should see your ship like before, but by pressing the arrow keys your ship should be moving. If you are not able to do this, check out my part-3 branch:
https://github.com/jamiltron/KhaShmup/tree/part-3
In Summary
We learned how to use a timer to get frame-independent movement, and we also built a class to contain our game’s controls. In part 4 we will be adding more controls, allowing the player to shoot, and we will also be learning about object pooling.