Chris. If you're reading this, it's because you went and forgot about how you did a thing. I'd be inclined to call you a forgetful lummox, but the only reason I'm writing this is because I know I'll forget.
You decided to build a game or two in Godot. The game engine and development environment. Of all the languages you could have picked (like C# or NativeScript) you picked the Python-like GDScript.
It's not like you've built games before, apart from writing about making one in JavaScript. You're also not that experienced in Python. Aside from that framework stuff.
You'll be fine.
You did manage to crank that one game out, about the oink who couldn't get enough yums. But, that was mostly because of the 1-bit Godot course you were doing at the time. Wow, that HeartBeast guy can teach!
I remember it because it was yesterday. You were watching HeartBeast's YouTube playlist, about making an RPG in Godot; and you thought about making your own.
The first two videos were on par with the 1-bit Godot course videos; but the first really interesting deviation was how the move_and_slide code changed to something more succinct and clear:
extends KinematicBody2D
var velocity = Vector2.ZERO
export(int) var max_speed = 100
export(int) var acceleration = 500
export(int) var friction = 500
func _physics_process(delta):
var input_vector = Vector2.ZERO
input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
input_vector = input_vector.normalized()
if input_vector != Vector2.ZERO:
velocity = velocity.move_toward(input_vector * max_speed, acceleration * delta)
else:
velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
velocity = move_and_slide(velocity)
This was from player.gd
Ok, so the normalized call was welcome news to me, since it prevents diagonal movement from being faster than the max in any one direction. Imagine Quake 3 Arena and Counterstrike 1.6 had normalised their movement. A whole skillset wouldn't have existed. The interesting bit, though, was using move_toward to make the player faster or slower. It's great, because you don't need convoluted logic or clamping. Just "how much faster until you get to max speed" and "how much slower till you stop".
Then you had a thought. What if you could rotate the world, similar to how Fez does it. Well not entirely similar. More like how Cities Skylines does it. Only without the actual 3D. Ok, let's try phrasing that better, so you can get it into your thick lummox skull: "what if we could make a 2D isometric RPG in which the camera can rotate?" You'd be able to make something like Stardew Valley, except with the house/barn repair mechanics of something like Farmer's Dynasty. Let's just...um...skip the voice acting. Anyway, that got you on to this lovely example...
Not bad, but it messes with the everything! Oh, wait, I'm getting ahead of myself. Let me just quickly remind you about sorting.
I think it was the fourth video in the playlist, where HeartBeast talks about ysort. That's some great functionality. TL;DR is that nodes lower on the Y axis will be drawn above nodes higher on the Y axis.
This will let your player sprite go in front and behind the same node, as long as there's a Collision2D node to make a nice distinction between the space in front and behind, where the player can be...
Still, adding the rotation messed that up.
For starters, rotating the level or the ysort allowed the player to see a different side of things, but the movement controls are all messed up. And, now ysort is actually xsort. 🤦♂️
Ok, a bit of YouTubery and you I discovered that it's possible to nest ysorts. This means I can have the "player" and the "level" the right way up (and then the movement controls work fine) but I can also rotate a nested ysort that contains all the obstacles.
Sorta like this...
Thing is, nested ysort nodes share the same "sorting space" as the outer ysort node. That means, even though the obstacles are rotated 90°; they are still sorted as though they're in the outer ysort.
Quick trip to the "level" node, to add the rotation code:
extends Node2D
onready var obstacles = $"global-ysort/obstacle-ysort"
func _ready():
state.orientation = "south"
func _process(_delta):
if Input.is_action_just_pressed("ui_rotate_left"):
match state.orientation:
"south":
state.orientation = "west"
obstacles.rotation_degrees = 90.0
"west":
state.orientation = "north"
obstacles.rotation_degrees = 180.0
"north":
state.orientation = "east"
obstacles.rotation_degrees = 270.0
"east":
state.orientation = "south"
obstacles.rotation_degrees = 0.0
if Input.is_action_just_pressed("ui_rotate_right"):
match state.orientation:
"south":
state.orientation = "east"
obstacles.rotation_degrees = 270.0
"east":
state.orientation = "north"
obstacles.rotation_degrees = 180.0
"north":
state.orientation = "west"
obstacles.rotation_degrees = 90.0
"west":
state.orientation = "south"
obstacles.rotation_degrees = 0.0
This was from level.gd
I thought, since it's going to be useful to know which way the "world" is facing; that I should store that sort of information in a global "state" object:
extends Node
var orientation setget set_orientation,get_orientation
func set_orientation(value):
orientation = value
func get_orientation():
return orientation
This was from state.gd
I hadn't used the match syntax before, but it seems to fit quite nicely in this example.
My first attempt at rotating sprites was an exercise in frustration. Turns out it's not so easy to rotate 2D pixel art. Especially for someone so...bad at it. I ended up removing the full rotation animation and doing it in 90° intervals instead.
This meant I would only need four static angles of any vertical object.
It was also around this time I changed the theme of the game I wanted to make. The new theme isn't important, since it still needs the same mechanics as the old one. Maybe you'll change your mind, again, so I'll keep it under wraps for now. This required some sprite work, to know which collider to use and which "face" to show:
extends Node2D
func _physics_process(_delta):
$north/collider.disabled = true
$north.visible = false
$south/collider.disabled = true
$south.visible = false
$east/collider.disabled = true
$east.visible = false
$west/collider.disabled = true
$west.visible = false
match state.orientation:
"north":
$north/collider.disabled = false
$north.visible = true
"south":
$south/collider.disabled = false
$south.visible = true
"east":
$east/collider.disabled = false
$east.visible = true
"west":
$west/collider.disabled = false
$west.visible = true
This was from obstacle.gd
There's probably an easier way I could have done this, but it does the trick. Show me you can do better!
The "obstacle" base scene looks like this:
Those warnings are because I wanted the subclasses to specify what their collision shapes and sprites are supposed to be. Looks funny when they're all rotated appropriately...
That's almost everything I needed to get this example to work:
Oh, yeah, I also added animations and...
Supper time! The rest will have to wait for next time.
I write about all sorts of interesting code things, and I'd love to share them with you. I will only send you updates from the blog, and will not share your email address with anyone.