今天,我主要学习主人公的血量显示,在屏幕的左上角,会显示主人公的头像,后面会显示血量进度条,当主人公受到伤害时,血量会实时变动,显示绿色血条减少,后是红色血量逐渐减少到正确位置,效果如下:
再开始今天的学习之前,修改以前的一些Bug,在敌人攻击和受伤,主人公的攻击和受伤学习中又许多Bug,主要是因为我把需要执行一次的状态代码和需要执行一段时间的代码全都写到_physics_process函数的原因,所以我将需要执行一次的代码加到state状态的set(v):函数内执行一次;把需要多次执行的代码加到了_physics_process函数内。
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
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
单击新建场景按钮,选择其它节点,在创建Node对话框中,选择HBoxContainer节点,单击创建按钮,创建新的场景,如下:
将新场景保存到Scenes文件夹下,命名为:StatusUI。
将根节点重新命名为StatusPanel,检查器中将Transform->Size重置为0,0。
首先为根节点添加PanelContainer子节点,再为PanelContainer添加TextureRect子节点,显示头像。为根节点添加TextureProgressBar子节点,显示血量,最终效果如下:
最后将PanelContainer命名为HeadBox;将TextureRect命名为Head;将TextureProgressBar命名为HeadthBar。
将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,如下:
切换到Head节点,在检查器中Texture选择AtlasTexture图集纹理,将主人公的Player.png图片拖入到Atlas属性,如下:
在检查器中选择Region,单击下面的编辑区域按钮,在弹出的区域编辑框中选择像素吸附,选取主人公正面头像,然后关闭。
head检查器中Stretch Mode属性选择 Keep Aspect Centered,表示图像保持比例缩放,居中。如下:
最后的头像效果如下:
切换到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属性选择居中收缩。如下:
最后显示效果如下:
复制HealthBar命名为HealthBarRead,并将其设为HealthBar的子节点,如下所示:
在其检查器中Textures属性重置Over属性,Progress属性选择唯一化,然后从新编辑区域,选择红色血条,效果如下:
然后在编辑器上方控制节点锚点和偏移值的预设中选择整个矩形,这样红色血量和绿色血量最终重合,如下:
然后在检查器中找到Visibility->Show Behind Parent并启用,这样绿色血量将会在红色血量的上方。
通过以上步骤所有节点都已布置完成,先忙开始编写代码。
无论是敌人还是主人公都有一些相同状态,如血量,我把这些相同的状态定义成一个类,需要的时候继承就好了。首先切换到脚本,单击文件新建脚本,新建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() #发出信号
第一行代码主要把该脚本命名为Stats类,这样的目的是,把所有跟敌人或者主人公有关状态的量抽象一个类,复用。
第二行代码说明该类继承了Node节点,Node节点的方法和属性都可以使用。
第三行定义了一个信号,表示当血量发生变化时,出现该信号。
第四行定义了最大血量,默认是100,@export表示该变量可以在编辑器中看到和修改。
第五行定义了当前血量,@onready表示该变量会在Node节点就绪状态时赋值,默认赋值为最大血量。
第六行表示当给health变量赋值时,发生。
第七行表示给health复制时,取值范围为0到max_health,当超过这个范围会自动变换到这个范围。
第八行、第九行表示如果复完值就跳出该方法。
第十行表示把用户输入的值付给health。
第十一行表示复完值后发出血量变化信号。
切换到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)
这段代码中第二行定义了一个状态变量,继承Stats。
第三至四行代码表示获取绿色血量和红色血量节点。
_ready函数中的stats.health_changed.connect(update_health)代码表示血量变化信号发生后会调用update_health函数。
update_health自定义函数表示血量发生变化后血量显示进度条适时变化。该函数前两行代码对绿色血量进行调整,后一行代码表示红色血量变化会在0.3秒内完成,并显示间补动画,就是红色血量有个减少过程,绿色血量直接发生变化。
这里面有个引擎自带函数tween_propert方法解释一下。create_tween()表示创建一个间补动画,tween_property方法第一个参数表示要发生变化的对象,第二个参数表示该对象的哪个属性发生变化,第三个参数表示该属性到达的最后状态,第四个参数表示达到最后状态需要的持续时间。
切换到Player场景,选择根节点,然后点击添加节点按钮,选择Stats,我们自己编写的类,如下:
单击Stats节点,在其检查器中,我们会看到我们编写该类时,暴漏的max_health的变量,我们可以在检查器中进行修改。
切换到Player场景,选择根节点,单击实例化子场景按钮,在实例化子场景中选择StatusUI点击打开,就把StatusUI场景加入到Player场景内了,重名为PlayerStatus,然后适当调整位置。
在PlayerStatus节点单击右键选择重设父节点为新节点,在对话框中选择CanvasLayer节点,这样的目的是把主人公的血条显示固定在一个位置。操作如下:
调整PlayerStatus节点位置到屏幕的左上角,如下:
在检查器中将Stats选择Stats,操作如下:
ok,这样就应该好了,看一下最终效果:
注意观看左上角的血量变化。今天就到这,下节见,同学们。