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:
best practices manual:
&"
and ^"
, Ctrl+F in this page: GDScript basics, including syntax notes
$Player.position
and $UserInterface/Retry.show()
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
For Ctrl+F convenience to remind myself of things:
to connect a signal:
signal caught
and func _on_area_2d_area_entered(area): caught.emit()
var mob = mob_scene.instantiate(); mob.connect("caught", Callable(self, "_on_mob_caught")); func _on_mob_caught(): # update score
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
body_entered(body:Node2D)
signal -->]
func _on_body_entered(_body)
in the script file of same node)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
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
CanvasLayer
's Inspector panel has a Layer property for a layer number that behaves like z-index
in CSS
ColorRect
for a solid colour backgroundTextureRect
for an image backgroundAudioStreamPlayer
for music/sound: Inspector > AudioStreamPlayer > Stream > (you can expand to show more settings, like Loop)
Label
's font can be set in: Inspector > Control > Theme Overrides > FontsLabel
's size can be set in: Inspector > Control > Layout > Transform > SizeButton
'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.)Area
or RigidBody
or Sprite
,
AnimatedSprite2D
in: Inspector > Node2D > Transform > ScaleCollisionShape2D
's size in: Inspector > CollisionShape2D > set dimensions after you choose a ShapeTimer
get_tree().call_group("mobs", "queue_free")
to call queue_free
on all nodes that are part of group "mobs"
to delete themselvesCharacterBody3D
= Area
or RigidBody
, but 3D and controlled by player instead of by physics engine
Node3D
as pivot named Pivot
to isolate the animations on this node's child Character
from overriding code-set values on the Pivot
's properties, letting you layer motion/rotations/offset/etc. on top of the child Character
's animationNode3D
as character 3D model named Character
, e.g. a .glb file, which can be exported from Blender by exporting to GLTFCollisionShape3D
named CollisionShape
to collide with environmentArea3D
named MobDetector
to detect collisions with other characters, but with "Monitorable" prop unchecked so other nodes can't detect itCollisionShape3D
s to the MobDetector
Area3D
AnimationPlayer
CharacterBody3D
has a native move_and_slide()
function you can call at the end of your func _physics_process(delta):
to smooth out motionCharacterBody3D
has a native look_at_from_position(start_position, position_of_target_to_face_towards, Vector3.UP)
function (3rd param = which way is up axis to rotate around)
look_at(player.position)
(you might need to @export var player: PackedScene
at the top of your code)CharacterBody3D
has a native rotate_y(randf_range(-PI / 4, PI / 4))
function that you can call after look_at_from_position
to further adjust rotation.CharacterBody3D
has a native is_on_floor()
function that uses up_direction
and floor_max_angle
to determine whether a surface is a "floor".basis = Basis.looking_at(direction)
move_and_slide()
makes the node move sometimes multiple times in a row to smooth out the motion. so loop over all collisions in get_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 with queue_free()
on the parent to save on memory when the child VisibleOnScreenNotifier3D
fires a screen_exited()
signalMarker3D
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 the Camera3D
is selected)Camera3D
's Size
property in Inspector lets you see widerFar
value affects directional shadow quality:
Far
: see farther, but worse shadows because rendering over bigger distance (shadows may be blurry)Far
, e.g 100
: better shadow quality, but can't see farther-away objectsPath3D
, see the instructions+images at https://docs.godotengine.org/en/stable/getting_started/first_3d_game/05.spawning_mobs.html
PathFollow3D
node as child of Path3D
node; Path
= path, PathFollow
= to select locations on that pathControl
! as well as the other things nested under it in the search for the "Create New Node" window
Label
, which has a default text
property: text = "Score: %s" % score
or text = "Score: %s" % [score,]
Label.text
)Control
's Theme panel will be accessible in a bottom tabControl
's children will inherit its theme, e.g. font (Inspector > Control > Theme > Default Font > click to expand details > Resource > Path > choose a font file).Label
or ColorRect
position relative to or fill its parent Control
and make that parent in turn fill the viewport too:
ColorRect
> click the green smash-bros-like icon for Anchor preset in the top bar > Full Rect (anchor value is relative to its parent).Control
> Inspector > Control > Layout > Anchors preset > Full Rect (anchor value is relative to its parent).mob
) created in code, need code to connect their signals:
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed)
in Main.gd, where:signal squashed
in Mob.gdfunc _on_Mob_squashed():
in Main/UserInterface/ScoreLabel.gdmob.connect("squashed", Callable($UserInterface/ScoreLabel._on_Mob_squashed, "_on_Mob_squashed"))
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.get_tree().reload_current_scene() # get the SceneTree
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 its AnimationPlayer
node > click on "Animation" > Manage Animations... > click on the paste icon (looks like a clipboard)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 the speed
variable in the Inspector
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)
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