Awesome
Learning Godot (game engine)
Just one of the things I'm learning. https://github.com/hchiam/learning
Godot in 100 Seconds by Fireship.io
https://github.com/godotengine/godot
templates: https://github.com/godotengine/godot-demo-projects (and their demos)
intro tutorials:
- general intro
- 2D intro tutorial + completed 2D tutorial code
- 3D intro tutorial + completed 3D tutorial code
best practices manual:
- physics
- multi-player networking:
- inputs
- 3D reference
- scripting:
- GDScript
- For syntax notes on things like
&"
and^"
, Ctrl+F in this page: GDScript basics, including syntax notes$Player.position
and$UserInterface/Retry.show()
- For syntax notes on things like
- GDScript
- exporting
- you'll need to have downloaded "Export templates"
- from within Godot, Export Template Manager > Download and Install, or
- from links lower down in this page https://godotengine.org/download and then Install Export Templates so that you can use them to export (without red error messages preventing Export Project)
- helpful Godot 4 export to itch.io tutorial: https://www.youtube.com/watch?v=JPL4-rXZNW8&t=131s
- my personal example: https://hchiam.itch.io/catch-the-chicken-game
- you'll need to have downloaded "Export templates"
- to check if on mobile:
if OS.has_feature("web_android") or OS.has_feature("web_ios"):
animation tips from DevWorm: https://youtu.be/XbDh2GAshBA?feature=shared
some helpful Godot plugins: https://youtu.be/bKNmsae5zXk?feature=shared
multiplayer godot repo from Battery Acid Dev
miscellaneous notes
For Ctrl+F convenience to remind myself of things:
- in my mind, there's only really 2 core concepts in godot: nodes and signals:
- scenes are the same thing as nodes, just isolated so you can run a sub-tree in isolation. everything's nested in trees of nodes. simpler than thinking in MVC (Model-View-Controller).
- signals enable "notification" callbacks (observer pattern). a node sends out a signal, and you can attach nodes to listen to that signal.
- keyboard shortcuts:
- option + spacebar = search help documentation
- (random tip: you can hover over a lot of things in the panels to get more info about them, like even titles/dropdowns/buttons/labels/fields)
- cmd + b = run from main
- cmd + r = run current scene
- cmd + . = stop running
- cmd + shift + A
- = "import" an instance of an other scene as a child node, which packages/hides that other scene's own children from view of the current scene
- you have to save the original instance for the "imported" instances to update to match
- you can customize one of those instances, which overrides the original instance's properties
- you can show an instance's children with: right-click > checkmark Editable Children
- you can customize one of those instances' PhysicsMaterial (a resource) in the Inspector panel with: down arrow dropdown > Make Unique > props unlocked!
- cmd + d = duplicate
- cmd + c --> cmd + v = create child
- you can click in the animation panel and then control animations:
- d = play
- s = stop
- s double-tap = start
- cmd + shift + d = toggle collapse/expand side panels
- option + o = toggle collapse/expand Output panel
- option + d = toggle collapse/expand Debugger panel
- option + spacebar = search help documentation
- you can mix scripting languages as needed (e.g. use C# only to implement complex algorithms with better performance)
- to add an image as a texture to a Sprite2D: Inspector > Texture > click to show dropdown > Load...
- to add a script to a node: Scene > right-click > Attach Script...
- to connect a signal:
- through the editor: select a node in the left-side panel that's within the currently-displayed scene tree > Node panel on the right > double-click a signal for that node > select a node that has a script (a receiver method will be auto-named) > Connect
- (if you want a different node from the node that's sending a signal, you need both displayed in the left-side scene tree)
- (but if you want to send a signal to a parent, i.e. parent instantiates the node that emits a signal, you should connect the signal with code)
- make mob fire signal as usual:
signal caught
andfunc _on_area_2d_area_entered(area): caught.emit()
- make node that instantiates mobs listen to that mob instance's emitted signal:
var mob = mob_scene.instantiate(); mob.connect("caught", Callable(self, "_on_mob_caught")); func _on_mob_caught(): # update score
- make mob fire signal as usual:
- the listener/target node will have the callback written in its script
-
func _on_button_pressed(): # you'll see a "->]" icon on the left side of this func set_process(not is_processing()) # toggle whether _process(delta) is running
- (note: the listener/target node can also be itself, e.g. Area2D
body_entered(body:Node2D)
signal-->]
func _on_body_entered(_body)
in the script file of same node)
- through code (needed when creating nodes inside of a script):
-
func _ready(): var timer: Timer = get_node('Timer') # 'Timer' must be already set up as a child node named 'Timer' # on the timer's timeout event, call _on_timer_timeout() timer.timeout.connect(_on_timer_timeout) func _on_timer_timeout(): visible = not visible
-
- through the editor: select a node in the left-side panel that's within the currently-displayed scene tree > Node panel on the right > double-click a signal for that node > select a node that has a script (a receiver method will be auto-named) > Connect
- to add a custom signal
-
signal health_depleted # ... health_depleted.emit()
-
signal health_changed(old_value, new_value) # these params show up in Node panel # ... health_changed.emit(old_health, health) # technically you can pass more params but it's up to you to be consistent in code
-
- a
CanvasLayer
's Inspector panel has a Layer property for a layer number that behaves likez-index
in CSS- (very different thing from a collision layer)
- use a
ColorRect
for a solid colour background - use a
TextureRect
for an image background - use an
AudioStreamPlayer
for music/sound: Inspector > AudioStreamPlayer > Stream > (you can expand to show more settings, like Loop)- to make music loop, you likely can't use .wav, and instead use .ogg in Inspector > AudioStreamPlayer > Parameters > Looping
- a
Label
's font can be set in: Inspector > Control > Theme Overrides > Fonts - a
Label
's size can be set in: Inspector > Control > Layout > Transform > Size - to see predefined actions or create custom-named action bindings that map to multiple input devices: Project > Project settings > Input Map
- set a
Button
's keyboard shortcut with: Inspector > BaseButton > Shortcut > set Shortcut = Shortcut > expand to show Events > Add Element > expand an element > Action = (what was set in Project > Project settings > Input Map > ...choose a name of an Action, which can be triggered by multiple different keys/etc.) - 2D: units = pixels; 3D: units = meters.
- you can't really precisely size an
Area
orRigidBody
orSprite
,- but you can scale an
AnimatedSprite2D
in: Inspector > Node2D > Transform > Scale - after you set a respective
CollisionShape2D
's size in: Inspector > CollisionShape2D > set dimensions after you choose a Shape
- but you can scale an
- if you need very low wait times, consider a process loop in a script instead of a
Timer
- if you want to be able to delete all mobs, including those from previous rounds:
- click on a node > Node panel > Groups > Add a group named "mobs" (this will let the code differentiate mob from ground collisions)
- add in main script:
get_tree().call_group("mobs", "queue_free")
to callqueue_free
on all nodes that are part of group"mobs"
to delete themselves
CharacterBody3D
=Area
orRigidBody
, but 3D and controlled by player instead of by physics engine- consider adding child
Node3D
as pivot namedPivot
to isolate the animations on this node's childCharacter
from overriding code-set values on thePivot
's properties, letting you layer motion/rotations/offset/etc. on top of the childCharacter
's animation- consider adding child
Node3D
as character 3D model namedCharacter
, e.g. a .glb file, which can be exported from Blender by exporting to GLTF
- consider adding child
- consider adding child
CollisionShape3D
namedCollisionShape
to collide with environment - consider adding child
Area3D
namedMobDetector
to detect collisions with other characters, but with "Monitorable" prop unchecked so other nodes can't detect it- consider adding several children
CollisionShape3D
s to theMobDetector
Area3D
- consider adding several children
- consider adding child
AnimationPlayer
- consider adding child
CharacterBody3D
has a nativemove_and_slide()
function you can call at the end of yourfunc _physics_process(delta):
to smooth out motionCharacterBody3D
has a nativelook_at_from_position(start_position, position_of_target_to_face_towards, Vector3.UP)
function (3rd param = which way is up axis to rotate around)- useful for 2D as well:
look_at(player.position)
(you might need to@export var player: PackedScene
at the top of your code)
- useful for 2D as well:
CharacterBody3D
has a nativerotate_y(randf_range(-PI / 4, PI / 4))
function that you can call afterlook_at_from_position
to further adjust rotation.CharacterBody3D
has a nativeis_on_floor()
function that usesup_direction
andfloor_max_angle
to determine whether a surface is a "floor".- rotate the player with
basis = Basis.looking_at(direction)
- collision layers:
- layer = the "group"/layer the node is in; mask = the "group(s)"/layer(s) the node can collide with/listen to.
- you can add custom names to collision layers: Project > Project Settings... > General > Layer Names > 3D Physics.
- the layers still display as numbers, but the tooltips will show the custom names.
- but you can also see checkboxes next to the custom names <- if you click on the 3 dots next to the layers number boxes.
- collision note:
move_and_slide()
makes the node move sometimes multiple times in a row to smooth out the motion. so loop over all collisions inget_slide_collision_count()
to check (break
the loop if needed). VisibleOnScreenNotifier3D
child node can tell its parent when it's off-screen (it has its own box), e.g. to despawn an off-screen mob withqueue_free()
on the parent to save on memory when the childVisibleOnScreenNotifier3D
fires ascreen_exited()
signal- you can set the viewport size with: Project > Project Settings... > General > Display > Window > Size > Viewport Width and Viewport Height
- unlike 2D, Y-axis positive is up in 3D. Y-axis positive is down in 2D.
- unlike 2D, need a camera to be able to see things in a 3D scene:
Marker3D
as camera pivot (in the center viewport, you can see scene and preview with: View > 2 Viewports (Alt))Camera3D
as camera view (in the center viewport, you can see a Preview checkbox under Perspective, if theCamera3D
is selected)
Camera3D
'sSize
property in Inspector lets you see wider- orthogonal camera's
Far
value affects directional shadow quality:- big
Far
: see farther, but worse shadows because rendering over bigger distance (shadows may be blurry) - small
Far
, e.g100
: better shadow quality, but can't see farther-away objects
- big
- for how to place points on a
Path3D
, see the instructions+images at https://docs.godotengine.org/en/stable/getting_started/first_3d_game/05.spawning_mobs.html- spawn points setup:
PathFollow3D
node as child ofPath3D
node;Path
= path,PathFollow
= to select locations on that path
- spawn points setup:
- GUI? add a
Control
! as well as the other things nested under it in the search for the "Create New Node" window- e.g.
Label
, which has a defaulttext
property:text = "Score: %s" % score
ortext = "Score: %s" % [score,]
- (note: in a more complex game, you might want to store data like score in a dedicated object instead of in a
Label.text
)
- (note: in a more complex game, you might want to store data like score in a dedicated object instead of in a
- the
Control
's Theme panel will be accessible in a bottom tab - the
Control
's children will inherit its theme, e.g. font (Inspector > Control > Theme > Default Font > click to expand details > Resource > Path > choose a font file).- to make a child
Label
orColorRect
position relative to or fill its parentControl
and make that parent in turn fill the viewport too:- select the
ColorRect
> click the green smash-bros-like icon for Anchor preset in the top bar > Full Rect (anchor value is relative to its parent). - select the
Control
> Inspector > Control > Layout > Anchors preset > Full Rect (anchor value is relative to its parent).
- select the
- to make a child
- e.g.
- nodes (like
mob
) created in code, need code to connect their signals:- e.g.: in Godot 4:
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed)
in Main.gd, where:signal squashed
in Mob.gdfunc _on_Mob_squashed():
in Main/UserInterface/ScoreLabel.gd
- e.g.: in Godot 3:
mob.connect("squashed", Callable($UserInterface/ScoreLabel._on_Mob_squashed, "_on_Mob_squashed"))
- e.g.: in Godot 4:
- to have music autoload and automatically restart the music on game start:
- attach music to the root viewport node (NOT under the Main node!): new scene > add
AudioStreamPlayer
> Inspector > AudioStreamPlayer > Stream > click to expand > Resource > select an audio file and make sure to checkmark Autoplay! you can also see if it loops in the expanded Stream menu items.- autoload music: Project > Project Settings... > Autoload > Add (Path = scene node, e.g. AudioStreamPlayer.tscn)
- will restart with game restart triggered by code:
get_tree().reload_current_scene() # get the SceneTree
- will restart with game restart triggered by code:
- autoload music: Project > Project Settings... > Autoload > Add (Path = scene node, e.g. AudioStreamPlayer.tscn)
- attach music to the root viewport node (NOT under the Main node!): new scene > add
- animation Autoplay on Load: the button turns blue when on, and looks like an A+ inside an arrow.
- stylistic tip for animations: in general, don't time and space everything evenly = offset and contrast give a certain feeling.
- you can copy an animation if copying to a similar structure of nodes:
- select an
AnimationPlayer
node > select an animation > click on "Animation" > Manage Animations... > click on the copy icon (looks ike 2 pieces of paper stacked) > OK > select a similar node > select itsAnimationPlayer
node > click on "Animation" > Manage Animations... > click on the paste icon (looks like a clipboard)
- select an
- .gitignore the .godot folder - the editor auto-generates the .godot folder
more example GDScripts:
extends Sprite2D
# instance member variables:
var speed = 400
var angular_speed = PI # godot defaults rad angles
# Called when the node enters the scene tree for the first time.
func _ready():
print('Hello World!')
# Called every frame. 'delta' is the elapsedf time since the previous frame.
func _process(delta): # use _physics_process for more consistent/smoother physics timing
var change = angular_speed * delta
rotation += change # rotation is a built-in property of Sprite2D
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta # rotation is a built-in property of Sprite2D
# note: you can set a constant rotation on a RigidBody2D in the Inspector panel with: Angular > Velocity
use func _unhandled_input(event):
to handle any input:
func _unhandled_input(event):
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
get_tree().reload_current_scene() # restart SceneTree = restart game
# get_tree() gets the global singleton SceneTree, which is what holds the root viewport that in turn holds all root scenes/nodes
use func _process(delta):
/func _physics_process(delta):
with things like Input.is_action_pressed("ui_left")
:
extends Sprite2D
var speed = 400
func _process(delta): # use _physics_process for more consistent/smoother physics timing
var direction = 0
if Input.is_action_pressed("ui_left"): # arrows on keyboard or D-pad
direction = -1
if Input.is_action_pressed("ui_right"):
direction = 1
if Input.is_action_pressed("ui_up"): # not elif, so you can move diagonally
pass
if Input.is_action_pressed("ui_down"):
pass
position += Vector2.RIGHT * direction * speed * delta
@export var speed = 400
lets you show thespeed
variable in the Inspector- NOTE: changing the value in the Inspector overrides this value in the script!
var speed = 400
var screen_size
func _ready():
screen_size = get_viewport_rect().size
func _process(delta): # use _physics_process for more consistent/smoother physics timing
var velocity = Vector2.ZERO
if Input.is_action_pressed(&"ui_right"): # use a StringName &"..." for faster comparison than a regular String "..."
velocity.x += 1
if Input.is_action_pressed(&"ui_left"):
velocity.x -= 1
if Input.is_action_pressed(&"ui_down"):
velocity.y += 1
if Input.is_action_pressed(&"ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed # so diagonal is same speed as orthogonal
$AnimatedSprite2D.play() # $AnimatedSprite2D is shorthand for getting children with get_node('AnimatedSprite2D')
else:
$AnimatedSprite2D.stop()
position += velocity * delta
# keep in screen:
position = position.clamp(Vector2.ZERO, screen_size)
# to rotate sprite "up" animation in 8 directions:
$AnimatedSprite2D.animation = &"up"
if velocity.x < 0 and velocity.y < 0:
rotation = - PI * 1/4
elif velocity.x == 0 and velocity.y < 0:
rotation = 0
elif velocity.x > 0 and velocity.y < 0:
rotation = PI * 1/4
elif velocity.x < 0 and velocity.y == 0:
rotation = - PI * 1/2
elif velocity.x > 0 and velocity.y == 0:
rotation = PI * 1/2
elif velocity.x < 0 and velocity.y > 0:
rotation = - PI * 3/4
elif velocity.x == 0 and velocity.y > 0:
rotation = PI
elif velocity.x > 0 and velocity.y > 0:
rotation = PI * 3/4
signal hit
func _on_body_entered(body):
hit.emit()
# must defer setting value of physics properties in a physics callback (to avoid error if while processing a collision):
$CollisionShape2D.set_deferred("disabled", true)
your_array.pick_random() # instead of your_array[randi() % your_array.size()]
queue_free() # vs free()
# Main.gd:
# so you can use the Inspector panel to select a node, to let you use it in this code as a property/variable mob_scene:
@export var mob_scene: PackedScene
# so you can then create instances of that node whenever you want:
var mob = mob_scene.instantiate() # like inside of func _on_mob_timer_timeout():
# and set random location along a PathFollow2D: (setup: PathFollow inside Path; Path = path, PathFollow = location on path):
var mob_spawn_location = $MobPath/MobSpawnLocation
# or var mob_spawn_location = get_node(^"MobPath/MobSpawnLocation")
mob_spawn_location.progress = randi() # or randf()
mob.position = mob_spawn_location.position
# ...
add_child(mob) # to add the mob instance to the Main scene (run this line in Main.gd)
var random_between_inclusive = randf_range(-PI / 4, PI / 4)
$MessageTimer.start()
await $MessageTimer.timeout
# after waiting, then can do something else
# basically "sleep" or "delay": create a one-shot timer for 1.0 second and await for it to finish:
await get_tree().create_timer(1.0).timeout # instead of using a Timer node
Debugging tips from DevWorm: https://www.youtube.com/watch?v=PB6YPnRAyjE
-
func _input(event: InputEvents): if event.is_action_pressed('1') and OS.is_debug_build(): print('-----------------') print('a', a) print('few', few) print('vars', vars) # at the same time print('-----------------')
-
push_error('this error message shows up in the Errors tab and is clickable to go to the place in the code') # or you can also use breakpoints, then step into or step over (in Debugger)
- there are lots of toggle under the Debug menu for things like "Visible Collision Shapes" or "Visible Paths"
3D:
extends CharacterBody3D
signal hit
@export var speed = 14 # meters per second
@export var jump_impulse = 20 # big = unrealistic but responsive feels good
@export var bounce_impulse = 16
@export var fall_acceleration = 75 # i.e. gravity
func _physics_process(delta): # better than _process(delta) for physics
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
basis = Basis.looking_at(direction) # use built-in basis property to rotate the player
$AnimationPlayer.speed_scale = 4 # speed up animation
else:
$AnimationPlayer.speed_scale = 1
velocity.x = direction.x * speed # left/right
velocity.z = direction.z * speed # forward/back
# velocity.y + jumping up:
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
# velocity.y - falling down:
velocity.y -= fall_acceleration * delta
move_and_slide() # to smooth out physics motion
# Here, we check if we landed on top of a mob and if so, we kill it and bounce.
# With move_and_slide(), Godot makes the body move sometimes multiple times in a row to
# smooth out the character's motion. So we have to loop over all collisions that may have
# happened.
# If there are no "slides" this frame, the loop below won't run.
for index in range(get_slide_collision_count()): # check all collisions over move_and_slide():
var collision = get_slide_collision(index)
if collision.get_collider().is_in_group("mob"): # if hit mob:
var mob = collision.get_collider()
if Vector3.UP.dot(collision.get_normal()) > 0.1: # if the collision happened roughly above the mob:
mob.squash() # call the custom squash() function of that mob instance's Mob.gd script
velocity.y = bounce_impulse
break # escape the for-loop to avoid over-counting squashing 1 mob
# this makes the character follow a nice arc when jumping:
rotation.x = PI / 6 * velocity.y / jump_impulse
func die():
hit.emit() # emit custom signal
queue_free() # clear this node from memory
func _on_MobDetector_body_entered(_body):
die()
# Player.gd:
# faster:
$AnimationPlayer.speed_scale = 4
# or normal speed:
$AnimationPlayer.speed_scale = 1
# ...
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
# to get viewport size:
# DON'T use this:
get_viewport().size # gave me incorrect values after screen resize (e.g. fullscreen)
# USE this:
get_viewport().get_visible_rect().size # e.g. inside the GDScript of a Node
# or when available:
get_viewport_rect().size # e.g. inside the GDScript of a CanvasItem or a RigidBody2D