Godot 游戏引擎学习 第六课

Godot 游戏引擎学习 第六课

December 24, 2020

今天学的是如何添加怪物,并让怪物移动。

这节课 UP 讲了好几个方面的东西,有点杂,我这里根据个人理解做点学习笔记。

一、 怪物对象化

怪物是有很多种类的,而且怪物具有很多相同过的特性和不同的特性。为了以后好开发将一些共有属性提取出来做个父类是很有必要的。

这里 UP 就讲了一个继承脚本、场景的的东西,

先建立一个 Enemy 场景,设定一些共有节点

image-20201224210915823

建立对应的脚本,独立出共有属性

extends KinematicBody2D

# 共有属性重力
const gravity = 2000
# 因为要处理精灵方向的问题,所以做个预载变量。
onready var sprite: Sprite = $Sprite

然后点击编辑器菜单上的场景按钮,选择建立继承场景,父场景选择 Enemy.tscn,就会实例化出一个具有父场景所有属性的子场景,命名为 Slime.tscn。然后在这个里面设置 Slime 的特有属性,如碰撞体、精灵等。

脚本也同理,选中父节点,按右键选择打开脚本,UP 住说这是一个翻译错误,弹幕里也有大佬说这里术语原文是 Extend,呼应脚本里的 extends 关键词 , 我的理解其实就是父类脚本。新建好的脚本如下,代码我都写了备注就不在这里说了:

extends "res://src/enemies/Enemy.gd"

# 定义枚举,用于指定怪物朝向
enum Direction { Left = -1, Right = 1 }

# 定义移动速度和加速度,这个其实可以提取出来到父脚本去
const max_speed = 50
const acceleration = max_speed / 2

# 这里用到一个新东西:export变量,
# 一个变量加上export就成为外部变量了,可以通过编辑器设置他的值
# 括号里是指定这个变量的展示类型为上面设置的枚举
export(Direction) var direction = Direction.Left
# 速度
var velocity = Vector2.ZERO

func _physics_process(_delta):
	# 每一个物理帧检查怪物是不是碰到墙了
	var was_on_wall = is_on_wall()
	# 怪物不会跳,所以把他们吸附在地板上
	var snap = Vector2.DOWN * 16
	# 进行移动操作
	velocity = move_and_slide_with_snap(velocity, snap, Vector2.UP)

	# 如果碰到墙就转身
	if is_on_wall() and not was_on_wall:
		direction *= -1


func _process(delta):
	# 进行加速移动
	velocity.x = move_toward(velocity.x, max_speed * direction, acceleration * delta)
	velocity.y += gravity * delta

	# 进行转身
	sprite.flip_h = velocity.x > 0
	

这里 UP 主还说了一个 BUG,就是在更改了父场景的层归属后子场景不会更新,实例化出来的场景也不会更新,要把主场景关掉后重开就正常了。


说到精灵这里插一下,有些精灵素材是整合在一起的,所以整个图片会很大,像下图这样,所以我们设置好精灵素材后,启用 Region,然后可以先设置 Scale 把图片缩放到合适的大小,再设置 v 和 hframes,开启网格吸附,将步长设置成每一帧图片的大小,再框选图片就好了。

image-20201224211435566


好了,建立好怪物之后就可以实例化到场景里 UP 又说了昨天碰撞层的问题,现在状态是怪物碰到玩家就不动了,他想让怪物穿过玩家,所以,我们再命名一个怪物层,把实例化的 Slime 的碰撞和遮罩层分配到这个里面。

这样因为玩家和怪物不在一个层,所以碰不到怪物,但是怪物们再一个层,他们会互相碰撞。

image-20201224213251529

怪物笼

其实就是怪物刷新点啦,让怪物定时刷新的功能。

第六课 怪物笼

我们可以利用 Position2D 的和 Timer 节点实现怪物笼效果,第一个用于定位怪物刷新出来的位置,第二个控制多常时间刷新。

(这里说下,所有的信号时间必须在编辑器里注册,不然直接写进代码是没有效果的,我前几天说的那个可以手动写信号的话暂时先放一边,目前了解到的暂时不可以)

代码如下:

extends Position2D

# 让外部可以设置打包场景
export var enemy_scene: PackedScene
# 刷新速度
export var interval = 1.0

#定时器
onready var timer =$Timer

func _ready():
	# 如果设置了打包场景
	if enemy_scene:
		# 开始计时
		timer.start(interval)

# 每进行一次循环完毕的信号
func _on_Timer_timeout():
	# 根据设置的打包场景实例化新的怪物对象
	var enemy: Node2D = enemy_scene.instance()
	# 将实例化后的怪物添加到父节点中
	get_parent().add_child(enemy)
	#并将位置设置为Position的节点
	enemy.global_position = global_position;

信号的使用和攻击、受伤、攻击特效

这里 UP 提出了攻击盒子和伤害盒子的概念,我因为不太理解的缘故中间代码和设置都整错了一部分,一直不太对,最后把添加的相关场景和代码全部删掉之后重写才正常实现。

攻击盒子和伤害盒子和场景内角色和怪物都有的一个特性,所以为了复用代码就单独做了两个场景,Hitbox,Hurtbox

这里我看的时候理解混乱了好一会儿,后来算是明白了,其实一个攻击或受伤的步骤分为两步,比如攻击

  1. 我感觉到我打到你了
  2. 你感受到我打到你了,你叫了出来。

其实理解后就很简单了,我的攻击碰到了你的受伤盒子,触发信号,我先执行攻击你的函数,再通知你受伤的信号,执行受伤的函数。

这里制作了踩中怪物是的攻击特效和怪物的受伤特效

# Player.gd > 省略前面的代码
# 如果被怪物攻击了,怪物那边的攻击函数就会调用角色的受伤信号
func _on_Hurtbox_hurt():
	# 重新加载当前场景。
	get_tree().reload_current_scene()

# 如果踩中敌人了则自己处理自己的攻击过程,并通知对方受伤了。
func _on_Hitbox_hit():
	velocity.y = -jump_force / 2
	
# Enemy.gd > 省略了一部分代码
# 编写信号链接函数
func _on_Hurtbox_area_entered(hitbox):
    # 如果检测到碰着,也就是受伤
    # 首先调用受伤的信号
    emit_signal("hurt")
    # 再调用攻击信号
    hitbox.emit_signal("hit")

大概就是这样把,感觉自己也没讲的很明白。

总体代码如下:

extends Area2D

# 自定义攻击信号,方便实例后调用
signal hit

# 检测攻击状态
func _on_Hitbox_area_entered(hurtbox):
    # 攻击到敌人先调用攻击信号
    emit_signal("hit")
    # 再调用对方的受伤信号
    hurtbox.emit_signal("hurt")
extends Area2D

# 自定义受伤信号,用于实例化调用
signal hurt

# 编写信号链接函数
func _on_Hurtbox_area_entered(hitbox):
    # 如果检测到碰着,也就是受伤
    # 首先调用受伤的信号
    emit_signal("hurt")
    # 再调用攻击信号
    hitbox.emit_signal("hit")

学习成果

第六课 ALL

加入评论