首页 > 其他分享 >【Godot4自学手册】第二十七节自定义状态机完成看守地宫怪物

【Godot4自学手册】第二十七节自定义状态机完成看守地宫怪物

时间:2024-03-23 16:29:24浏览次数:16  
标签:状态 自定义 动画 Godot4 状态机 state func var

本节,我将使用自定义状态机实现看守地宫怪物,完成了基础类State,状态机类StateMachine的编码,实现了怪物的闲置巡逻类、追踪类和攻击类,以及对应动画等。这节代码有点多,不过还好,代码比较简单。最终效果如下:
请添加图片描述

一、基本概念

状态机(State Machine)是有限状态自动机的简称,是指一个数学模型,通常体现为一张状态转换图。

基本组成

有限状态机主要由以下几个部分组成:
1.状态(State): 状态是有限状态机的一个基本元素,代表了系统在某一时刻的一种情况。
2.输入(Input): 输入是系统从一个状态转换到另一个状态的触发条件。有限状态机在接收到特定的输入时,会从当前状态转换到另一个状态。
3.输出(Output): 输出是状态机在执行某个动作或处理某个输入时产生的结果。在状态转换中可能会伴随有输出。
4.状态转换(Transition): 状态转换描述了状态机在接收到特定输入时,从当前状态迁移到下一个状态的过程。
5.初始状态(Initial State): 初始状态是系统开始运行时的状态。
6.终态(Final State): 终态(也称为接受状态或终止状态)是系统执行完毕或处理结束后的状态。

工作原理

有限状态机的工作原理可以概括为:
1.系统开始于初始状态。
2.接收到输入后,根据当前状态和输入,确定下一个状态。
3.进入新状态后,执行该状态对应的动作(如果有)。
4.重复上述过程,直到到达终态。

应用

有限状态机因其结构简单、逻辑清晰,在多个领域有广泛的应用:
计算机科学: 在编译原理中用于词法分析,在操作系统中管理进程状态等。
控制理论: 在自动化控制系统中,用来设计控制器。
通信系统: 在数字通信中,用于编码和同步。
软件工程: 在游戏开发、用户界面设计等领域中管理复杂的逻辑。

有限状态机是理解复杂系统行为的一种强有力的工具,通过将系统的行为分解为一系列的状态和转换,可以更容易地分析和设计系统。

二、基础代码编写

1.添加状态类代码

新建脚本文件保存在Class文件夹下命名为:State.gd,这是个状态类基础代码,定义了一个状态改变信号Transitioned,和四个基本函数进入函数Enter()、退出函数Exit()、更新函数Update()、物理更新函数Physics_Update(),只是定义了函数没有函数内容,当继承时具体填写内容。代码如下:

extends Node
class_name  State
signal  Transitioned
func Enter():
	pass
func Exit():
	pass	
func Update(delta:float):
	pass
func Physics_Update(delta:float):
	pass
2.添加有限状态机代码

新建脚本文件保存在Class文件夹下命名为:StateMachine.gd。顾名思义,这是对敌人的各种状态进行管理的一个类,代码中有具体功能注释。代码如下:

extends Node
@export var inital_state:State  #初始状态
var current_state:State  #当前状态
var states:Dictionary={}  #状态字典

func _ready():
	#完成状态字典数据
	for child in get_children():
		if child is State:
			states[child.name.to_lower()]= child
			child.Transitioned.connect(on_child_transition)  #连接信号到本页脚本
	#设置初始状态
	if inital_state:
		inital_state.Enter() #调用状态进入函数
		current_state = inital_state
	pass 


func _process(delta):
	#调用当前状态更新函数
	if current_state:
		current_state.Update(delta)
		
func _physics_process(delta):
	#调用当前状态物理更新函数
	if current_state:
		current_state.Physics_Update(delta)

#状态改变信号调用函数,第一个参数表示目前处于状态,也就是进入新的状态有哪个状态发起的;第二个参数表示要进行新状态的名称
func on_child_transition(state,new_state_name):
	#如果传入的状态部署当前状态,退出信号
	if state!=current_state:
		return
	#根据状态名称调出状态数据字典中对应的状态
	var new_state = states.get(new_state_name.to_lower())
	#如果状态数据字典中不存在对应的状态退出
	if !new_state:
		return
	#退出当前状态,调用状态退出函数
	if current_state:
		current_state.Exit()
	#进入新的状态,调用进入函数
	new_state.Enter()
	#将当前状态设置为新的状态
	current_state = new_state	

这样我们有效状态机的基础代码就写好了。

三、敌人的各种状态代码

在我们的文件系统重新建一个States文件夹用来保存各种状态。在该文件下新建EnemyState文件夹来保存敌人的状态代码。

1、空闲巡逻代码

新建脚本文件保存在States->EnemyState文件夹下命名为:EnemyIdle.gd。代码如下:

extends  State  #继承基本状态类
class_name  EnemyIdle  #类名称
@export var enemy:CharacterBody2D  #敌人,出现在该类的检查器,可拖入敌人的CharacterBody2D对象
@export var move_speed:=30.0  #敌人移动速度,出现在该类的检查器
@export var anima:AnimatedSprite2D #敌人播放动画类,出现在该类的检查器
var player:CharacterBody2D  #玩家对象
var move_direction:Vector2  #敌人移动方向
var wander_time  #敌人巡逻时间
#随机巡逻函数,产生随机方向和巡逻时间
func randomize_wander():
	#产出敌人随机移动方向
	move_direction = Vector2(randi_range(-1,1),randi_range(-1,1)).normalized()
	#敌人此方向随机巡逻时间
	wander_time = randf_range(1,3)
#状态进入时调用的函数
func Enter():
	#在主目录中根据分组查询主人公对象
	player = get_tree().get_first_node_in_group("Player")
	randomize_wander()#调用随机巡逻函数

func Update(delta:float):
	if wander_time>0:#如果该方向巡逻时间大于0,巡逻时间减去delta时间
		wander_time -=delta
	else:#如果敌人在方向巡逻时间完成,从新产生巡逻随机方向和时间
		randomize_wander()

func Physics_Update(delta:float):
	#获取敌人和主人公之间的方向和距离
	var direction= player.global_position-enemy.global_position	
	#如果敌人和主人公之间的方向和距离大于跟踪距离,敌人进行巡逻状态
	if direction.length()<25:
		Transitioned.emit(self,"Attack")#如果敌人和主人公之间的方向和距离小于攻击距离,发出攻击信号
		return
	elif direction.length()<100:
		Transitioned.emit(self,"Follow")#如果敌人和主人公之间的方向和距离处于跟踪距离,发出跟踪信号
		return
	if enemy:
		enemy.velocity = move_direction * move_speed#设置敌人的速度
		if enemy.velocity==Vector2.ZERO:#如果敌人的速度为0,播放休闲动画
			anima.play("Idle")
		else:#如果敌人的速度不为0,播放行走动画
			anima.play("Walk")
2、跟踪状态代码

新建脚本文件保存在States->EnemyState文件夹下命名为:EnemyFollow.gd。代码如下:

extends State  #继承基本状态类
class_name  EnemyFollow   #类名称
@export var enemy:CharacterBody2D #敌人,出现在该类的检查器,可拖入敌人的CharacterBody2D对象
@export var move_speed:=30.0 #敌人移动速度,出现在该类的检查器
@export var anima:AnimatedSprite2D  #敌人播放动画类,出现在该类的检查器
var player:CharacterBody2D #玩家对象
#状态进入时调用的函数
func Enter():
	#在主目录中根据分组查询主人公对象
	player = get_tree().get_first_node_in_group("Player")
	pass
	
func Update(delta:float):
	pass

func  Physics_Update(delta:float):		
	#计算敌人与主人公之间的方向和距离
	var direction= player.global_position-enemy.global_position
	if direction.length()>100:#如果敌人与主人公之间的距离未达到跟踪范围,发出空闲巡逻状态信号
		Transitioned.emit(self,"Idle")
		return
	if direction.length()<25:#如果敌人与主人公之间的距离进入攻击范围,发出攻击状态信号
		Transitioned.emit(self,"Attack")
		return
	if anima:#如果动画设置不为空,播放行走动画
		anima.play("Run")
	enemy.velocity = direction.normalized() * move_speed #设置行走速度	
3、攻击状态代码

新建脚本文件保存在States->EnemyState文件夹下命名为:EnemyAttack.gd。代码如下:

extends State #继承基本状态类
class_name EnemyAttack  #类名称
@export var enemy:CharacterBody2D #敌人,出现在该类的检查器,可拖入敌人的CharacterBody2D对象
@export var anima:AnimatedSprite2D #敌人播放动画类,出现在该类的检查器
var player:CharacterBody2D #玩家对象
#状态进入时调用的函数
func Enter():
	#在主目录中根据分组查询主人公对象
	player = get_tree().get_first_node_in_group("Player")

func  Physics_Update(delta:float):	
	enemy.velocity= Vector2()
	#计算敌人与主人公之间的方向和距离
	var direction= player.global_position-enemy.global_position
	#如果敌人与主人公之间的距离大于100,发出空闲巡逻状态信号
	if direction.length()>100:
		Transitioned.emit(self,"Idle")
	elif direction.length()>25:#如果敌人与主人公之间的距离达到跟踪范围,发出跟踪状态信号
		Transitioned.emit(self,"Follow")
	if anima:#播放攻击动画
		anima.play("Attack")

四、应用到场景中

新建CharacterBody2D场景,存到Scenes文件夹下,命名为Monster。为场景添加相关节点。

1.添加AnimatedSprite2D节点。

添加AnimatedSprite2D节点,命名为Anima。在其检查器中,选择Animation->Sprite Frames属性,下拉菜单中选择新建SpriteFrames。选中该属性,在动画帧面板中讲default命名为Idle,单击从精灵表中添加动画帧按钮,在弹出的打开文件对话框中选择我们准备的敌人图片素材,如下:
请添加图片描述

在弹出的选择帧面板中将水平设置为4,垂直设为5,这是根据我们敌人图片对应进行设置的,因为我们的敌人图片正好是5行4列。然后选择0-3帧图片,最后单击添加帧按钮。
请添加图片描述

然后在动画帧面板中开启循环和自动播放按钮,如下:
请添加图片描述

这样就完成了等待动画。下面单击添加动画按钮,命名为Walk,然后跟制作等待动画类似完成行走动画;依此类推完成跑动动画Run、攻击动画Attack,这里面有个细节需要说一下,行走动画为图片素材的第2行、跑步动画为图片素材的第3行;攻击动画为图片素材的4和5行。这3个动画都不需要开启自动播放,但是行走动画、跑步动画需要开启循环,攻击动画不需要开启循环。跑步动画和攻击动画设为8FPS,动画变快,游戏显得更合理些。
请添加图片描述

2.添加CollisionShape2D节点

添加CollisionShape2D节点,命名为Collision。在其检查器中,选择CollisionShape2D->Shape属性选择新建CapsuleShape2D(椭圆形碰撞),然后在场景中将椭圆形调整合适大小和位置。
请添加图片描述

3.添加Node2D节点

一是添加Node2D节点,命名为StateMachine。然后单击为选中节点创建或设置脚本按钮,选择我们前面编写好的代码StateMachine.gd。
请添加图片描述

然后在检查器中,将Inital State设置为Idle状态。
请添加图片描述

二是选择StateMachine节点,单击添加子节点按钮,然后在创建节点对话框中选择EnemyIdle节点,该节点重命名为Idle。
请添加图片描述

在检查其中将Enemy设置成Monster根节点;Anima设置成该场景中Anima节点。
请添加图片描述

三是与二方法类似添加EnemyFollow和EnemyAttack节点,重命名为Follow和Attack。在检查器中对应设置Enemy和Anima属性,最终节点目录如下:
请添加图片描述

4.根节点添加脚本

选择Monster跟节点,单击为选中节点创建或设置脚本按钮,把脚本保存到Scripts目录,命名为Monster.gd。编写如下代码:

extends CharacterBody2D
@onready var anima = $Anima  #获取动画
func _physics_process(delta):
	if velocity.x<0:#如果速度小于0,翻转动画
		anima.flip_h=true
	else:
		anima.flip_h= false
	move_and_slide()
5.主场景中调用

切换到Main主场景中,单击实例化子场景,选择Monster场景。
请添加图片描述

然后调整到需要的位置。
请添加图片描述

最后看一下效果:
请添加图片描述

这节就到这了,下节见。

标签:状态,自定义,动画,Godot4,状态机,state,func,var
From: https://blog.csdn.net/zhaoyang314/article/details/136934709

相关文章

  • lua/c开发:lua增加自定义require方式
    我们会有需要自定义加载模块逻辑的需求,比如支持从自定义格式数据包中加载一个被加密过的lua文件的方式,这在生产环境中非常常见,可以有效保护源码同时保持整洁;lua模块管理库会从若干个loader中逐个尝试加载模块,lua原生提供了4个loader;staticconstlua_CFunctionsearchers[]={......
  • 鸿蒙自定义控件实现罗盘数字时钟效果
    前言:DevEcoStudio版本:4.0.0.600关注过我的小伙伴一定知道我之前写过一篇基于Android的 仿抖音效果的数字时钟罗盘 最近看了鸿蒙的Canvas组件,今天通过Canvas组件也实现下罗盘数字时钟的效果。参考链接:OpenHarmonyCanvas  OpenHarmonyCanvasrenderingcontext2d效果:......
  • 使用Django-Simple-Captcha在Django项目加入验证码模块并自定义样式
    在Django项目中加入验证码功能,通常需要借助第三方库,比如Django-Smple-Captch、Django-reCAPTCHA、DEF-reCAPTCHA、Wagtail-Django-ReCaptcha、Django-Friendly-Captcha等。其中,Django-Smple-Captcha是一个流行的选择,它提供了一个简单而强大的Django应用,无需调用第三方API,......
  • 深度解析webpack5以及打包实践攻略,看完这篇带你玩转高级自定义打包
    1.webpack5对比webpack4做了哪些优化Webpack5对比Webpack4存在一些重要的优化。Webpack5在性能、构建速度、TreeShaking等方面都有所改进:性能改进:Webpack5在构建速度和性能方面有所提升。这主要是通过改进缓存策略、优化构建算法以及增强的持久化缓存等方式......
  • Vue开发日志:自定义组件:通用开发流程
    自定义组件:通用开发流程通用流程一组概念:key,value,labelProps:required和default同时存在的必要性让我们简单梳理一下通用流程在Vue.js中开发自定义组件的通用流程如下:定义组件模板:创建一个.vue文件,里面包含模板、样式和脚本部分。例如:<!--MyCustomCompone......
  • python 教你如何创建一个自定义库 colorlib.py
    目录Colorlib生成代码模块代码导入测试测试一测试二应用测试颜色列表colorList随机颜色元组randcolorTuples随机颜色字串randcolorStringsColor类测试测试一测试二题外话Colorlib有没有碰到过这样的场景:写代码时想要用上丰富的色彩,但苦思冥想搜肠刮肚只记......
  • 自定义类型--结构体、联合体、枚举类型
    **Ladiesandgentlemen**,今天,我们将来进行对自定义类型的学习!目录1.结构的特殊声明2.结构体内存对齐2.1对齐规则2.1.12.1.22.1.32.1.42.2为什么存在内存对齐?1.平台原因(移植原因):2.性能原因:2.3修改默认对齐数3.结构体传参4.结构体实现位段4.1什么......
  • uni-app/小程序自定义导航栏下拉刷新loading图标看不到问题解决
    实际效果图 我们在page.json中开启了自定义导航栏属性和下拉刷新属性后//开启下拉刷新"enablePullDownRefresh":true//自定义导航栏"navigationStyle":"custom"此时,页面中的下拉刷新三个小圆点会被我们的导航栏遮盖住,导致用户下拉刷新看不到loading效果,如下图:......
  • ROS2自定义msg
    在ROS2中,您可以通过编写自己的自定义消息来扩展消息类型。以下是如何创建自定义消息的一般步骤:1.**创建消息文件夹**:在功能包下创建msg的文件夹2.**编写消息文件**:在`msg`文件夹内创建一个`xxx.msg`文件,命名为所需的消息类型,例如`MyCustomMsg.msg`。3.**定义消息结构**:在消......
  • ThinkPHP自定义指令
    官网文档https://www.kancloud.cn/manual/thinkphp6_0/1037651创建命令类文件运行指令创建一个自定义命令类文件phpthinkmake:commandHellohello生成内容如下<?phpnamespaceapp\command;usethink\console\Command;usethink\console\Input;usethink\console\in......