使用Unity制作游戏AI
本文由独立游戏工作室Synnaxium Studio介绍游戏AI的概念和开发方法。本文中所有内容都是他们在开发《Radiant Blade》游戏的原型阶段所积累的经验。
下面是《Radiant Blade》的演示画面。
使用游戏AI的原因
首先,我们要思考为什么要给游戏添加AI?
长期以来,我们都在幻想着为游戏开发令人惊奇的AI,让AI给玩家带来印象深刻的体验。这种AI可以预料到玩家的每一个操作,几乎无法被打败。但说实话,这种AI毫无对抗的乐趣。
值得玩家去玩的游戏应该是玩家可以获得乐趣的游戏。因此,AI必须可以和玩家旗鼓相当。AI可以作为伙伴,让玩家通过特别的方法进行交互。
显然,只有乐趣的游戏不会是一个优秀的游戏。游戏也必须有炫酷的机制,深刻的含义以及精美的外观。但对AI而言,我们希望AI具有娱乐性,因此我们要进一步缩小这个概念的范围。
游戏设计
游戏中的娱乐性是什么?我们的开发团队花了一些时间思考这个问题,结论可以总结为一个词:学习,具备娱乐性的游戏是玩家可以从中学习和利用知识的游戏。
娱乐性源于小小的好奇心,在玩家看到新事物时,好奇心会占据玩家的头脑,并会不断增长,直到玩家完全理解这项新事物。也就是说,具有娱乐性的AI必须是可以被玩家学习的。
这个简单的概念形成了所有游戏中AI的广泛解读,包括:《超级玛丽》、《毁灭战士》、《魔兽世界》和《以撒的结合》。如果分析这些游戏的AI,我们会发现它们都是可以预测的。
由于加入了一些随机元素,这些游戏AI不是完全固定不变的,但仍有预测的可能。这样又出现了另一个问题:如何制作出可预测的游戏AI?
答案很简单:使用状态机。
状态机
状态机是包含状态和过渡的数学工具。
在确定性状态机中,我们会处于一个特定状态,在移动时,我们会随着其中一个可用过渡转变到新状态。过渡可能会受到条件限制,例如:只有在拥有特定法术时,AI才可以到达指定状态。
状态机的优点是:它们具有表现力和可预测性。假设状态包括“攻击”、“受击”、“奔跑至目标”和“逃跑”,我们可以使用一些过渡,创建出模拟AI基本行为的状态机。
下面是简单的AI示例。
我们制作开发的AI可以用下面三句话描述:
如果AI的生命值在10%以下时,它就会逃跑。
AI可以受到攻击。
玩家处在AI范围内时,AI会向玩家跑去,然后攻击玩家。
这意味着我们的AI很简单。如果无法简单地描述自己设计的AI,那么我们可能需要对自己的AI做进一步思考。
状态机与Unity
我们在Unity使用状态机大致的方法有三种:
- 自己开发。
- 使用Animator实现。
- 从Asset Store资源商店获取相应资源。
状态机是游戏中很常见的工具,所以我们不建议开发者自己编写代码开发状态机。除非开发者希望学习如何通过编码实现状态机,否则我们可以获取可直接使用的状态机。
第二种方法是使用Unity的内置Animator功能,它其实是一种可以播放动画的状态机。但在Animator中,我们不一定要使用动画,如果不使用动画的话,它的工作方式和状态机一样。
Animator使用起来快捷而直观。下图是《Radiant Blade》中使用Unity Animator实现的弓箭手AI。
第三种方法是从Asset Store资源商店获取相关资源,不少资源有和Animator一样不错的效果。
Animator
或许你使用过Animator在Unity中实现标准动画,但我们会根据需求调整一些方法。
状态
通常,Animator的状态包含动画。我们没有这样使用,而是把状态关联到描述行为的代码。
为了展示这个方法,我们现在查看定义弓箭手的游戏对象。Behaviours对象的子对象是AI行为,它们其实是小型控制器,在对应状态激活时,它们会控制弓箭手。
在Shoot状态激活时,会在弓箭手上使用Shoot Behaviour脚本。
这是基于状态的对象。在完成行为后,Shoot Behaviour会通知Animator。Animator内置的蓝色进度条可能会让人迷惑,但它只在外观上起到作用。
变量
我们的AI设计是响应式系统,它会随条件而变化,条件是玩家和环境。
Animator的变量用于描述游戏的状态,以及作出已知决策。下图是弓箭手使用的变量,它们描述了形成AI的所有要素。
在以传统方法使用Animator时,大多数状态过渡会随着关联动画结束而结束。对于AI来说,状态就是行为,它会在未定义的时间内保存游戏逻辑。
我们使用了两个变量behaviour_ended和behaviour_error,作用是通知状态的结束。它们是状态的输出结果,表示状态成功结束,或是出现错误。
过渡
过渡定义了AI行为的改变过程。例如,过渡可以表示:当AI完成向目标行走的过程后,它应该要做什么。
下图是示例过渡:如果目标在近战范围内,AI会进行攻击。
对Unity的Animator,有些开发者可能不知道的是:过渡是有先后顺序的。特定过渡会被首先评估,仅在它的相关条件为假时,第二个过渡才会进行评估。
如下图所示,选中Neutral状态时,我们可以查看过渡的优先级。
这项功能很不错,它允许我们把AI设计为中心大脑,根据优先级来作出合适的选择。
在我们的弓箭手AI中,需要注意AI的顺序和中心部分。Neutral节点是决策中心,它的主要工作过程如下:
- 如果没有玩家的话,AI停止战斗;
- 如果玩家距离较远,AI向玩家移动,进入射击范围;
- 如果玩家不在AI的视线方向,AI向玩家移动,从而能够进行射击;
- 如果处于近战范围,则进行近战攻击;
- 如果玩家过于接近AI,AI可能会向后退;
- AI有可能随机改变和玩家的方向;
- AI会向玩家射击。
该功能的好处是每个单独的过渡都非常简单:过渡会归结为一次测试,或甚至不进行测试。使用后续过渡的前提是之前的过渡条件必须为假。
实现方法
现在,我们开始了解具体操作。你应该会注意到,我们还未提供过任何相关代码。
这意味着我们的框架有足够高的抽象级,不必处理任何技术细节,就可以很好进行解释。在代码部分完成后,设计AI的过程非常直观。
我们需要什么
下面是实现AI的三个任务:
编写AI行为。
将Animator和可用行为关联。
为Animator更新游戏相关变量的列表。
行为
开始处理前,回顾行为的功能:
行为会和游戏的角色控制器配合使用。
行为可以被识别。
行为可以被启用。
行为可以成功完成。
行为也可以出现错误。
行为可以被中断。
现在我们知道了想要的功能,我们要把它编写为API。
- public abstract class AbstractAIBehaviour : MonoBehaviour {
- // 角色由行为控制
- [SerializeField]
- protected CharController charController;
- // 必须返回对应行为的Animator状态的短哈希值。
- abstract public int GetBehaviourHash();
- // 在行为成功结束时调用的事件。
- public event Action OnBehaviourEnded;
- // 在行为失败时,要调用的事件
- public event Action OnBehaviourError;
- // OnDisable()
- // enable = true/false;
- }
复制代码
对于启用和禁用部分,我们会利用Unity的内置方法,这里不必自己编写方法。我们会使用简洁的API。
对于识别符,我们创建了带有特殊名称的方法:GetBehaviourHash。因为Animator状态的识别方式是:使用状态的标识符,也就是其名称的哈希值。
因此对于Shoot状态,对应的标识符是Animator.StringToHash(“Shoot”)。
为了弄清楚对象,避免再次计算相同的哈希值,我们可以把它们保存为静态变量:
- /**
- *该类是预计算哈希值的占位符。
- * 目的是创建Animator状态名称和AI行为之间的关联。
- * 下面定义的整数应该用于GetBehaviourHash中继承自AbstractAIBehaviour的类。
- */
- public class BehaviourHashes {
- //该行为会让角色向目标移动。
- static public readonly int OBJ_MOVETO_STATE = Animator.StringToHash("Obj MoveTo");
- // 该行为会让角色什么都不做。
- static public readonly int IDLE_STATE = Animator.StringToHash("Idle");
- // 此时角色会漫无目的地四处移动。
- static public readonly int ROAM_STATE = Animator.StringToHash("Roam");
- // ...
- }
复制代码
考虑到这点,AbstractAIBehaviour的实现代码如下。
- // 必须返回对应行为的Animator状态的短哈希值。
- public override int GetBehaviourHash()
- {
- // State name in the Animator is “Idle”
- // Animator中的状态名称为Idle。
- return BehaviourHashes.IDLE_STATE;
- }
复制代码
我们将每个哈希值存到对应的脚本中,因此ROAM_STATE可以保存在RoamBehaviour类中。
唯一的问题是手游账号购买平台,由于我们暗中把每个行为关联到名称,因此打开每个行为类收集Animator状态的授权名称可能很麻烦。
现在,我们的工作是为真实行为编写实际的代码,我们需要做的是实现AbstractAIBehaviour的子类。
关联行为和Animator
我们的AI的行为可以被识别、监听、启用和禁用,现在我们要利用行为。
我们从控制器开始。由于我们有多个彼此独立的实体,因此我们需要同步它们,从而实现流畅的工作效果。该控制器的目的是确保每次只启用一个行为,并提供修改当前行为的切入点。
一些开发者可能不知道应该何时给游戏添加新控制器的类。好的习惯是把控制器看作用来同步多个较小功能的代码。
- /**
- * AIBehaviourController应该关联AI的Animator和相应行为。
- */
- public class AIBehaviourController
- /**
- * 这部分包含可用行为
- *
- * 行为的关键是GetBehaviourHash方法返回的数值
- */
- protected Dictionary<int, AbstractAIBehaviour> behaviours = new Dictionary<int, AbstractAIBehaviour>();
- // AI的Animator
- private Animator stateMachine;
- // 该变量表示正在执行的行为
- private AbstractAIBehaviour currentBehaviour;
- // 下面是必须存在AI Animator中的触发器
- public static readonly int BEHAVIOUR_ENDED = Animator.StringToHash("behaviour_ended");
- public static readonly int BEHAVIOUR_ERROR = Animator.StringToHash("behaviour_error");
- /**
- * 强制某个行为中断正在执行的行为
- */
- public void SetBehaviour(int behaviorHash)
- {
- // 安全地禁用当前行为
- if (currentBehaviour)
- currentBehaviour.enabled = false;
- try
- {
- // 开始新的行为
- currentBehaviour = behaviours[behaviorHash];
- currentBehaviour.enabled = true;
- }
- catch (KeyNotFoundException)
- {
- currentBehaviour = null;
- }
- }
- void Awake()
- {
- stateMachine = GetComponent<Animator>();
- // 对于每个子对象
- foreach (AbstractAIBehaviour behaviour in GetComponentsInChildren<AbstractAIBehaviour>())
- {
- // 注册行为
- behaviours.Add(behaviour.GetBehaviourHash(), behaviour);
- // 监听行为
- behaviour.OnBehaviourEnded += OnBehaviourEnded;
- behaviour.OnBehaviourError += OnBehaviourError;
- }
- }
- /**
- * 在行为结束时,通知AI的Animator
- */
- private void OnBehaviourEnded()
- {
- stateMachine.SetTrigger(BEHAVIOUR_ENDED);
- }
- /**
- * 在行为失败时,通知AI的Animator
- */
- private void OnBehaviourError()
- {
- stateMachine.SetTrigger(BEHAVIOUR_ERROR);
- }
- }
复制代码
这个类比较长,但是代码其实很简单:
- 字典包含我们已知的行为。
- 方法可以激活特定行为。
- 两个事件用于在行为结束时通知Animator。
有了切入点,我们可以把它和Animator连接起来。我们会使用一个不常用的功能:StateMachineBehaviour。
如下图所示,选中Animator时,如果在空白处单击左键,我们会聚焦Animator本身,并显示Animator的隐藏检视窗口。
StateMachineBehaviour允许我们向Animator插入自定义代码。我们会在Animator的状态变化时,调用我们的AIBehaviourController。
- /**
- * 该类会插入AI的Animator。
- *
- * 它的唯一作用是监视Animator中的状态转换。
- */
- public class AIStateController : StateMachineBehaviour {
- /**
- * 在Animator进入新状态时,通知AI控制器。
- */
- override public void OnStateEnter(Animator animator, AnimatorStateInfo info, int layerIndex)
- {
- if (!animator.GetComponent<AIBehaviourController>().SetBehaviour(animatorStateInfo.shortNameHash))
- {
- // 如果状态不存在,那么把它设为决策中心。
- // 强制Animator直接评估该状态。
- animator.Update(0f);
- }
- }
- }
复制代码
这些代码非常直观,它会处理Unity的一个特别之处:Animator无法在每帧处理多个状态,因此在我们遍历决策中心时,会造成短暂的延迟。
幸运的是,解决方法很简单,我们可以强行执行Update方法,强制Animator处理状态。
通过使用我们的新类,我们可以把功能结合起来,只要把该脚本添加到AI的Animator即可。现在进入新状态时,我们的AI Animator会调用AIBehaviourController。
最后,我们在框架中包含三个类,子类以及一个角色控制器,它们包含着实际的游戏逻辑。
下图是一个组合成AI框架的小型类图示。
处理游戏逻辑
总而言之,技术解决方案可以总结为三个类,每个类都非常简洁。
我们还需要什么呢?当然是游戏本身了。但这个部分必须由开发者自己制作,实现自己的AI需要的内容如下:
一个角色控制器,负责角色和其渲染的实际逻辑。
变量以及让变量与Animator保持同步的代码。
自定义行为,例如:攻击,移动。
此时我们要处理的都是常见的Unity标准代码。
小结
如何在Unity中制作游戏AI的方法为大家介绍到这里,行动起来,在你的游戏中,实现自定义行为的AI吧。
使用Unity制作游戏AI相关推荐
- Unity制作游戏中的场景
Unity制作游戏中的场景 1.2.3 场景 在Unity中,场景(Scene)就是游戏开发者制作游戏时,所使用的游戏场景.它是一个三维空间,对应的三维坐标轴分别是X轴.Y轴和Z轴本文选自Unity ...
- unity制作游戏开始界面_开始使用Unity Playground制作游戏
unity制作游戏开始界面 We are super excited to announce the official launch of Unity Playground – the first o ...
- 利用NEO与Unity制作游戏(第1部分)
2019独角兽企业重金招聘Python工程师标准>>> 欢迎来到使用Unity游戏引擎制作的NEO区块链游戏的'A-Z'多系列讲解教程.我们将保证每个系列内容的简洁清晰,从而对整体的 ...
- 利用NEO与Unity制作游戏(第2部分)
欢迎来到使用Unity游戏引擎制作的NEO区块链游戏的'A-Z'多系列讲解教程的第2部分. 在第1部分中,我们设置了基本的Unity环境,并准备好连接到我们的私有测试网络. 在第2部分中,我们将设置这 ...
- Unity制作游戏中改名系统(第一次改名免费、改名提示、充值获得改名次数等 )
Unity游戏制作中UI界面名字修改 游戏名称修改 简单粗暴直接上代码 关于充值系统的概念 游戏名称修改 现在网络游戏中名字是标新立异.展示自己独一无二的个性的方式之一,名字越来越烧,操作越来越低,也 ...
- [Unity] 制作游戏 小球爱碰撞
先上效果: 游戏玩法: 小球将边缘的几个盒子撞掉后就胜利了. 首先制作一个天空盒材质,调整好其Shader,气候和地面颜色,如下图: 然后,在Scene场景中添加一个平面Plane,然后将自己制作好的 ...
- [Unity] 制作游戏 赛车小游戏
模拟赛车小游戏 效果展示 做一款模拟赛车的小游戏demo,方向键控制.有刹车.重玩,还有漂移和查看车型的功能 可以看到,还有翻车的效果哈哈哈!有兴趣的小伙伴来看看吧! 资源准备 从网上下载赛车模型和桥 ...
- 【游戏AI】2 - 游戏AI模型
在这本书中有大量的算法和技术.读者很容易迷失其中,所以从全局着眼去了解各个部分是如何组合在一起的十分重要. 为了有所帮助,我使用了一个统一的结构来讨论游戏中使用的AI.这不是唯一可选的模型,也不是唯一 ...
- Unity制作2D动作平台游戏视频教程
Metroidvania工具包:打造统一的2D行动平台 流派:电子学习| MP4 |视频:h264,1280×720 |音频:AAC,48.0 KHz 语言:英语+中英文字幕(根据原英文字幕机译更准确 ...
最新文章
- 对网页是否为当前展示标签页、是否最小化、以及是否后台运行进行监听
- linux 空闲等待时间TMOUT 反空闲设置 简介
- 4.1 多层感知机从0开始 4.2 多层感知机简洁实现(API调用)
- 大剑无锋之研发笔试题(一)
- oracle和mysql的安装教程_客户端安装和配置(1) - Oracle 10g 安装图解教程_数据库技术_Linux公社-Linux系统门户网站...
- Spring 教程03
- 我的第一次——网站备案
- 15分钟从零开始搭建支持10w+用户的生产环境(二)
- python 提取代码中的所有汉字
- debian 连接 蓝牙键盘
- matlab突然打不开的解决办法
- 安装语音计算机到桌面,桌面百度推出 语音搜索技术让电脑听“人话”
- Rocketmq broker迁移方案
- (纯故事)我简单写几篇,就这一次
- cubemx配置正点原子lcd屏-完整版
- ipsec.conf 各配置含义
- 最新上传缙云县五云镇欣禾社凭证(部分)
- Ratinisa's Lontrir
- idea基础配置(史上最全,你想要的全都有)
- ESPCN论文阅读笔记
热门文章
- 图像处理的数学模型与高性能算法——介绍
- iia期是第几期_IIa期和IIb期的定义,区别
- snprintf() 函数
- python split()函数
- 【ORACLE】ORACLE IMPDP导入提示ORA-01918:user‘XXX’ does not exist
- Android 横竖屏切换
- Abp mysql guid_使用ABP框架踩过的坑系列5
- Jmeter之事务控制器
- 手机的键盘是这样的: 1 2 abc 3 def 4 ghi 5 jkl 6 mno 7 pqrs 8 tuv 9 wxyz * 0 #
- 漏洞通告 | Atlassian Confluence存在远程代码执行漏洞,悬镜云鲨RASP天然免疫防护...