Space Ships Are Cool
Following the last POC on capturing inputs, I wanted to get my head around movement. The movement implementation in the Getting Started project was OK, but I still didn’t really understand how it worked (vector math!). I decided to do a spaceship instead.
My concept proof terms were:
- Left and Right will rotate the ship
- Up and Down will accelerate forward and backward respectively
- In space your velocity remains constant unless acted upon
- The player should teleport to the opposite edge of the screen if they hit the edge of the play area
You can find the complete source code on GitHub.
Comparisons
It’s worth noting that my implementation is not the same as the original Asteroids. In the original, your rotation happened at a fixed speed, and stopped immediately when you released the control.
Mine is much more awkward to control, but better reflects what actually happens in space (if space was a 2d plane of course). As you continue to apply thrust, you accellerate faster and faster, and you won’t stop unless you counteract the thrust in an opposing direction.
I also did not cap maximum speed at all. I wanted to see what would happen at really extreme velocity values 🚀. You can in fact continue to accelerate until your velocity overflows into negative (or vice versa) territory, causing your directional velocity to reverse instantly. I am advised that this is not good for the astronaut inside.
I also found that the sprite starts to glitch out after you do this, and some of the controls stop working - do try it out 😄
The “Asteroids” style movement example in the Godot movement documentation is different again, the main one being that the sprite’s forward speed moves with it’s rotation, giving more of an aeroplane or glider feel in my opinion. I specifically wanted to apply directional and rotational thrust separately.
The Code
I had the most trouble with stopping the forward speed from following the y transform of the sprite (forwards) - there is some vector math involved in understanding how this works, however the simple version is that the examples I was referring to were all applying a total velocity figure in the forward direction every physics cycle. To get the effect I wanted, I needed to figure out how to preserve the existing velocity regardless of where the ship was pointed.
Thrust
I needed to preserve the intertia of the ship. The solution was to add the incremental velocity in the direction of thrust (forward or backward). This way you accellerate if you stay pointed in one direction, but also if thrust is released you can freely rotate without changing your directional movement.
Here is the main physics process function:
func _physics_process(delta: float) -> void:
set_directional_speed()
check_screen_wrap()
rotation += _yaw_speed * delta
velocity += transform.y * _forward_speed * delta
move_and_slide()
I was originally accellerating by adding to a speed figure and using that in the velocity calculation, but doing this would prevent me from incrementally changing the velocity.
“Speed” in the set_directional_speed() function here would better be referred to as thrust. The amount of speed that is added each cycle is constant (the power of the thrusters). This thrust gets added to the existing velocity property value in the direction of the y transform, causing it to continue to increase while thrust is applied in the same direction. When it is released, it is set to zero, which causes velocity to stay the same.
Rotation
I did the exact same for rotation as for thrust, which is more realistic, but actually is quite awkward to control in practice.
The function that sets the directional thrust works the same for both forward and rotational thrust, a static value while engaged and resets to zero when released:
func set_directional_speed():
if Input.is_action_pressed("input_accelerate"):
_forward_speed += -3.0
if Input.is_action_pressed("input_decelerate"):
_forward_speed += 3.0
if Input.is_action_pressed("input_left"):
_yaw_speed += -0.1
if Input.is_action_pressed("input_right"):
_yaw_speed += 0.1
if Input.is_action_just_released("input_accelerate") or Input.is_action_just_released("input_decelerate"):
_forward_speed = 0
Screen Wrap
Finally, I wanted the screen to wrap because I knew that this POC would be awkward to control. It would not be much good if you were off the screen within a few seconds. I initially was trying to figure out how to implement this using the same PathFollow2D used to determine where on the screen edge mobs spawn in the tutorial project. Whilst there is probably a way to make that work, after struggling with it for a while I succumbed and googled it… it turned out I was on completely the wrong track! If all you have is a hammer I suppose…
I found what I was after on Kids Can Code. Every day is a school day 😆
After my ego recovered from finding the solution to my problem on a site that teaches children, I noted that the site is actually a very good resource, and is not at all only for kids. Definitely worth a bookmark if you are learning.
It goes to show that sometimes you just don’t know what you don’t know. I implemented this pretty much exactly as per the prescribed recipe, much simpler and more direct than what I was attempting:
func _ready() -> void:
motion_mode = MotionMode.MOTION_MODE_FLOATING
_screen_size = get_viewport_rect().size
...
func check_screen_wrap():
if position.x > _screen_size.x:
position.x = 0
if position.x < 0:
position.x = _screen_size.x
if position.y > _screen_size.y:
position.y = 0
if position.y < 0:
position.y = _screen_size.y
Asteroiding Out
In working through this I found fun in the ridiculous nature of continuous acceleration and having the ship completely spiral out of control. The overflow of velocity values and resulting jank also gave me some good context on some things to avoid and the types of bugs that can happen.
I definitely found some design inspiration from this excercise, and my next step will likely be a rough design of a game that leverages some of the aspects I played with here. Watch this space.