• 【Godot4自学手册】第十八节主人公的血量显示


    今天,我主要学习主人公的血量显示,在屏幕的左上角,会显示主人公的头像,后面会显示血量进度条,当主人公受到伤害时,血量会实时变动,显示绿色血条减少,后是红色血量逐渐减少到正确位置,效果如下:
    请添加图片描述

    一、修改BUG

    再开始今天的学习之前,修改以前的一些Bug,在敌人攻击和受伤,主人公的攻击和受伤学习中又许多Bug,主要是因为我把需要执行一次的状态代码和需要执行一段时间的代码全都写到_physics_process函数的原因,所以我将需要执行一次的代码加到state状态的set(v):函数内执行一次;把需要多次执行的代码加到了_physics_process函数内。

    1.Player代码修改如下:
    extends CharacterBody2D
    @onready var anima_tree=$AnimationTree
    @onready var animation_player = $AnimationPlayer
    @onready var stats = $Stats
    @onready var direcion = Vector2.ZERO
    enum {
    	IDLE,
    	WALK,
    	SWORD,
    	HURT
    }
    var state = IDLE:
    	set(v):
    		state=v
    		match  state:
    			IDLE:
    				idle_state()
    			WALK:
    				walk_state()
    			SWORD:
    				sword_state()
    			HURT:
    				hurt_state()
    		
    const SPEED = 120.0
    var enemyPositon
    var knockback=200
    
    
    func _ready():
    	Globals.last_Player = position
    	
    
    func _physics_process(delta):	
    	direcion = Vector2.ZERO
    	direcion.x = Input.get_axis("left", "right")
    	direcion.y = Input.get_axis("up", "down")
    	if state!=SWORD and state!=HURT:
    		if direcion:
    			anima_tree.set("parameters/Idle/blend_position",direcion)
    			anima_tree.set("parameters/Walk/blend_position",direcion)
    			anima_tree.set("parameters/Sword/blend_position",direcion)
    			anima_tree.set("parameters/Hurt/blend_position",Vector2(direcion.x*-1,direcion.y))
    			state = WALK
    		else:
    			velocity=Vector2.ZERO
    			state=IDLE
    	if Input.is_action_just_pressed("sword"):
    		velocity= Vector2.ZERO
    		state = SWORD
    	Globals.last_Player = position
    	move_and_slide()	
    
    func idle_state():#待机状态
    	anima_tree["parameters/playback"].travel("Idle")
    func walk_state():#行走状态
    	anima_tree["parameters/playback"].travel("Walk")
    	velocity = direcion * SPEED	
    
    func sword_state():#进攻状态
    	anima_tree["parameters/playback"].travel("Sword")
    	await anima_tree.animation_finished	
    	state=IDLE
    	
    func hurt_state():#受伤状态
    	var dir = enemyPositon.direction_to(global_position).normalized()
    	anima_tree["parameters/playback"].travel("Hurt")
    	if dir.x>0:
    		velocity.x +=knockback
    	else :
    		velocity.x -=knockback
    	await anima_tree.animation_finished
    	stats.health -=10	
    	state=WALK
    	
    func _on_hurt_box_area_entered(area):
    	enemyPositon = area.owner.global_position
    	state=HURT	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    2.Enemy代码修改如下:
    extends CharacterBody2D
    enum {
    	IDLE,
    	CHASE,
    	ATTACK,
    	HURT,
    	DEATH
    }
    const SPEED = 1.0
    var hitCount=2
    @onready var anima_s = $AnimaS
    @onready var anima_p = $AnimaP
    @onready var hit_box = $CheckBox/HitBox
    @onready var trace_collsion = $CheckBox/TraceArea/Trace_Collsion
    
    var knockback=300
    var hurtdirecion
    
    var state = IDLE:
    	set(v):
    		state = v
    		match state:
    			IDLE:
    				idle_state()
    			CHASE:
    				chase_state()
    			ATTACK:
    				attack_state()
    			HURT:
    				hurt_state()
    			DEATH:
    				death_state()
    
    func _physics_process(delta):	
    	var direction =  Globals.last_Player-global_position
    	if direction.x<0:
    		anima_s.flip_h=true
    		hit_box.scale.x =-1
    	else:
    		anima_s.flip_h = false
    		hit_box.scale.x=1
    	if state==CHASE:		
    		global_position = global_position.move_toward(Globals.last_Player,SPEED)
    	
    	move_and_slide()
    	
    	
    func idle_state():#待机状态
    	anima_p.play("Idle")
    
    func chase_state():#跟踪状态
    	anima_p.play("Run")
    
    func attack_state():#攻击状态
    	anima_p.play("Attack")	
    	await  anima_p.animation_finished
    	state = CHASE
    	
    	
    	
    	
    func hurt_state():	#受伤状态
    	var dir = hurtdirecion.direction_to(global_position).normalized()
    	if abs(dir.x)>abs(dir.y):
    		if dir.x<0:
    			velocity.x =-knockback
    		else :
    			velocity.x =knockback
    	else:
    		if dir.y<0:
    			velocity.y =-knockback
    		else :
    			velocity.y =knockback
    	anima_p.play("TakeHit")
    	await  anima_p.animation_finished	
    	velocity = Vector2.ZERO
    	state=CHASE
    
    
    func death_state():	
    	pass
    
    func _on_trace_area_body_entered(body):
    	if body.name=="Player" and state!=HURT and state!=ATTACK:
    			state = CHASE
    
    
    func _on_trace_area_body_exited(body):	
    		if body.name=="Player" and state!=HURT and state!=ATTACK:
    			state = IDLE
    
    
    func _on_hit_area_body_entered(body):#进入攻击范围
    	if body.name=="Player" :
    		state = ATTACK
    
    
    func _on_hurt_box_area_entered(area):	#进入受伤范围
    	hurtdirecion = area.owner.global_position
    	state = HURT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    二、新建血量显示场景

    1.新建场景。

    单击新建场景按钮,选择其它节点,在创建Node对话框中,选择HBoxContainer节点,单击创建按钮,创建新的场景,如下:
    请添加图片描述

    将新场景保存到Scenes文件夹下,命名为:StatusUI。
    将根节点重新命名为StatusPanel,检查器中将Transform->Size重置为0,0。

    2.添加节点

    首先为根节点添加PanelContainer子节点,再为PanelContainer添加TextureRect子节点,显示头像。为根节点添加TextureProgressBar子节点,显示血量,最终效果如下:
    请添加图片描述

    最后将PanelContainer命名为HeadBox;将TextureRect命名为Head;将TextureProgressBar命名为HeadthBar。

    3.设置人物头像背景框

    将HeadBox检查器中Theme Overrides->style->Panel属性设置为StyleBoxTextture。并将我们事先做好的背景边框图片结合托人到Texture内。
    请添加图片描述

    单击Sub-Region属性,展开界面选择编辑区域,在区域编辑框中选择像素吸附,画出图像背景框,然后选择关闭按钮,如下:
    请添加图片描述

    检查器中Custom Minimum Size属性设置为23,23,表示图像框的大小为23.如下:
    请添加图片描述

    检查器中Theme Overrides->Styles->Content Margins中,Left,Top,Right,Bottom均设置为3,表示填充边框为3,如下:
    请添加图片描述

    4.设置头像

    切换到Head节点,在检查器中Texture选择AtlasTexture图集纹理,将主人公的Player.png图片拖入到Atlas属性,如下:
    请添加图片描述

    在检查器中选择Region,单击下面的编辑区域按钮,在弹出的区域编辑框中选择像素吸附,选取主人公正面头像,然后关闭。
    请添加图片描述

    head检查器中Stretch Mode属性选择 Keep Aspect Centered,表示图像保持比例缩放,居中。如下:
    请添加图片描述

    最后的头像效果如下:
    请添加图片描述

    5.设置血量显示条

    切换到HealthBar节点,在检查器中Textures展开属性,在Progress选择AtlasTexture,将HUD.png拖入到Atlas中,如下:
    请添加图片描述

    在Region属性下单击编辑按钮,在区域编辑框中选择自动剪裁,然后选择绿色图片,最后单击关闭。
    请添加图片描述

    然后突出Progress属性,在HealthBar检查器中Range属性中Max Value设置为1,step设置为0,Value设置为1,如下所示:
    请添加图片描述

    同样方法设置Texture下的Over,图片选择黑色背景边框,然后设置Progress Offset设置为1,1,这样保证绿色图片在黑色背景边框中间,设置如下所示:
    请添加图片描述

    在HealthBar的检查器中Control->Layout->Container Sizing->Vertical属性选择居中收缩。如下:
    请添加图片描述

    最后显示效果如下:
    请添加图片描述

    6.设置红色血量显示调

    复制HealthBar命名为HealthBarRead,并将其设为HealthBar的子节点,如下所示:
    请添加图片描述

    在其检查器中Textures属性重置Over属性,Progress属性选择唯一化,然后从新编辑区域,选择红色血条,效果如下:
    请添加图片描述

    然后在编辑器上方控制节点锚点和偏移值的预设中选择整个矩形,这样红色血量和绿色血量最终重合,如下:
    请添加图片描述

    然后在检查器中找到Visibility->Show Behind Parent并启用,这样绿色血量将会在红色血量的上方。
    请添加图片描述

    通过以上步骤所有节点都已布置完成,先忙开始编写代码。

    三、编写代码

    1.抽象状态类。

    无论是敌人还是主人公都有一些相同状态,如血量,我把这些相同的状态定义成一个类,需要的时候继承就好了。首先切换到脚本,单击文件新建脚本,新建Class文件夹,将新建的脚步保存到Class文件夹内,命名为Status。编写如下脚本:

    class_name Stats
    extends Node
    signal  health_changed
    
    @export var max_health: int = 100
    @onready var health: int = max_health:
    	set(v):
    		v = clampi(v, 0, max_health)
    		if health == v:
    			return
    		health = v
    		health_changed.emit()  #发出信号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第一行代码主要把该脚本命名为Stats类,这样的目的是,把所有跟敌人或者主人公有关状态的量抽象一个类,复用。
    第二行代码说明该类继承了Node节点,Node节点的方法和属性都可以使用。
    第三行定义了一个信号,表示当血量发生变化时,出现该信号。
    第四行定义了最大血量,默认是100,@export表示该变量可以在编辑器中看到和修改。
    第五行定义了当前血量,@onready表示该变量会在Node节点就绪状态时赋值,默认赋值为最大血量。
    第六行表示当给health变量赋值时,发生。
    第七行表示给health复制时,取值范围为0到max_health,当超过这个范围会自动变换到这个范围。
    第八行、第九行表示如果复完值就跳出该方法。
    第十行表示把用户输入的值付给health。
    第十一行表示复完值后发出血量变化信号。

    2.StatusUI场景代码

    切换到StatusUI场景,选择根节点,单击创建脚本按钮,将代码保存到Scripts文件夹内,命名为StatusUI。如下:
    请添加图片描述

    编写如下代码:

    extends HBoxContainer
    @export var stats:Stats
    @onready var health_bar = $HealthBar
    @onready var health_bar_red = $HealthBar/HealthBarRED
    
    
    func _ready():
    	stats.health_changed.connect(update_health)  #血量变化信号连接到血量更新UI
    	update_health()
    
    func update_health()->void:
    	var percentage :=stats.health/ float(stats.max_health)
    	health_bar.value = percentage
    	create_tween().tween_property(health_bar_red,"value",percentage,.3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这段代码中第二行定义了一个状态变量,继承Stats。
    第三至四行代码表示获取绿色血量和红色血量节点。
    _ready函数中的stats.health_changed.connect(update_health)代码表示血量变化信号发生后会调用update_health函数。
    update_health自定义函数表示血量发生变化后血量显示进度条适时变化。该函数前两行代码对绿色血量进行调整,后一行代码表示红色血量变化会在0.3秒内完成,并显示间补动画,就是红色血量有个减少过程,绿色血量直接发生变化。
    这里面有个引擎自带函数tween_propert方法解释一下。create_tween()表示创建一个间补动画,tween_property方法第一个参数表示要发生变化的对象,第二个参数表示该对象的哪个属性发生变化,第三个参数表示该属性到达的最后状态,第四个参数表示达到最后状态需要的持续时间。

    四、给主人公添加血量显示

    1.添加状态类

    切换到Player场景,选择根节点,然后点击添加节点按钮,选择Stats,我们自己编写的类,如下:
    请添加图片描述

    单击Stats节点,在其检查器中,我们会看到我们编写该类时,暴漏的max_health的变量,我们可以在检查器中进行修改。
    请添加图片描述

    2.添加StatusUI场景到Player场景

    切换到Player场景,选择根节点,单击实例化子场景按钮,在实例化子场景中选择StatusUI点击打开,就把StatusUI场景加入到Player场景内了,重名为PlayerStatus,然后适当调整位置。
    请添加图片描述

    在PlayerStatus节点单击右键选择重设父节点为新节点,在对话框中选择CanvasLayer节点,这样的目的是把主人公的血条显示固定在一个位置。操作如下:
    请添加图片描述

    调整PlayerStatus节点位置到屏幕的左上角,如下:
    请添加图片描述

    在检查器中将Stats选择Stats,操作如下:
    请添加图片描述

    ok,这样就应该好了,看一下最终效果:
    请添加图片描述

    注意观看左上角的血量变化。今天就到这,下节见,同学们。

  • 相关阅读:
    JVM(十三)—— 执行引擎
    使用矢量坐标对自然语言建模
    Python "爬虫"出发前的装备之一正则表达式
    深度学习环境搭建入门环境搭建(pytorch版本)
    09.webpack5搭建vue环境(三)
    07.智慧商城——商品详情页、加入购物车、拦截器封装token
    多任务学习算法在推荐系统中的应用
    解读 FlashAttention
    JAVA集合(一)Collection接口
    像你这么优秀的测试工程师,为什么连面试都过不了?
  • 原文地址:https://blog.csdn.net/zhaoyang314/article/details/136373931