godot-walkabout/addons/walkabout/characters/character.gd
2025-08-06 10:54:13 -06:00

174 lines
5 KiB
GDScript

@tool
@icon("character.svg")
class_name WBCharacter
extends CharacterBody2D
## A character that can be controlled by events and behaviors and moves on a grid.
## Emitted when the character begins moving.
signal move_started()
## Emitted when the character reaches its target position.
signal move_finished()
enum Dir {LEFT, RIGHT, UP, DOWN}
const DIR_VECTORS: Dictionary[Dir, Vector2] = {
Dir.LEFT: Vector2.LEFT,
Dir.RIGHT: Vector2.RIGHT,
Dir.UP: Vector2.UP,
Dir.DOWN: Vector2.DOWN,
}
const DIR_ANIM_SUFFIXES: Dictionary[Dir, StringName] = {
Dir.LEFT: &"_left",
Dir.RIGHT: &"_right",
Dir.UP: &"_up",
Dir.DOWN: &"_down",
}
## Size of the grid the character is restricted to.
@export var tile_size: float = 16.0
## Speed the character walks at.
@export var walk_speed: float = 4.0
## Speed the character runs at.
@export var run_speed: float = 8.0
## Direction the character is facing.
@export var facing: Dir = Dir.DOWN
## Animation library for the character. [br]
## At a minimum, the [code]idle_*[/code] animations are required. [br]
## The following animations are used by default behavior: [br]
## [code]idle_[left,right,up,down][/code] [br]
## [code]walk_[left,right,up,down][/code] [br]
## [code]run_[left,right,up,down][/code] [br]
## [code]run_*[/code] will fallback to [code]walk_*[/code],
## which will fallback to [code]idle_*[/code]. [br]
## Addition custom animations may be provided to play on demand.
@export var animations: SpriteFrames:
set(value):
animations = value
sprite.sprite_frames = animations
## Texture drawing offset of the animated sprite.
@export var sprite_offset: Vector2 = Vector2.ZERO:
set(value):
sprite_offset = value
sprite.offset = sprite_offset
## True when the character is moving.
var moving: bool = false
## Whether the character is running.
var running: bool = false
## Tile position of the character on the grid.
var tile_position: Vector2i:
get():
return pos_to_tile(global_position)
var sprite: AnimatedSprite2D
var _next_pos: Vector2
var _playing_custom_animation: bool = false
func _init() -> void:
sprite = AnimatedSprite2D.new()
sprite.sprite_frames = animations
add_child(sprite)
func _ready() -> void:
global_position = closest_tile_center(global_position)
for child in get_children():
if child is CollisionShape2D or child is CollisionPolygon2D:
return
var col_shape := CollisionShape2D.new()
col_shape.shape = RectangleShape2D.new()
col_shape.shape.size = Vector2(tile_size - 2.0, tile_size - 2.0)
add_child(col_shape)
func _physics_process(delta: float) -> void:
if moving:
var move_delta := (run_speed if running else walk_speed) * tile_size * delta
global_position = global_position.move_toward(_next_pos, move_delta)
if global_position == _next_pos:
moving = false
move_finished.emit()
func _process(delta: float) -> void:
if moving:
_playing_custom_animation = false
var anims: Array[StringName] = [
&"walk" + DIR_ANIM_SUFFIXES[facing],
&"idle" + DIR_ANIM_SUFFIXES[facing]
]
if running:
anims.push_front(&"run" + DIR_ANIM_SUFFIXES[facing])
_try_animations(anims)
elif not _playing_custom_animation:
_try_animations([&"idle" + DIR_ANIM_SUFFIXES[facing]])
## Makes the character move one tile in the given direction. [br]
## If [param ignore_collision] is true, the character will not perform collision checks.
func start_move(dir: Dir, ignore_collision: bool = false) -> bool:
if moving:
return false
facing = dir
_next_pos = global_position + DIR_VECTORS[dir] * tile_size
var col := move_and_collide(_next_pos - global_position, true)
if col and not ignore_collision:
return false
moving = true
move_started.emit()
return true
## Plays a given custom animation from the animation set. [br]
## If [param reset_after] is [constant true], the animation will return to
## the default idle animation after it finishes.
func play_custom_animation(anim: StringName, reset_after: bool = false) -> void:
_try_animations([anim])
_playing_custom_animation = true
if reset_after and not animations.get_animation_loop(anim):
await sprite.animation_finished
_playing_custom_animation = false
## Stops playing custom animation if one is currently playing.
func end_custom_animation() -> void:
_playing_custom_animation = false
## Returns the closest tile center position to a given position in global coordinates.
func closest_tile_center(pos: Vector2) -> Vector2:
var tile := pos - Vector2(tile_size, tile_size) * 0.5
tile = tile.snappedf(tile_size)
tile += Vector2(tile_size, tile_size) * 0.5
return tile
## Returns the tile coordinates of a given position in global coordinates.
func pos_to_tile(pos: Vector2) -> Vector2i:
return Vector2i((global_position / Vector2(tile_size, tile_size)).floor())
## Returns the center position of a given tile in global coordinates.
func tile_center_pos(tile: Vector2i) -> Vector2:
return (Vector2(tile) * Vector2(tile_size, tile_size)) + (Vector2(tile_size, tile_size) * 0.5)
func _try_animations(anims: Array[StringName]) -> void:
for anim in anims:
if animations.has_animation(anim):
sprite.play(anim)
return