Godot learning

I did these between 2019-07-28 and 2019-088-17. I never made that game.

Part 1

I'm learning how to use the Godot game engine and so I'm going to start keeping my notes on my blog so I can remember them, and hopefully gather resources that may help you.

My goal is to rebuild an ancient RPG I made called Final Existence on the Amiga in AMOS Basic, which was a YA alternate universe apocalyptic game that looked and felt a lot like Chrono Trigger. I've become a bigger fan of ARPGs lately, and I'll rebuild it using that paradigm.

There are a crap ton of tutorials out there, but I've been having trouble gathering all the pieces I want together to build an ARPG, and there's a lot of moving parts in Godot, so this is my attempt to wrangle all that together in one place for myself.

So, on with the notes:

What do I (think I) need next?

  • TileMap/Area2D collisions for making the character walk around and get stopped by things, and know what I ran into.
  • A map that's bigger than the screen that follows the character around.

What have I found?

  • Started with Your First Game to get a handle on editor in general
    • GDScript is very Python-y, which is not my strongest language, but I'm getting the hang of it
    • I wish Godot had vim keyboard shortcut support
  • Learned TileMap and TileSet because I plan on using tiles to make maps
    • Still need to figure out how to get things to run into tiles
  • Walked through Kinematic Character 2D
    • Using KinematicBody2D on KidsCanCode
    • Still think I'm missing something with this:
      • Can't quite get the character to walk/move as expected
      • Also this is more platformer-y than I need right now
      • Looks like gettext
    • Switch scenes via “NPC” with a Screen Change custom event
      • export var to select target scene
      • export var for warp target in other scene
      • Remove old scene from DOM
        • get_tree().root.get_child()/.add_child()
        • Scene#queue_free to safely remove from memory!
      • Add new scene to DOM
      • Find player in new scene
      • Move to position of warp target
        • Put nodes into a group called “exit” to focus the search
    • Dialog box
      • Move it out of the way if the character is there
        • The scene will need a camera
        • Find the Camera2D
          • Only one camera in a scene, pick one using Camera2D#make_current()
          • Put the camera in the thing that needs to be tracked
          • Drag margins require tracked object to hit edges before moving
          • Make the camera current, or nothing will move
        • Clamp the camera scroll to the size of screen with Camera2D#limit_??
          • Grab the dimensions of something that's as big as the scene
            • Can you get scene/child node offset size
    • Player move from scene to scene
      • Primarily to transfer player state
        • Video shows physically moving player node, since Player object contains state, but I'm thinking to have a Vuex/Redux like store external to visual scenes where such data can be stored and reconstituted
        • Moving nodes from Scene to Scene seems error-prone, more reason for external state
      • Like Ruby meets C?
    • Using separate TileMaps
      • One for walkable areas, one for collisions, one for stuff outside the play area
      • Emit a signal on transport to recalculate camera clamping

Solution to "RPG Map with tiles and doors"

  • Node2D Main scene with KinematicBody2D Player and Node2D MapContainer
    • MapContainer contains the first Map
    • Player handles own position, using move_and_collide to detect collisions, and move_and_slide to complete moves after initial collisions.
    • Player gets collision Node and determines if it is a Door
      • Door is a subclass of KinematicBody2D with the exported properties DestinationScenePath : String and ExitID : int.
      • On collision, Player emits a signal with DestinationScenePath and ExitID
    • Main listens for collision signal from Player and, once received, manually swaps out the old map in MapContainer with the loaded DestinationScenePath
      • …then searches the new scene for nodes in the exit group and finds a node in that group with an ExitID property that matches the one received in the signal. If found, move Player to that Node2D#position.
  • Map and Map2 have nodes in this order:
    • TileMap Walkable
    • Any Doors with appropriately set DestinationScenePath and ExitID
    • TileMap Walls with tiles with Collision enabled
    • ExitNodes, each in the exit group and with an ExitID that matches the Door from the other scene.

Part 2

Lunchtime video: Platform Game Tutorial

  • Grid setup & snapping in top/dot menu
  • Remember to use KinematicBody2D#_physics_process to sync code with physics engine!
  • The Vector2D motion object:
    • Having a drag on horizontal motion & capping horiz motion to have inertia?
  • StaticBody2D doesn't move but it interacts with physics
  • CanvasItem#Modulate can hack in a color on a thing.
  • Ensure you move extents of Collision shapes and leave the Transform alone.
  • is_on_floor() and move_and_slide(velocity, floor_vector)
    • Vector2(0, -1) is an up vector, which makes floors work.
  • velocity = move_and_slide(velocity) to set it to “remaining motion”
    • If you've hit the ground, there's no more remaining velocity, so you don't have to stop adding gravity yourself.

Game planning

I'm now thinking the game will be a walking simulator with QTE. No items, no stats, no tight action stuff, just dialog choices, exploration, and QTE. Should be pretty straightforward.

Solution to "Pixel size of Scene for Camera2D clamping"

This doesn't take into account transforms on the Map or the MapContainer.

python func get_map_max_size(): var maxSize = Rect2(0,0,0,0) for node in $MapContainer/Map.get_children(): # Object#is_class is like Ruby Object#is_a?. # if I used instances in the tree whose classes subclassed TileMap, # is_class will return true for those instances. if node.is_class("TileMap"): var mapSize = Transform2D( Vector2(node.cell_size.x, 0), Vector2(0, node.cell_size.y), Vector2() ) * node.get_used_rect().size maxSize = maxSize.expand(Vector2(mapSize.x, mapSize.y)) return maxSize

Part 3

Lunchtime Video: Branching Dialogue and Dynamic Events

  • Use Nodes to group objects rather than using top level Node type (KinematicBody2D for Player)
  • Player canMove and canInteract
    • canMove = false in dialogue boxes
    • canInteract != false if colliding with Area2D
      • Item contains Area2D for interaction, and StaticBody2D for collision
      • On load map _ready, find all in group Interactible and connect to Player body_enter and body_exit
        • Would need more refinement if 2+ interactible objects entered
          • Facing? Distance to center of all objects?
    • _input event for capturing input
    • Array Player#inventory
  • Picking a data structure for:
    • Event tracking
    • Dialogue
    • Other game state
  • Object/Array traversal is JavaScript-like
  • Dialogue/routing/game state seems finicky enough that a testing framework would be worth using
  • Build the dialog box via instancing new Classes and adding to a container

Solution to "Clamp camera to max area of Map Scene"

  • Emit a signal on start/map change of calculated max scene area
  • Player picks up signal and resets its Camera2D limit_? properties from the provided Rect2.

Solution to "Fade to black when changing rooms"

Part 4

Dialogue Boxes

  • Separate node for running the logic of the dialogue box
    • Like Vue data/computed/methods separated from template, but more manual lifting
    • Player is a state machine based on input dialogue data
    • DB object to get assets
      • Can iterate over a res:// directory with Directory instance and cursor get_next()
      • Can create text Resources as an alternative data storage vs. JSON/YAML
    • If we need JSON for native, switching out loaders should be easy
  • GUI containers:
    • Create a CanvasLayer overtop of everything where GUIs live, otherwise they'll be affected by the viewport/Camera.
    • Docs say that Containers and their children reflow automatically, but I found I had to reset settings a lot in child containers after adjusting parent containers to get them “right”.
      • Getting a ColorRect to take up the whole container's background is a challenge.
    • MarginContainer Custom Constants is where you're setting the interior margins of the box! This is the setting you want to make a MarginContainer do what you expect it to do!
    • Use (H|V)BoxContainers with the Size Flag set to Expand to create spacers.

Solving "Move RPG dialogue box to other end of screen to not hide player"

  • Put the DialogueBox inside a VBoxContainer, I call it Aligner.
  • Player is above the centerline of the viewport if global_position.y < $Camera.get_camera_screen_center().y.
  • Set that on a property of Player during _process, I use atTopOfScreen.
  • Use the VBoxContainer's alignment and margin_bottom to position the DialogBox:

    python if $Player.atTopOfScreen: $GUI/Aligner.alignment = VBoxContainer.ALIGN_END $GUI/Aligner.margin_bottom = -100 # height of container else: $GUI/Aligner.alignment = VBoxContainer.ALIGN_BEGIN $GUI/Aligner.margin_bottom = 0

Part 5

Dialogue Boxes Part 2

  • Use a combination of Input.is_action_just_released and !Input.is_action_pressed with ui_accept to find out if interaction should continue. Rough pseudocode logic:

    python var stopInteracting = false # only continue if everything is ok if isInteracting and isAllowedToContinueInteracting and Input.is_action_just_released("ui_accept"): emit_signal("continue_interaction", "next") isAllowedToContinueInteracting = false # have to release the key to continue if !Input.is_action_pressed("ui_accept"): isAllowedToContinueInteracting = true if canMove && !isInteracting: if interactTarget && Input.is_action_just_released("ui_accept"): isInteracting = true emit_signal("start_interaction", self) isAllowedToContinueInteracting = false if stopInteracting: isInteracting = false stopInteracting = false

    This gets a little weird with a menu system, though, as I want the DialogBox to control input. I may move Player and DialogBox Input handling up to Main and send it down to the appropriate objects based on game state.

  • setget doesn't fire setters and getters if you are setting or getting the value in the same script, so this does not fire the setter:

    python # CoolCat.gd var cat setget set_cat func set_cat(newCat): cat = newCat do_cat_things() func _process(): if somethingHappens: cat = "meow" # do_cat_things is not called.

Part 6

Odds and Ends

  • If you want to make a factory class method, you can't use new in a static method:

    python class_name Kitten static func forCharacter(char): var kitten = new() kitten.setupCharacter(char) return kitten

    Instead, load the file from res:// and run new from that. This is to get around multithreading issues it seems:

    python class_name Kitten static func forCharacter(char): var kitten = load('res://classes/Kittens.gd').new() kitten.setupCharacter(char) return kitten

  • I hate making up/down/left/right if blocks for movement:

    python const MOVES = [ ["ui_down", "y", WALK_SPEED], ["ui_up", "y", -WALK_SPEED], ["ui_left", "x", -WALK_SPEED], ["ui_right", "x", +WALK_SPEED], ] func _process(delta): var vector = Vector2(0,0) for move in MOVES: if Input.is_action_pressed(move[0]): vector[move[1]] += move[2]

Making Pixel Art

Krita is my art tool of choice anyway, so let's see what the Internets have to say about using it for stills and animations:

    • The newest video I could find
    • Modern Krita comes with a Pixel Art brush to align the pointer with the grid
    • Fill Bucket
      • Threshold 1, Grow Selection 0, Feathering 0
    • Disable Antialiasing on selecton tools
    • Subwindows for new views
    • Transform
      • Nearest Neighbor
    • Use wraparound (w) for tiling backgrounds
    • Use Alpha Lock to make shading easier
    • Emulate antialiasing to make transitions between colors smoother
    • I need to get better about mirroring the canvas to check symmetry…

Part 7

YSort and 2d RPG games

I made a pixel art in Krita:

Then I put him in my game:

Then I walked behind him:

Oops.

My structure of the game looks like this, because I thought having Player outside of Map and not repeated in all the maps would make things easier:

  • Main
    • Map
      • Rabbit
    • Player

But it should look like this, and I'll have to do more management of Player within maps when maps change:

  • Main
    • Map

My idea of centralizing all player input to Main and pushing down movement events to Player may be the direction I go for restructuring this…stay tuned.

Part 8

YSort!

Success! I got my player to walk behind the rabbit by placing both in a YSort:

But I have to make sure everything map-related goes into the YSort. My Exit Nodes were outside the YSort, so when I warped back into the larger first Map, I ended up in a weird spot. Also, moving all of the input logic to Main and pushing down events to Map and such is the way to go.

Next is making my TileMaps be the kind where the player can walk “behind” them.

Android Export

I got the game to export to Android, as I already had the Android environment installed for React Native and NativeScript-Vue compilation:

I hooked up a USB controller via an OTG cable and got the game to react to inputs, but, as expected, the YAML data didn't work, so I'll need to hook up a preflight script to covert YAML to JSON and use the JSON loader on Android, or just use JSON everywhere and run a watcher to convert YAML on-the-fly while I work.