65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014年8月29日

CPOL

10分钟阅读

viewsIcon

29948

精通 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测试痛苦经历的巧妙技巧,那么这本书就是为你准备的。

如果你想了解更多详情或有任何具体要求,请告诉我,我会尽力涵盖尽可能多的内容(尽管以我自己的风格,我已经在很多方面超出了预算,提供了比你可能需要的更多细节,但那也阻止不了我。)

© . All rights reserved.