无望其速成,无诱于势利。
目录
一、展示
本文实现简易有限状态机—— 空闲/行走/跳跃。
二、复刻
(一)节点结构
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
- Player(player.tscn):见1
(二)独立资源/脚本
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 |
五、免费开源资产包
某开源网站精灵图资源包链接: 点击此处
- 进入链接后点击下图按钮
- 然后点击【No thanks,just take me to the downloads】(不了谢谢,只想下载)
- 最后点击下图按钮完成下载(注意导入前需解压缩)