Learning godot. Just notes on a simple two-level dungeon. Code is annotated.
The scene graph. Sprites are polygon2ds so i dont have to worry about spriting or animation for now.
area_entered
attached)
area_entered
attached)
MovementController2D.gd:
#
# # Movement Controller
#
# WARNING: not production quality, just learning code.
#
# This movement controller is used to apply generic movement
# functionality to 2D objects. Just add a basic Node object
# under your Player object of type Node2D (or extended) then
# drag the script onto your MovementController Node object.
#
# * Player (Area2D)
# * MovementController (Node) [script]
#
extends Node
#
# ## Moving Actions
#
# Moving actions are simple objects which have a special _process
# method that returns a boolean signifying whether the "action"
# is still running or not.
#
# ### Empty Moving Action
#
# This one always returns "still running" / true and only gets
# replaced when input is triggered.
#
class EmptyMovingAction:
extends Object
func _process(_delta):
return true
#
# ### MovingAction
#
# This one extends EmptyMovingAction and provides
# actual behavior, just moves in a direction and duration
# given.
#
class MovingAction:
extends EmptyMovingAction
var duration: int
var direction: Vector2
var sprite: Node2D
func _init(dir: Vector2, dur: int, spr: Node2D):
duration = dur
direction = dir
sprite = spr
func _process(delta):
if duration <= 0:
return false
duration = duration - delta
sprite.translate(direction*delta)
return true
#
# ## State
#
# We define our current moving action as Empty, first,
# as it means "no movement" and the _proecss loop just triggers
# nothing.
#
# Empty is a singleton so we do not have to instantiate it
# over and over.
var empty_action = EmptyMovingAction.new()
var moving_action: EmptyMovingAction = empty_action
# We also have our sprite object, but it's just get_parent
var sprite: Node2D
#
# We export two variables, this makes them show up
# in the godot UI. Distance is how far a single input will
# send the player and duration is how long it should run for.
#
@export var duration: int
@export var distance: int
func _ready():
sprite = get_parent()
#
# ## Input
#
# Most input is handled below in _process but handling the
# unpress action here ensures we get smooth stopping when we
# press 2 arrow buttons at once. Left and Up then releasing
# at the same time might be seen as "left unpressed, up pressed"
# for a brief moment, causing a stray movement in the up direction.
#
# this ensures that doesn't happen by just stopping movement the moment
# you release any button. If you keep holding down 'up' after releasing 'left'
# the next 'up' in _process will spawn a new up, causing smooth motion.
#
func _input(event):
if not event.is_pressed():
if event.is_action_type():
moving_action = EmptyMovingAction.new()
#
# We calculate dx and dy based on input so no one path has
# priority (aka, checking ui_right before ui_left could
# cause ui_right to always trigger if you press both buttons down).
#
# This design ensures 8-way movement with multi-button input.
#
func _process(delta):
var dx = 0
var dy = 0
var pressed = false
if Input.is_action_pressed("ui_right"):
pressed = true
dx += distance
if Input.is_action_pressed("ui_left"):
pressed = true
dx -= distance
if Input.is_action_pressed("ui_up"):
pressed = true
dy -= distance
if Input.is_action_pressed("ui_down"):
pressed = true
dy += distance
#
# this is why MovingAction itself cant be a Node we add or remove,
# we need to be able to detect when a MovingAction is done and
# remove it from our loop.
#
if not moving_action._process(delta):
moving_action = empty_action
if pressed:
moving_action = MovingAction.new(Vector2(dx, dy), duration, sprite)
dungeon.gd:
#
# ## Dungeon
#
# This is the node which represents a 2D top-down dungeon containing 2
# levels.
#
# This and the generic MovingController2D script are the only
# scripts in this demo.
#
extends Node2D
#
# ## Starting up
#
# We have two tilemaps in this, each one representing a "Level" in this
# dungeon. On start, we disable the second tilemap and hide it.
#
# hiding it disables rendering while setting process_mode removes
# all eventing and processing, including collision detection.
#
func _ready():
$TileMap2.process_mode = PROCESS_MODE_DISABLED
$TileMap2.hide()
$TileMap1.process_mode = PROCESS_MODE_INHERIT
$TileMap1.show()
#
## Events
#
# Each level in our dungeon has 1 event each. Both events are bound to
# area_entered signals on the Area2D object of the "Event" receiver (within
# godot UI, not in the code), meaning the "area" variable is the Player
# or another non-player Area2D, such as another monster or a UI element.
#
# This handler triggers when one Area2D enters the "Downstairs" event,
# which is triggered when you walk into the stairs object. This
# "teleports" the user to the level 2 of the dungeon by disabling
# one tilemap and enabling the other.
#
func _on_downstairs_event_area_entered(area: Node2D):
if area == $Player:
$TileMap2.show()
$TileMap1.hide()
# call deffered is used because this is a "collision/physics" callback
# and we cant change the physics logic during it. I wouldn't know
# except godot prints an error
$TileMap2.call_deferred("set_process_mode", PROCESS_MODE_INHERIT)
$TileMap1.call_deferred("set_process_mode", PROCESS_MODE_DISABLED)
#
# This is called when the player comes into contact
# with a monster. We trigger a "push-back"
# animation which would represent taking damage from the monster.
#
func _on_monster_area_entered(area: Node2D):
if area == $Player:
# there is no touched_monster signal in the player
# object (yet) but this is allows us to trigger stuff like
# 'lower HP, play damage animation, etc' and make it all
# encapsulated within the player object.
area.emit_signal("touched_monster", $TileMap2/Monster)
# for now, this janky animation...
var monster_pos: Vector2 = $TileMap2/Monster.global_position
area.translate (-abs(monster_pos - (area.global_position)))