精通 Unity 2D 游戏开发 – AI 和状态机





5.00/5 (6投票s)
精通 Unity 2D 游戏开发 – AI 和状态机
终于来了,我的第一本书已在Packt出版社网站和所有主要的在线书店(可能还有一些不太知名的书店??)上架。如果你喜欢这里展示的片段,那么你一定会爱上整本书。以下是本书内容概览。
本片段的示例项目和代码可以在这里找到:Mecanim State Machines.zip
关于本书
写这本书对我来说是一次有趣的挑战,它的写作风格与我的博客一脉相承。我一直认为,教育读者不仅仅是展示如何做,更要解释为什么选择一种方式而不是另一种,而且如果有替代方案,我也会指出。作为读者,你应该了解你的选择(然后自己做决定)。
在书中,你将构建一个RPG游戏框架,然后可以对其进行扩展和个性化。目标是为你提供足够的提示、技巧和帮助,来构建自己的完整游戏。
本书内容概览
- 回顾Unity 4.3和2D游戏开发的新改进(以及你可能错过的所有其他内容)
- 深入探讨新的Sprite系统和动画改进(这是我第一个章节,内容太多以至于不得不一分为二)
- 使用2D摄像机、场景和Sprite分层,以及一些高级编码技术,最终构建你自己的RPG对话系统。
- 我们将涵盖构建地图和探索系统,最终与一些持有真正凶狠剑的讨厌的哥布林遭遇。
- 如果你喜欢购物,那么你来对地方了,请允许我推荐这把可爱的1级剑。学习构建一个购物系统,然后回到战场。
- 在第二个章节,同样因为内容过多不得不一分为二,我们将介绍回合制战斗系统,包括一些你可能从未考虑过的Mecanim用法(状态战斗机和AI,有人感兴趣吗?)
- 完成游戏框架后,我们将着眼于完成你的游戏,并研究编辑器,看看如何扩展它来帮助我们构建游戏(编辑器脚本,太棒了),最后将深入介绍如何正确实现应用内购买。
- 最后,我们将介绍如何扩展和部署到各个平台,其中包含大量的序列化(保存和加载)帮助,让代码只在特定平台或编辑器上运行,以及关于营销的大量提示和技巧。
我对这本书唯一的遗憾是它不能再大一些
。这本书的内容足以让你完成自己游戏的90%,你所要做的就是完成它并添加更多内容!
与我所做的所有事情一样,如果你对书中任何主题有更多想了解的地方,请给我留言或在我的博客上评论,我将非常乐意就该主题撰写更多内容。
关于书的内容就到这里——我的片段在哪里?
本系列的第二个片段与其说是一个完整的片段,不如说是一个预告,因为它是一个非常有趣的主题,只有书中才能真正展现其魅力,提供完整的实际示例。
Mecanim状态机
(我老是打错Mecanim,要删掉那个H,哈哈。)
对于使用过Mecanim的各位,你们会发现它是一个很棒的3D动画系统,可以用来为3D模型制作动画,使用骨骼和准备好的动画,还可以混合这些动画以获得更逼真的效果。但究其本质,Mecanim不过是一个非常高级的状态机,拥有一个出色的图形界面。在Unity 4.3中,它得到了增强,也支持2D Sprite动画。
你可能没有意识到的是,Mecanim不仅仅可以用于动画,它几乎可以用于任何需要状态机的场景,从游戏状态到AI机器!每种都有其独特的实现方式和使其发挥最佳作用的小技巧。
一个简单的游戏状态机
非动画器Mecanim系统最简单的例子就是游戏中的战斗状态机。这种方法存在一些复杂性(主要是由于Mecanim处理当前状态的方式),我们需要处理这些问题,但最终我们将获得一个具有易于管理界面的更优系统。
如果我们尝试用代码实现一个简单的游戏状态系统,我们通常会陷入一堆复杂的 `switch` 或 `if` 语句的混乱中,它们争相判断在每次游戏更新中应该发生什么(不一定总是混乱,但很容易变得如此)。
假设我们有以下流程
在普通代码中,我们很可能会从定义每个状态的 `Enum` 开始(如果你胆子大,也可以直接使用 `string`),如下所示:
enum BattleState
{
Battle_start,
Intro,
Player_attack,
Opponent_attack,
Player_dead,
Opponent_dead,
Battle_result,
Battle_over
}
然后在更新循环中实现类似如下的代码:
void Update () {
//wait loop when a pause is required
if (timer > 0)
{
timer -= Time.deltaTime;
return;
}
// Set the next state;
currentBattleState = nextBattleState;
//What to do in the current state and where to go next
switch (currentBattleState)
{
case BattleState.Battle_start:
playerHealth = 10;
opponentHealth = 10;
nextBattleState = BattleState.Intro;
break;
case BattleState.Intro:
timer = 3f;
nextBattleState = BattleState.Player_attack;
break;
case BattleState.Player_attack:
if (Input.GetKeyDown(KeyCode.Space))
{
opponentHealth -= 5;
if (opponentHealth <= 0)
{
nextBattleState = BattleState.Opponent_dead;
}
else if (playerHealth <= 0)
{
nextBattleState = BattleState.Player_dead;
}
else
{
nextBattleState = BattleState.Opponent_attack;
}
}
break;
case BattleState.Opponent_attack:
playerHealth -= Random.Range(0, 10);
if (opponentHealth <= 0)
{
nextBattleState = BattleState.Opponent_dead;
}
else if (playerHealth <= 0)
{
nextBattleState = BattleState.Player_dead;
}
else
{
nextBattleState = BattleState.Player_attack;
}
timer = 1f;
break;
case BattleState.Player_dead:
case BattleState.Opponent_dead:
timer = 3f;
nextBattleState = BattleState.Battle_over;
break;
case BattleState.Battle_result:
timer = 2f;
nextBattleState = BattleState.Battle_over;
break;
}
}
只要我们想进行一点小小的改动,我们就会陷入对所有路径的搜索,以检查我们当前处于什么状态,需要转移到什么状态,以及需要设置哪些标志。(根据我的经验,你还会遇到意外的效果,导致你添加更多的状态来跟踪不同的条件,通常会使事情变得更糟或以后更难诊断。)
你可以通过查看可下载项目示例场景中附加到 `BattleStateMachine` GameObject上的 **OldStyleStateMachine.cs** 脚本来更全面地了解这一点。只需启用它即可查看基本示例(确保关闭Mecanim脚本)。
这确实是一个非常基础的例子,但想象一下它放大50倍,有许多复杂的路径,代码的每个部分都需要了解周围的一切才能做出正确的决定,现在你可能会开始看到更大的图景。
那么Mecanim到底给我们带来了什么?
使用Mecanim本身来实现状态机流程非常简单,事实上,我们上面已经画出了大概的样子。根据图片中的大纲,我们可以将其转换为Mecanim设计面板,看起来像这样:
我们已将游戏状态设计流程复制为空的Mecanim状态,并向Animator添加了一些参数来跟踪生命值、是否处于战斗状态以及一个表示攻击已发生的触发器。那么代码呢?Mecanim是如何简化事情的?
简单来说,它消除了代码中的所有选择和决策,这些都转移到了Mecanim,我们只需要在某些事情发生变化时通知Animator即可。如果我们将这个Animator应用到示例场景的 `BattleStateMachine GameObject` 上,并使用上面分配给控制器的控制器,我们就可以通过脚本来利用它。
更棒的是,任何状态都可以通过简单快速的转换快速地连接到任何其他状态。
仍然使用我们之前的 `Enum`(因为它是代码了解当前情况的指南),我们的代码将简化为:
void Update()
{
currentBattleState = battleStateHash[battleStateMachine.GetCurrentAnimatorStateInfo(0).nameHash];
switch (currentBattleState)
{
case BattleState.Battle_start:
playerHealth = 10;
opponentHealth = 10;
battleStateMachine.SetInteger("Player_health", playerHealth);
battleStateMachine.SetInteger("Opponent_health", opponentHealth);
battleStateMachine.SetBool("Battle_inprogress", true);
break;
case BattleState.Player_attack:
Attacking = false;
if (Input.GetKeyDown(KeyCode.Space) && !keyPressed)
{
keyPressed = true;
opponentHealth -= Random.Range(3, 5);
battleStateMachine.SetTrigger("Attack");
}
battleStateMachine.SetInteger("Opponent_health", opponentHealth);
break;
case BattleState.Opponent_attack:
keyPressed = false;
if (!Attacking)
{
playerHealth -= Random.Range(0, 10);
battleStateMachine.SetTrigger("Attack");
Attacking = true;
}
battleStateMachine.SetInteger("Player_health", playerHealth);
break;
case BattleState.Battle_result:
battleStateMachine.SetBool("Battle_inprogress", false);
break;
}
}
这从代码中消除了许多状态机的复杂性,你只需要在每个节点处说明你想要发生什么。
有一些我们必须注意的陷阱,这主要与Mecanim的强大和速度有关:
- Update的运行速度比Mecanim快,所以如果在update代码中有单独的动作,你需要考虑到这一点(如通过几个 `private` 布尔值所示)。
- 输入(如键盘)可能在几个update循环中都为真(因此需要为键盘输入使用布尔值)。
- Mecanim会严格按照你的指示执行,这可能会导致混淆,并且多个路径可能同时为真!
现在这几乎是故事的结尾了,你会注意到在每次更新的开头,我们都会获取Animator的当前状态。问题在于Mecanim目前根本不与状态名一起工作,它实际上使用一种哈希机制来跟踪Animator当前所处的状态,以及状态在其生命周期中所处的精确点(如果你正在进行混合或动画,这很有用;如果你只想获取状态,则用处不大)。由于我们喜欢使用名称(当然是可选的,你也可以只使用哈希值),我们需要缓存它们。在脚本中,我创建一个字典并在脚本启动时缓存它们;如果你愿意,也可以在构建时缓存它们。
在测试中,我没有看到太多性能影响,但如果你有很多状态,可能需要考虑在构建时缓存状态名。
所以在示例中,`Start` 和 `Awake` 方法如下所示,用于缓存状态名和哈希键:
Animator battleStateMachine;
private Dictionary<int, BattleState> battleStateHash = new Dictionary<int, BattleState>();
private BattleState currentBattleState;
void Awake()
{
//Get the Animator state machine, error if none found on this GO
battleStateMachine = (Animator)GetComponent(typeof(Animator));
if (!battleStateMachine || !battleStateMachine.runtimeAnimatorController)
{
Debug.LogError("State Machine Missing or not configured)");
}
}
void Start () {
//Cache all the hashes of the States in our State Machine (case sensitive!)
foreach (BattleState state in (BattleState[])System.Enum.GetValues(typeof(BattleState)))
{
battleStateHash.Add(Animator.StringToHash("Base Layer." + state.ToString()), state);
}
}
这有点麻烦,但因为这是一件很小的事情,所以很容易处理。
正如你可能希望看到的那样,这非常强大,可以大大简化Mecanim中的复杂决策实现,只是要记住,能力越大,责任越大!。
这篇文章确实只是触及了Mecanim作为纯状态机可能实现的皮毛,在书中,我们探索了像上述那样的完整系统,甚至深入研究了通过Mecanim实现的简单AI系统。
希望你喜欢这场演出
我希望你喜欢这个小片段,这只是(希望)书中许多小章节中的一个。这些片段有更多的细节,因为我有更多的空间可以发挥(500多页的限制竟然如此令人惊讶),但每节都包含了你所需的一切。
我很高兴这本书终于出版并已上市供大家抢购,有任何疑问/问题/想法,请随时通过我博客上的“联系”页面给我留言,我保证会尽快回复你。
本片段的示例项目和代码可以在这里找到:Mecanim State Machines.zip
等等,还有更多!
现在Unity终于揭开了新的、闪亮且先进的UI系统的面纱,我可以正式宣布,我的第二本Packt书籍已经接近完成,这本书将是对新UI系统的深入概述和指南。
所以,如果你想在如何充分利用新UI系统方面抢占先机,并学习一些来自数月Beta测试痛苦经历的巧妙技巧,那么这本书就是为你准备的。
如果你想了解更多详情或有任何具体要求,请告诉我,我会尽力涵盖尽可能多的内容(尽管以我自己的风格,我已经在很多方面超出了预算,提供了比你可能需要的更多细节,但那也阻止不了我。)