To build upon the starscape shader from my last post I took it upon myself to build an asteroids style game on top. Thankfully conventional godot gamedev is much better known to me than shader stuff so I should be able to provide some helpful insight.
To start with I drew up a simple white triangle in photoshop to act as our player sprite. As the player requires custom physics this should be a kinematic body so we can interact with physics objects and control our character. Then we can add a collision shape drawn over the sprite which is a necessary child of the kinematicBody.
Now a script can be attached to the root node and we can work on the player controls. We require 4 to replicate asteroids: rotate left and right, move forwards and shoot. We can poll a few default keyboard inputs in Godot from the arrow keys and space bar/ enter key, giving them placeholder functions will look like this.
func _process(delta): if Input.is_action_pressed("ui_left"): turn_left() if Input.is_action_pressed("ui_right"): turn right() if Input.is_action_pressed("ui_up"): thrust() if Input.is_action_pressed("ui_accept"): fire_weapon()
There are a few key points here that require explanation. Firstly _process(delta) is a function that runs every frame on the object with the attached script and delta is the amount of time that has passed since the last frame. Each of those functions within if blocks is a placeholder that just runs the pass command like so.
pass tells the interpreter to do nothing. This is helpful for quick prototyping as we can write code that is well laid out without having to define all our functionality there and then and focus on whatever our most pressing matter is. This is a delightful artifact from python that I’m thankful Godot takes on board.
Input allows us to get an input from any source which we can define in the godot editors preferences. It has a few predefined shown in the image below. The is_action_pressed method lets us poll the inputs defined here and just check if the input is down. then if so run the function, this is helpful for continuous acts like movement.
This is a relatively simple process. We need to define a rotational acceleration and maximum rotational speed. Having these two variables will allow us to control the rate of change of rotation which would feel jarring if it didn’t change over time. All we need to do is check our current rotation isn’t beyond the limit and if so increase our current spin by the acceleration (in the appropriate direction where positive is counter clockwise and negative is clockwise).
var spin = 0 var r_acc = 0.01 var max_r = 0.15 func rotate_left(): if abs(spin) < max_r: spin -= r_acc func rotate_right(): if abs(spin) < max_r: spin += r_acc
The final part of this process is to apply our rotation in the process function so frame by frame we can update rotation.
there is a little novel jiggery pokery involved here to turn our players direction into a vector to move them by but other than that its a straightforward few steps.
var v = 0.0 var acc = 0.1 var max_v = 2.0 func thrust(): if v < max_v: v += acc
as you can see the thrust method itself is a little simpler as we are only moving positively between 0.0 and 2.0 however applying this velocity is where things become a little more complicated.
var velocity = Vector2(0, 0) var speed = 200 velocity = Vector2(v, 0).rotated($player.rotation) * speed velocity = $player.move_and_slide(velocity)
in the _process function we need to keep a track of the players velocity. The magic happens in Vector2(v, 0).rotated($player.rotation). Here we take a vector2 which has our v value from thrust as a magnitude. It is rotated to point in the direction of our ship and multiplied by a speed value to make our ship move at a controlled rate.
The final part of this process is using the move_and_slide method on the kinematic body, which allows us to move the player sprite whilst paying attention to friction and the physics of other objects it may collide with.
Drag and camera
Now our ship moves but never decelerates, which is undesirable even when simulating a zero G environment. There is one neat trick we can do to apply smooth drag to both rotation and thrust, all we need to do is say if a button isn’t pressed multiply the velocity or rotation by a drag factor between 0.0 and 0.99. As this will apply every frame the value will be eased smoothly to 0.
# handle rotation if Input.is_action_pressed("ui_left"): rotate_left() elif Input.is_action_pressed("ui_right"): rotate_right() else: spin *= drag # handle thrust if Input.is_action_pressed("ui_up"): thrust() else: v *= drag
This last step won’t take a moment, we just need to add a camera and we are done. Thankfully Godot makes this incredibly easy.
We just add a camera2D to our player scene with a couple of parameters. Current is set to true so we use this camera by default, zoom is set to 0.75 as it felt a little far out and finally we set the limits to -500 and 500. Those are the extents of our 1000 pixel star field background, so now the camera cannot leave our lovely backdrop.
That’s it! This was a longer post than I expected but it was a nice opportunity to neatly lay out some game development. I do hope this was of interest or help and stay tuned next time for shooting and some primitive asteroids.
extends Node2D var spin = 0 var v = 0.0 var acc = 0.1 var velocity = Vector2(0, 0) var speed = 200 var max_v = 2.0 var r_acc = 0.01 var max_r = 0.15 var drag = 0.9 # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta): # handle rotation if Input.is_action_pressed("ui_left"): rotate_left() elif Input.is_action_pressed("ui_right"): rotate_right() else: spin *= drag $player.rotate(spin) # handle thrust if Input.is_action_pressed("ui_up"): thrust() else: v *= drag velocity = Vector2(v, 0).rotated($player.rotation) * speed velocity = $player.move_and_slide(velocity) # handle shooting if Input.is_action_just_pressed("ui_accept"): fire_weapon() func thrust(): if v < max_v: v += acc func rotate_left(): if abs(spin) < max_r: spin -= r_acc func rotate_right(): if abs(spin) < max_r: spin += r_acc func fire_weapon(): pass