惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
F
Fortinet All Blogs
U
Unit 42
F
Full Disclosure
雷峰网
雷峰网
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
罗磊的独立博客
D
DataBreaches.Net
C
Check Point Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
O
OpenAI News
C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
S
Security @ Cisco Blogs
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
SegmentFault 最新的问题
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Hacker News
The Hacker News
Webroot Blog
Webroot Blog
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
酷 壳 – CoolShell
酷 壳 – CoolShell
N
News | PayPal Newsroom
P
Proofpoint News Feed
B
Blog RSS Feed
MongoDB | Blog
MongoDB | Blog
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
Google Online Security Blog
Google Online Security Blog
H
Help Net Security
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
M
MIT News - Artificial intelligence
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
MyScale Blog
MyScale Blog
腾讯CDC

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
Godot入门到弃坑:再见敌人
cameLcAsE · 2025-01-08 · via 机核

之前在2D部分做过一个简单的敌人。这次在3D中又来做一个简单的敌人。

敌人的表示

我们先来实现一些基本的功能,暂时不关心它的具体视觉表现。所以我简单地用一个胶囊表示就行了。当然你也可以导入一个3D模型什么的。

当然,如果你像我一样懒,只给了一个简单的MeshInstance3D占位的话,那么记得再给它个什么东西表示他的前方。不然我们不好调试。

根节点依然用方便的CharacterBody3D,但是和玩家不同,就目前来说不需要给它摄像机。添加脚本即可。

由于我们不需要控制敌人,所以脚本模板中只需要留下重力相关的代码即可。

看向玩家

这里说的行为其实算是敌人AI的一部分。要实现敌人的AI,其实没那么简单。我们也没法在一篇文章中说完(可能“永远”也说不完)。我们先做一个看向玩家的功能。例如我们想要实现,“玩家离敌人只有5m的时候,就看向玩家”。

首先的问题是,如何判断和玩家的距离?

如果一定要计算两点之间的距离,我们应该使用Vector3distance_to或者distance_squared_to来求得(毫无疑问Vector2也有对应的方法)。这是两个实例方法。后者会比前者快一些,后者计算的是没有开平方的距离。

两点距离是如何求得的?不要忘了,我们把点也视作向量。“AB间的距离”就是两点之间拉一个向量的长度。这个向量就是两点坐标之差。由于向量是有方向的,所以A-B得到的其实是从B指向A的向量。这里可能会显得反直觉,是需要注意的地方。当然,要计算的“距离”是一个标量,所以这里其实也不那么重要。

要推导出这个距离怎么算其实视角有很多,但是这里我肯定说中小学生都能理解的。在2D空间中,想象一条斜着的向量。这个向量在X轴上的投影(v),v末端和P的连线,构成了一个三角形。这个三角形斜边的长度——勾股定理知道吧?那你就会求向量长度了。两条直角边的长度恰好就是向量的两个分量。公式我就懒得打了,简单用伪代码表示就是sqrt(x*x + y*y),三维空间中也是一样,只不过是三个分量的平方和。

这就是求准确距离需要开方的原因,如果你不需要那么精确,那么就直接用没开方的结果即可。由于我们这里明确知道是“5m以内”,那么直接position.distance_squared_to(player.position) < 25即可。当然这里你需要用一种办法在场景中找到player,然后每帧做这个运算去检查。

然而,这里实际上不用真的去算这个距离。还记得Area吗?一个可以用来检查是否有可碰撞对象的没有实体的区域。在3D中我们同样有Area3D来做这个事情。为Enemy套上一个Area3D,给它一个shape然后调整到适当大小,连上body_entered即可检查玩家是否进入!具体操作不赘述。

注意,实际上这里需要调整碰撞层让它不检查enemy自己,因为CharacterBody3D本身也要参与碰撞的。这里请自行调整或者偷懒。

接下来要实现“看向玩家”。直接说结论,需要使用Transform3Dlooking_at方法。名字已经很直白了,就是要让这个transform“看向”某个点。第二个参数指定“上方”,用这个方向确定这个transform会绕哪个方向转过去。默认就是Y轴正方向(通常意义的上方),所以调用时可以省略。

现在Area3Dbody_entered信号的处理函数大致是这样:

func _on_area_3d_body_entered(body: Node3D) -> void: if body is not FpsCharacter: return transform = transform.looking_at(body.position)

为什么需要给transform赋值?因为looking_at方法并不直接修改transform本身,它会返回一个新的Transform3D实例。

现在玩家进入Enemy的area时,敌人会一瞬间看向玩家:

有被吓到。如果这是你想要的效果,那也行。但是你可能期望可以看到敌人转身,哪怕转身很快也要能看到这个过程感觉才真实。当然,这里也不是bug,因为你代码就是这么写的。我们就是在某一帧里直接设置了transform,所以这个变换肯定也是一瞬间完成。

如何解决?说白了,要达到这种效果,我们需要让这transform的这种变化,不在一帧中完成。但是我们只有起始值和最终值,如何获得在中间这些帧的值呢?

话都说到这个份上了,肯定就是要插值。Transform3D是一个相对复杂的数据结构,所以一般情况下不可能我们自己去插值。它提供了一个interpolate_with方法来插值。第一个参数就是目标值,第二个参数为一个取值为0到1的值,就是说插值到哪里了。

先定义一堆变量:

var turning = false var turn_alpha = 0 var turn_time = 1 var turned_time = 0 var target_transform: Transform3D

由于我们要在Area的信号响应函数中确定何时开始插值,因此目标transform要在这里确定,但是要在其它函数中使用。在处理body_entered的函数中,我们不直接设置transform,只是确定目标并指出正在转向:

func _on_area_3d_body_entered(body: Node3D) -> void: # ... if turning: return target_transform = transform.looking_at(body.position) turning = true

_physics_process中,我们只有在turning时才处理插值:

func _physics_process(delta: float) -> void: # ... if turning: turned_time += delta turn_alpha = clamp(turned_time/turn_time, 0, 1) transform = transform.interpolate_with(target_transform, turn_alpha) if transform.is_equal_approx(target_transform): turning = false turned_time = 0 turn_alpha = 0 move_and_slide()

实际上也不复杂。turn_time是一个我们自行设计的值,也就是整个转身过程需要多长时间。turned_time指的是已经转了多少时间了,每次进入_physics_process时就给它加上时间。turn_alpha就是两者之比,也就是插值进度,它会作为interpolate_with的参数传入。当然这个中间变量是没有必要的,这里为了清楚写一下(题外话,如果编译器足够智能,可能就算这么写这个变量也会被优化掉)。

如果插值后发现当前transform已经和目标差不多了,那么就重置这些状态。

为什么不用等号?因为由于浮点数精度等问题,很难确定两者精确相等。如果在这里用等号,你会发现几乎不可能碰到两者相等的时候!但是!这里实际上不用比较transform,我们直接比较时间即可!简化后的代码:

if turning: turned_time += delta transform = transform.interpolate_with(target_transform, clamp(turned_time/turn_time, 0, 1)) if turned_time >= turn_time: turning = false turned_time = 0

为了这个简单的插值操作我们定义了大量状态变量,如果我们需要控制很多插值,可能还要定义很多变量。

学完手动挡,我们还是开自动挡吧。

别忘了,我们还有Tween可以用。如果不需要那么精细地控制,我们可以直接用Tween来插值。用tween的话,我们就可以不用自己来计算时间和插值用的alpha(weight),很多代码又可以省掉:

func _on_area_3d_body_entered(body: Node3D) -> void: # ... if turning: return target_transform = transform.looking_at(body.position) turning = true var tween = create_tween() tween.tween_property(self, "transform", target_transform, turn_time) tween.finished.connect(func(): turning = false)

当然这里保留了turning用以判断是否还在转身过程中。这里相关的行为可以根据游戏设计调整。当然,手动挡已经告诉你怎么开了,当你真的需要手动挡的时候,你应该知道怎么做。

怎么杀死敌人?

在2D部分的文章中,为了实现玩家的HP特性,我们直接在玩家的脚本中定义了一个属性。同时由于那时的敌人不需要被击杀,所以它也没有HP。很多FPS中,玩家和敌人都会被击杀,所以双方一般都是有HP属性的。当然,对于玩家和敌人来说,可能受到伤害的代码都是类似的。

我们先设想一下如何实现对敌人进行攻击。

我们已经实现了通过计算射线和物体是否相交来判断能否击中某物。如果可以击中,我们就可以获得一个代表被击中物的collider,其类型为Node3D,我们知道,这是非常抽象的类型,它可以代表任意3D节点。

为了对被击中物施加伤害,我们首先要知道它是否可以接受伤害

我们如何知道一个节点是否可以受到伤害呢?根据我们对GDScript的了解,我们可以想到可以对它进行类型检查。假如我们在代表敌人的类上定义了一个hp属性,如果这个东西是敌人,那么肯定就可以操作它的hp,亦可以调用其它相关方法:

# 只作为例子展示 func fire(): # ... var result = get_world_3d().direct_space_state.intersect_ray(parameters) if not result: return var collider = result.collider if collider is Enemy: var e = collider as Enemy e.apply_damage(1) # ... # ...

随着开发的进行,我们可能有多种不同的敌人,需要实现类似的处理伤害的功能,可以预见这些代码是雷同的。为了减少这种不必要的复制粘贴,我们可以为各种敌人定义一个共有的基类。这样在应用伤害时可以不用关心具体敌人。

这样的通用敌人基类的基类可能是CharacterBody3D。但是,有没有可能我们的敌人不需要、甚至不应该继承CharacterBody3D呢?比如它可能是一个载具或者其它什么东西,它们不利用CharacterBody3D实现。

还有一种可能,一个东西会对攻击做出反应,但是它并不会被打死,因此它不需要一个hp什么的,它怎么能被当成敌人的子类呢?我们甚至不能操作它的hp!

你可能会想,我们用最抽象的Node来表示一种“可以被打”的东西,以此为基类,再构造一些略为具体的基类,最后再是真正直接使用的具体节点类。

想法不错。但是如果我们需要抽象出不止一种行为呢?比如载具既可以被攻击,也可以被“开”。如果它继承了一个可以被攻击的基类,那么它就不能再去继承另一个被开的基类了。

GDScript以及很多基于类的面向对象编程语言实际上都主动抛弃了多继承(可以同时有多个基类)来避免多重继承带来的一些问题。所以上述设想在GDScript中是没法实现的。

组合(可能)优于继承

那怎么办呢?既然我们知道可以把某种能力剥离出来,那么我们也就可以把它包装成一个单独的节点。Godot的节点树设计天然地让我们可以让一个节点作为根节点,让它带有若干子节点。这样一来这个根节点就可以利用这些子节点的能力来实现各种功能。并且,HP相关的数据、状态本身是不需要和视觉呈现交互的。把它(比如叫它HpComponent)作为一个Node的派生类来实现也是合情合理的:

# HpComponent示例 extends Node class_name HpComponent ​ @export var MAX_HP: int = 100 var hp: int = MAX_HP ​ signal damaged() signal died(node: Node3D) ​ func apply_damage(d: int) -> int: hp = clamp(hp - d, 0, MAX_HP) damaged.emit() if hp == 0: died.emit(get_parent()) return hp

当然,如此一来我们就无法直接从一个节点的类型上得到它关于这一能力的信息,因为现在和接受伤害相关的能力有关的属性和方法都在这个单独的子节点上。如果使用这种模式,为了检测一个节点“是否可以接受伤害”,我们需要做的是检查它身上是否有某个节点。

在Godot中可以使用get_nodeget_node_or_null来获得对节点树中某个节点的引用。这两个方法唯一的区别就是前者在找不到指定节点时会发生错误并返回null,后者不会报错。这个节点本质上就等于用GDScript的语法糖来获得节点,就像我们在脚本中做过的那样。只不过,在我们需要检查来自另一个场景或节点身上有没有某个节点时,我们就要通过这个方法来操作:

func fire(): # ... if not result.collider: return var collider = result.collider var hp_component = collider.get_node_or_null("HpComponent") if hp_component: hp_component.apply_damage(1) # ...

由于我们需要通过一个类似于字符串的参数来获得节点,所以说并不能保证这个过程一定成功。虽然说这种实现具体功能的节点我们可能一般就作为根节点的直接子节点(第一层),但是你必须要在开发过程中和自己以及所有开发参与者达成一致,不然的话可能你在某些能被伤害的节点上用HpComponent可以找到这样一个HpComponent节点,有些东西上又找不到了。另外,你还有可能给它改了名字或者打错字了什么的。所以很多时候用字符串当参数的约束力是很弱的,类型系统相比之下要安全很多。

当然这种模式的好处是显而易见的。我们可以在任意场景中加入这个HpComponent,不用管它们的继承关系,也不用关心具体类型,并且可以组合其它任意的功能。

当然,还有个问题没有回答,那就是万一这个东西根本不需要一个HP属性呢?

何必这么麻烦?

不要忘了,GDScript是一个足够动态的编程语言。我根本不需要关心你到底是哪种节点,甚至不需要关心你有没有某个子节点——只要你有叫某某的属性或者方法,我就可以调用之!

你可以在所有可以受伤害的节点上都定义一个apply_damage方法,并且使用统一的参数列表。这样一来,不管被射线碰到的节点是什么,也不管它如何处理伤害,都可以通过这个方法来处理。

但是,不要忘了,如果这个被射线碰到的东西没有这个方法,那么就会报错。如果要利用动态语言的这一特性来实现,还是那句话,事先约定,保持一致。

针对这一问题,GDScript中可以通过Objecthas_method方法来检查一个节点(提醒:Node是Object的子类)上是否有某个方法。但是,在游戏运行时调用这种方法比直接调用会慢得多。

利用这种动态语言的特性来实现还有一个坏处就是,就算你的代码是完全正确的,由于类型不确定,编辑器无法给你提供代码补全的提示,出现打字错误的可能性更高。

因此,为了方便起见,我文章中的示例代码就定义apply_damage方法,然后直接动态调用它就行了。

当然,如果我用C#来编写脚本,我不会这样做。但是如果你一定要问可不可以,那其实还是可以。但是那样就白瞎了C#带来的类型安全。

补充知识:这种仗,它们会怎么打?

这种对具体能力的抽象,对实现和接口的剥离,在“正经的”编程语言中一般就称为interface(接口),或者用苹果更喜欢的说法protocol(协议)表示。

这种抽象的接口定义了一系列方法(某些语言也支持定义属性),要求接口实现者(某个类),和接口使用者(通过接口操作某个对象的代码)达成一种协议。以Godot和Unity等引擎支持的C#语言为例:

interface IDamageable { void ApplyDamage(int d); }

这样一来,承诺实现该接口的类就必须提供一个相应的方法定义,且任意类型均可实现该接口。射线检测到实现了这个接口的节点时,就必然能调用这个接口中定义的方法。这种确定性是C#作为一种(在绝大多数情况下都很)静态的编程语言的类型系统能够保证的。

很多现代的基于类的面向对象编程语言都是只允许继承一个类,但是可以实现多个接口。例如等价的Enemy可以去实现这个接口:

public partial class Enemy: CharacterBody3D, IDamageable { # ... [Export] public int MaxHp {set;get;} = 100; public int Hp {set; get;} = MaxHp; public void ApplyDamage(int d) { Hp -= d; } }

假设前面的fire方法被转换成了等价的C#代码:

void Fire() { // ... var collider = result["collider"]; if (collider is IDamageable d) { d.ApplyDamage(1); } // ... }

collider is IDamageable d是C#模式匹配语法的一种用法。如果发现collider实现了IDamageable接口,那么就将其所指对象绑定到局部变量d上,便于在下方的代码块中使用。这里通过d只能调用接口中定义的方法。一般有接口概念的编程语言都可以在需要类型的地方写上接口,因此可以用接口类型的变量来引用对象。

如你所见,通过接口调用方法的代码是不用关心具体类型是如何实现这个方法的。这些具体的类型上你可以背上一个类似于HpComponent的节点来实现具体功能,也可以完全不用——我根本不关心好吧。

Unity虽然也用C#,但是它更强调组合。一个GameObject身上的各个组件构成了这个东西的能力。不过也可以通过一些手段让一个组件放到某个东西身上时自动给它加一个它依赖的组件,一定程度上可以避免找不到某个组件的问题。Unity的射线检测结果可以拿到的也是一个GameObject(类似于Node2D和Node3D,因为GameObject有transform,但是Node没有),可以通过调用获得具体类型的组件。当然,还是因为有C#在,你可以定义一个类似的类层次结构,定义一个大的Enemy组件,让它实现接口,也可以用GetComponent<IDamageable>来获得具体的组件引用。

Unreal的编程语言是(被Epic加了很多黑魔法的)C++。C++其实是支持多重继承的,但是Unreal的开发者选择用黑魔法实现了一种类似于“现代面向对象语言”的接口。尽管Unreal不如Unity那么“强调”组合,但是Unreal中也存在组件的概念,再加上接口,因此实际上也可以用上述的各种方法来解决问题。

杀敌(略)

基本的逻辑对于从头看到这里的读者来说应该很简单,建议先自己思考一下。

hp没了直接queue_free就行了。

朝玩家方向移动

可是不动的敌人没什么意思。

我们这里的场景和敌人逻辑都很简单。事实上,这个功能涉及很复杂的情况。比如场景很复杂,敌人和玩家有高度差,简单的距离计算是找不到路的。后续会介绍一种“更先进”的做法。

这里的敌人是用CharacterBody实现的,玩家逼近后,我们直接修改速度。如果你的敌人响应玩家进入area的信号的函数里面,转向是用Tween来实现的,那么可以直接这样写:

# ... tween.finished.connect(func(): turning = false velocity = (body.position - position).normalized() * 0.5 # TODO: 这里应该加上追逐玩家的代码 )

正如前面提到的那样,两点坐标相减就是一个向量。这里用normalized方法得到规范化之后的向量,然后乘以我们设计的速度(speed)即可。

如果你的逻辑不复杂,这样直接写一个lambda表达式即可(快速回忆:形如func(): pass这样的、匿名的简单函数,这样的表达式直接返回一个函数或者说Callable)。还要注意就是这样的lambda表达式也可以有多行代码,不需要限制自己只能写一行代码。当然,当这里的代码真的复杂起来之后,正确的做法是提取一个单独的函数。