【极简】Godot 4.4 有限状态机

发布于:2025-04-16 ⋅ 阅读:(34) ⋅ 点赞:(0)

无望其速成,无诱于势利。

一、展示

本文实现简易有限状态机—— 空闲/行走/跳跃。

请添加图片描述

二、复刻

(一)节点结构

1. player.tscn

  • Player(CharacterBody2D)
    • AnimateSprite2D
    • CollisionShape2D
    • NodeState(Node)
      • Idle(Node)
      • Walk(Node)
      • Jump(Node)

2. test_scene.tscn

  • TestScene(Node2D)
    • Player(player.tscn):见1
      • Camera2D
    • StaticBody2D
      • CollisionShape2D

在这里插入图片描述
在这里插入图片描述

(二)独立资源/脚本

1. state.gd(状态基类)

功能:被所有状态子类继承的父类模板,状态子类在此基础上重写方法

class_name State
extends Node
## 状态基类
## 用于状态模式中表示一个状态,子类应继承此类并实现具体状态逻辑

@warning_ignore("unused_signal")
signal transition  # 当前状态发出转换信号 


# 状态处理函数(每帧调用)
func _on_process(_delta : float) -> void:
	pass


# 物理处理函数(固定时间步长调用)
func _on_physics_process(_delta : float) -> void:
	pass


# 状态转换条件检测(由状态管理器定期调用)
func _on_next_transitions() -> void:
	pass


# 状态进入时回调
# 当状态到此状态时自动调用,用于初始化状态相关逻辑
func _on_enter() -> void:
	pass


# 状态退出时回调
# 当状态机切换出此状态时自动调用,用于清理状态相关资源
func _on_exit() -> void:
	pass
	

2. game_input.gd

功能:管理游戏输入逻辑

class_name GameInput
extends Node


static func movement_input() ->float:
	# 获取键盘输入方向:-1/0/1
	var direction = Input.get_axis("left", "right")
	
	return direction
	
static func is_movement_input() ->bool:
	if movement_input() == 0.0:
		return false
	return true

(三)附加脚本

1. player.gd(玩家脚本)

附加到player.tscn中的 Player(CharacterBody2D)节点:

class_name Player
extends CharacterBody2D

const SPEED: float = 100.0

var direction: float

2. state_machine.gd(状态管理器)

附加到player.tscn中的 StateMachine(Node)节点:

class_name StateMachine
extends Node

@export var initial_state : State

var states : Dictionary = {}
var current_state : State
var current_state_name : String
var parent_name: String


func _ready() -> void:
	parent_name = get_parent().name
	
	# 获取状态管理器下所有状态
	# 加入字典、连接信号
	for child in get_children():
		if child is State:
			states[child.name.to_lower()] = child
			child.transition.connect(transition_to)
	
	# 进入初始状态
	if initial_state:
		initial_state._on_enter()
		current_state = initial_state
		current_state_name = current_state.name.to_lower()


# _process 和 _physics_process
# 执行当前状态的行为(如空闲:播放空闲动画)
func _process(delta : float) -> void:
	if current_state:
		current_state._on_process(delta)


func _physics_process(delta: float) -> void:
	if current_state:
		current_state._on_physics_process(delta)
		current_state._on_next_transitions()
		#print(parent_name, " Current State: ", current_state_name)


# 状态转变信号的回调函数
# 状态转换
func transition_to(state_name : String) -> void:
	if state_name == current_state.name.to_lower():
		return
	
	var new_state = states.get(state_name.to_lower())
	
	if !new_state:
		return
	
	if current_state:
		current_state._on_exit()
	
	new_state._on_enter()
	
	current_state = new_state
	current_state_name = current_state.name.to_lower()
	#print("Current State: ", current_state_name)

3. idle.gd

附加到player.tscn中的 Idle(Node)节点:

extends State
## 空闲状态

@export var player: Player
@export var animated_sprite_2d: AnimatedSprite2D


func _on_process(_delta : float) -> void:
	pass


func _on_physics_process(delta : float) -> void:
	# 播放空闲动画
	if player.direction != 0.0:
		animated_sprite_2d.flip_h = player.direction < 0
	
	animated_sprite_2d.play("idle")
	# 添加重力
	if not player.is_on_floor():
		player.velocity += player.get_gravity() * delta
	

	player.move_and_slide()
	
func _on_next_transitions() -> void:
	# 按下跳跃键转为跳跃状态
	if Input.is_action_just_pressed("jump") and player.is_on_floor():
		transition.emit("jump")
	# 有方向输入转为行走状态
	elif GameInput.is_movement_input():
		transition.emit("Run")


func _on_enter() -> void:
	pass


func _on_exit() -> void:
	animated_sprite_2d.stop()

4. run.gd

附加到player.tscn中的 Run(Node)节点:

extends State
## 行走状态

@export var player: Player
@export var animated_sprite_2d: AnimatedSprite2D


func _on_process(_delta : float) -> void:
	pass


func _on_physics_process(delta : float) -> void:
	# 获取输入
	var direction: float = GameInput.movement_input()
	
	# 播放移动动画
	if direction != 0:
		animated_sprite_2d.flip_h = direction < 0
		player.direction = direction
	
	animated_sprite_2d.play("run")
		
	# 控制移动
	if direction:
		player.velocity.x = direction * player.SPEED
	else:
		player.velocity.x = 0
	
	# 添加重力
	if not player.is_on_floor():
		player.velocity += player.get_gravity() * delta
		
	player.move_and_slide()


func _on_next_transitions() -> void:
	# 按下跳跃键转为跳跃状态
	if Input.is_action_just_pressed("jump") and player.is_on_floor():
		transition.emit("jump")
	# 无方向输入转为空闲状态
	elif not GameInput.is_movement_input():
		transition.emit("idle")


func _on_enter() -> void:
	pass


func _on_exit() -> void:
	animated_sprite_2d.stop()

5. jump.gd

附加到player.tscn中的 Jump(Node)节点:

extends State
## 跳跃状态

@export var player: Player
@export var animated_sprite_2d: AnimatedSprite2D

const JUMP_VELOCITY = -300.0


func _on_process(_delta : float) -> void:
	pass


func _on_physics_process(delta : float) -> void:
	# 添加重力
	player.velocity += player.get_gravity() * delta
	
	player.move_and_slide()
	
	
func _on_next_transitions() -> void:
	# 着陆后根据输入选择状态
	if player.is_on_floor():
		if GameInput.is_movement_input():
			transition.emit("run")
		else:
			transition.emit("idle")

func _on_enter() -> void:
	# 播放跳跃动画
	animated_sprite_2d.flip_h = player.direction < 0
	animated_sprite_2d.play("jump")
	# 控制跳跃
	player.velocity.y = JUMP_VELOCITY
	
	
func _on_exit() -> void:
	animated_sprite_2d.stop()
	player.velocity.x = 0

(四)其他设置

0. 导入资产

从文末链接下载资产源后,导入Godot引擎。

1. 新建动画

选中 player.tscn 场景下的 AnimateSprite2D 节点,在检查器中新建SpriteFrames;

在这里插入图片描述
从精灵表添加帧 的方式:
新建 idle 动画;

在这里插入图片描述
新建 run 动画;

在这里插入图片描述
新建 jump 动画;

在这里插入图片描述

2. 添加角色碰撞

选中 player.tscn 场景下的 CollisionShape2D 节点,在检查器中为角色添加圆形碰撞;

在这里插入图片描述

3. 添加碰撞平台

选中 test_scene.tscn 场景下的 CollisionShape2D 节点,在检查器中设置碰撞 WorldBoundaryShape2D ,并拖动到合适位置;

在这里插入图片描述

4. 显示碰撞区域

调试->显示碰撞区域,勾选后可见碰撞体;

在这里插入图片描述

5. 添加导出属性

选中 player.tscn 场景下的 StateMachine 节点,在检查器中选择Idle节点;

在这里插入图片描述
分别选中 player.tscn 场景下的 Idle、Run、Jump 节点,在检查器中选择Player、AnimateSprite2D节点;

在这里插入图片描述

6. 设置输入映射

项目设置->输入映射:添加 A、W、D 控制左右移动和跳跃;

在这里插入图片描述

7. 显示画面太小

在项目设置->显示->窗口:缩放设置为4.0;

在这里插入图片描述

8. 画面模糊

在项目设置->渲染->纹理->画布纹理:默认纹理过滤设置为Nearest;

在这里插入图片描述

三、运行测试

搭建测试场景如下:
请添加图片描述
测试完成!

四、自查对照

问题现象 关键检查点 解决方案
The InputMap action “left” doesn’t exist. Did you mean “ui_down”? 检查输入映射是否存在left,right,jump动作 项目设置→输入映射→添加left,right,jump并绑定键盘A、D、W键
画面太小 检查项目设置->常规->显示->拉伸->缩放 调整缩放大小
角色像素纹理不清晰 检查项目设置->渲染->纹理->画布纹理->默认纹理过滤 在默认纹理过滤设置为Nearest
@ _on_physics_process(): There is no animation with name ‘idle’. 检查AnimateSprite2D的底栏动画是否创建,若已创建,则查看命名有无问题 创建idle动画,或者修改动画名为idle

五、免费开源资产包

某开源网站精灵图资源包链接: 点击此处

  1. 进入链接后点击下图按钮

下载

  1. 然后点击【No thanks,just take me to the downloads】(不了谢谢,只想下载)

No thanks,just take me to the downloads

  1. 最后点击下图按钮完成下载(注意导入前需解压缩)

下载


网站公告

今日签到

点亮在社区的每一天
去签到