Godot游戏练习01-第10节-组件化,玩家受伤,YSort,和一点思考

0 阅读6分钟

今天将"伤害"与"受伤"功能组件化, 并且在玩家身上实现受伤机制, 同时也能体会组件化的好处, 最后实现了Player与Enemy之间的YSort效果

本次实现的内容主要体现在组件化与复用, 可观察的内容并不多

看看效果

之前的实现中, 无论Player与Enemy处于什么样的相对位置, 都是Enemy覆盖在上面, 看起来没有层次感, YSort实现Enemy和Player的动态排序显示

YSort效果: 观察Player走在Enemy上方和走在Enemy下方时的显示层次

并且实现了Player的受伤, 生命值耗尽后的死亡机制, 目前仅打印日志 (Player碰到Enemy受伤多次后提示"Died")

动画6.gif

实现过程

伤害与受伤的组件化

在之前的课程中, 通过在Bullet和Enemy场景上添加Area2D, 实现伤害和受伤的功能

但这两个功能具有很强的复用性

  • 子弹只具有伤害功能
  • 敌人具有伤害(玩家)功能, 还具有受伤(来自子弹)功能
  • 玩家只具有受伤(来自敌人)功能

其中, 受伤功能与健康组件(HealthComponent)是依赖关系, 具备HealthComponent才可以受伤

之前已经实现了HealthComponent, 可以按照相同方式实现HurtboxComponent(受伤组件)和HitboxComponent(伤害组件)

这两个组件都是一个Area2D节点, 没有默认的CollisionShape2D组件, 因为各场景的碰撞形状不同, 需要单独定义, 没有必要复用

HurtboxComponent源码

class_name HurtboxComponent
extends Area2D

@export var health_component: HealthComponent


func _ready() -> void:
	if is_multiplayer_authority():
		area_entered.connect(_on_area_entered)
	else:
		process_mode = Node.PROCESS_MODE_DISABLED


func take_damage(damage: int) -> void:
	health_component.take_damage(damage)


func _on_area_entered(area: Area2D) -> void:
	if not area is HitboxComponent:
		return
	var hitbox := area as HitboxComponent
	take_damage(hitbox.damage)
	hitbox.register_hit(self)

该组件处理受伤事件, 将伤害传递给外部的HealthComponent组件, 并反向通知HitboxComponent击中事件, 仅在服务端进行物理判断和逻辑处理

HitboxComponent源码如下

class_name HitboxComponent
extends Area2D

signal hit(hurtbox: HurtboxComponent)

var damage: int = 1


func _ready() -> void:
	if not is_multiplayer_authority():
		process_mode = Node.PROCESS_MODE_DISABLED


func register_hit(hurtbox: HurtboxComponent) -> void:
	hit.emit(hurtbox)

处理击中事件, 并向外传递信号

组件化的哲学

组件化将一个个单独的功能拆分开来, 这符合Godot的思想哲学, 组件化具有以下优点

  1. 每个组件的源码都不多, 仅完成一件事, 职责单一简单

  2. 复杂场景需要什么功能, 不需要什么功能可以方便定制, 只需要添加/删除对应功能的组件即可

    比如Player需要添加血量/死亡, 以及受伤功能, 在上述组件构建之后, 直接添加一个HealthComponent与HurtboxComponent组件即可

    再稍微调整下层级配置, 处理生命值耗尽的信号(目前仅打印died日志)就行了

  3. 防止复杂场景的脚本膨胀, 逻辑爆炸

    从Player新增血量和受伤功能也能看出, Player新增功能基本没有新增很多源码, 因为对应逻辑都分散在对应组件中了, 主场景脚本处理一下信号就行

  4. 通过信号交互, 逻辑解耦

    比如HitboxComponent, 对外提供一个hit(hurtbox: HurtboxComponent)信号, 表示已经"伤害"另一个实体, 但是这个信号是谁来处理, 有没有人处理, 本组件都不关心, 我只是通知一下, 后面都不用管了

    对于子弹(Bullet)场景, 它需要关心这件事, 因为子弹击中敌人后, 自身需要销毁, 因此它需要监听该信号做处理

    对于敌人(Enemy)场景, 它不关心这件事, 伤害玩家后它不需要额外的逻辑处理, 因此它不用管这个信号

YSort

YSort的效果: 当Player的位置y值大于Enemy的位置y值, 则显示在Enemy上方; 若位置y值小于Enemy的位置y值, 则显示在Enemy下方

YSort的开启: 当某个节点开启YSortEnabled之后, 其所有直接子节点在同一个Y-Sort空间中进行Y排序渲染

也就是说, 节点的YSortEnabled开关, 影响的是直接子节点, 而不是影响自身, 要牢记这个规则

其实完整的Y-Sort规则比较绕, 如果涉及到父子节点同时开启Y-Sort很难理解, 但是当前的理解已经够完成开发内容, 以后要使用再继续深究

一点思考

以前的我想写程序或者想做游戏时, 总觉得要先看一个完整详细的系统教程, 要了解所有细节, 准备完全之后再动手开发

觉得这样有掌控感, 并且我懂得细节, 才能按照最佳实践去架构去实现

但是往往都是过分纠结于细节理论, 耽误太多时间, 而完不成任何内容

现在看, 以前的我都是"追求完美"的心态, 实现都妄想一开始就最好, 但实际上是我能完整跟着一个教程做完, 完整写下一个小项目都很不容易了, 如果碰到一点点不懂的内容就去不断深究, 想弄懂这背后所有的设计与实现细节, 很可能就迷失自我了

记得印象最深的一个是以前尝试做一个小demo时, 觉得人物受击之后, 应该有受击动画才比较好看, 就去研究2D受击动画实现, 了解到是用Shader实现, 于是去看了受击动画的Shader源码, 完全没看懂, 觉得这个要啃下来, 做游戏怎么能不懂Shader, 以后还要做特效, 还要代替粒子系统优化性能, 学! 必须学透, 然后, 我不知道自己迷失到哪里去了, Shader学了多少不知道, demo肯定是忘记了

要按照以前的思想, 做游戏怎么能不懂美术/物理/粒子/音效/数值/机制设计......, 碰到每一个不了解的细节或者方向, 都妄想掌控, 然后以完美的姿态来继续任务, 不是没有可能, 只是过于依赖自己本就稀缺的"意志力", 我忘记了自己只是一个普通人, 不是那种真的能一口气啃完所有内容还能记得回来战斗的完美六边形战士

不要高估自己的意志力, 不要给自己的实际行动增加阻力

我只能尽量保持手头项目/demo的完成, 如果碰到不懂的, 卡住我的内容, 那就以最简单最快的方式了解必要内容, 获取必要的资源, 之后立即回到任务上, 避免过度扩散, 避免给自己增加行动阻力

虽然不了解细节会写出烂代码, 不懂美术会画出抽象作品, 但没关系, 只要我还在保持做游戏, 都会越来越好, 或者找到解决问题的方法

代码烂没关系, 等到我后期真的需要500个敌人同屏显示, 卡成PPT的时候再回来找问题做优化

美术烂没关系, 我没有那种艺术天赋, 不能做出靠画风吸引人的作品, 但是我可以琢磨机制玩法, 等真的实现了一个不错的Demo时再去找美术资源, 或者在必要的时候学习绘制需要的风格素材

保持自己在桌上, 才是最重要的