作者归档:贺 利华

关于贺 利华

正在学习编程,享受编程 热爱文学,闲来读读《读库》 有思想,没理想 正在学会专注

[译文]Coroutine简介

原文链接在这里[Unity Patterns | Introduction to Coroutines]

序言

在Unity中,Coroutine是我最喜欢的特性之一。我几乎在所有的项目中都会用它来控制角色的运动,连续的行为以及对象的行为等等。在这个教程中,我将会详细介绍Coroutine的工作原理,同时还会通过几个实际的小例子来演示Coroutine能如何被应用到实际的项目中。

Coroutine简介

注意:这是关于Coroutine的教程中的第一篇,如果你有兴趣还可以读一下这个教程中的第二篇文章,地址在这里

Unity的Coroutine系统源自于C#的IEnumerator,IEnumerator接口的功能简单又强大,我们可以很容易的为我们自己的集合类型定义自己的enumarator。不过我们不用担心自定义Enumerator的实现是不是会太复杂,我们可以直接先来看一个例子,通过这个简单的例子我们来分析一下Coroutine的工作原理。首先,我们先看一段简单的代码。

计时器

这是一个非常简单的计时器脚本,计时器时间到0的时候,就输出一个Log信息,表明计时已经结束。

using UnityEngine;
using System.Collections;

public class Countdown : MonoBehaviour
{
    public float timer = 3;
    void Update()
    {
        timer -= Time.deltaTime;
        if (timer < = 0)
            Debug.Log("Timer has finished!");
    }
}

这段代码乍看上去还不错,代码简短同时也很好地实现了计时的需求。但是问题来了,如果我们有一些更加复杂的脚本(例如玩家角色或者怪物),这些脚本中都含有多个计时器呢?那么我们的代码最终就有可能会变成这个样子了。

using UnityEngine;
using System.Collections;

public class MultiTimer : MonoBehaviour
{
    public float firstTimer = 3;
    public float secondTimer = 2;
    public float thirdTimer = 1;

    void Update()
    {
        firstTimer -= Time.deltaTime;
        if (firstTimer < = 0)
            Debug.Log("First timer has finished!");

        secondTimer -= Time.deltaTime;
        if (secondTimer <= 0)
            Debug.Log("Second timer has finished!");

        thirdTimer -= Time.deltaTime;
        if (thirdTimer <= 0)
            Debug.Log("Third timer has finished!");
    }
}

其实如果能忍的话,这段代码也还算可以啦,并没有啥硬伤。但是就我个人而言,我实在不喜欢有这么多的Timer变量,搞得整个代码毫无美感。而且我当我想重启这些Timer的时候,我一定得重置这些Timer的值(而事实上我总忘),这简直不能忍啊,有木有。

那么如果我写个循环是不是能再优化一些呢?

for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
{
    //Just do nothing...
}
Debug.Log("This happens after 3 seconds!");

嗯,看起来已经好多了,这回Timer变量成了循环内部的局部变量了,我也不需要再手动地设置每个循环中Timer的值了。

你可能已经知道接下来我要说的是啥了:对的,Coroutine干的就是这个。

初尝Coroutine

下面是一个跟前面我们写的计时器脚本功能一模一样的例子,只不过这个例子是使用了Coroutine来实现计时的功能。咱们最好实际动手敲一遍这个脚本,然后在Unity中运行一下,看看效果是不是一样的。

using UnityEngine;
using System.Collections;

public class CoroutineCountdown : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Countdown());
    }

    IEnumerator Countdown()
    {
        for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
            yield return 0;

        Debug.Log("This message appears after 3 seconds!");
    }
}

看上去跟之前的脚本还是有很大差别的呢,那么好吧,接下来我们就好好地来分析一下在这段代码里头都发生了些啥。

StartCoroutine(Countdown());

首先,这行代码启动了一个调用Countdown方法的Coroutine。请注意在这里我直接传入了一个方法调用作为参数(这样可以有效地将Countdown方法的返回值当做参数传入到StartCoroutine方法中),而没有传入Countdown函数的引用。

Yielding

这个Countdown方法除了以下两部分内容,其实已经很好地完成了自解释的过程:

  • IEnumerator类型的返回值
  • 循环中的yield return代码段

为了让Countdown方法能跨多帧执行(这个例子中,Countdown方法会在三分钟之内的每一帧都执行),Unity需要将整个函数执行的状态保存到内存的某个地方。而这个操作就是通过你使用yield return代码段返回的IEnumerator来实现的。当你“yield”一个方法的时候,实际上你就在告诉Unity“暂时先停止执行这个方法,等到下一帧的时候再从这儿开始执行这个方法!”。

注意:yield return代码段返回0或者null的意思就是告诉Unity等下一帧再从这里开始执行这个方法。但是你完全可以yield return其他的Coroutine,这个我们会在下一个教程中谈到。

举些栗子

在我们刚刚开始接触Corounines的时候,确实比较容易被搞得稀里糊涂的,我已经看过不少挫逼和牛逼的程序员对这个Coroutine的语法愁眉不展了。所以捏,偶觉得还是需要通过一些栗子来让我们更快更好地去理解Coroutine这货:

说几次“Hello”

首先我们要清楚一点,yield return这货的作用就是告诉程序“运行到这里先停一下,等到程序执行到下一帧的时候再继续往下执行”,这就意味着我们可以这么搞:

//This will say hello 5 times, once each frame for 5 frames
IEnumerator SayHelloFiveTimes()
{
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
}

//This will do the exact same thing as the above function!
IEnumerator SayHello5Times()
{
    for (int i = 0; i < 5; i++)
    {
        Debug.Log("Hello");
        yield return 0;
    }
}

不停地说“Hello”

通过上面的代码,我们可以想象出在一个while循环中使用Coroutine的话,其实是可以达成死循环的效果滴。这个特性可以让我们很轻易的就模拟出Update方法的效果了,如下:

//Once started, this will run until manually stopped or the object is destroyed
IEnumerator SayHelloEveryFrame()
{
    while (true)
    {
        //1. Say hello
        Debug.Log("Hello");
        //2. Wait until next frame
        yield return 0;
    } //3. This is a forever-loop, goto 1
}

读个秒

其实我们不仅仅能模拟个Update方法的效果,我们还可以在Coroutine中做更牛逼的事情,例如:

IEnumerator CountSeconds()
{
    int seconds = 0;

    while (true)
    {
        for (float timer = 0; timer < 1; timer += Time.deltaTime)
            yield return 0;

        seconds++;
        Debug.Log(seconds + " seconds have passed since the Coroutine started.");
    }
}

这个方法向我们展示了Coroutine的一个非常骚比的特性,那就是这个方法执行体内部的所有状态都会被自动保存,包括方法体中定义的局部变量,都会在整个程序执行的过程中被自动保存起来,直到下一帧再次被调用。还记得文章开头的那些无聊的timer字段们吗?有了Coroutine之后,我们就不再需要那些货了,只需要把每个计时器需要的字段放到自己的计时方法内定义就OK了,是不是处女座程序猿童鞋的福音捏?

启动和结束Coroutine

至此,我们已经学习了通过StartCoroutine()方法来启动一个Coroutine:

StartCoroutine(Countdown());

如果我们想结束所有正在运行的Coroutine的话,我们可以通过StopAllCoroutines()这个方法来结束。但是需要注意的是,这个方法只会结束由调用StopCoroutines()方法的同一个MonoBehaviour组件启动的Coroutine们,对于通过其他MonoBehaviour组件启动的Coroutine就没有效果了。

如果我们通过下面的这种方式启动了两个Coroutine:

StartCoroutine(FirstTimer());
StartCoroutine(SecondTimer());

然后我们又想单独结束两个Timer中的某一个,上面提到的方法就无法胜任了。如果我们想单独结束某一个指定的Timer的话,我们可以通过使用Coroutine的名称来作为StartCoroutine()和StopCoroutine()方法的参数,如下:

//If you start a Coroutine by name...
StartCoroutine("FirstTimer");
StartCoroutine("SecondTimer");

//You can stop it anytime by name!
StopCoroutine("FirstTimer");

[译者自己加上的]又或者这样,不使用字面常量:

// Define the Timers
IEnumerator firstTimer = FirstTimer();
IEnumerator secondTimer = SecondTimer();

// Start the Coroutines
StartCoroutine(firstTimer);
StartCoroutine(secondTimer);

// Stop the first timer coroutine
StopCoroutine(firstTimer);

扩展阅读

扩展阅读链接

如果你知道一些不错的关于Unity Coroutine的教程或者相关的教程的话,希望你能在评论中留一下这些教程的链接。
另外,如果你发现这个教程中有哪些不对的地方或者不可用的链接,或者有什么其他的问题,欢迎你联系我们

欢迎打赏

网站主机租赁需要费用,写教程呢,又是个颇费时间的活,所以你懂的。欢迎通过PayPal打赏,以便Unity Patterns能持续运营下去。 Chevy(译者注:原文作者名称)是个穷小子,给多给少都是莫大的帮助哦。


以上翻译未对原文做任何删减,酌情增加了一丢丢内容,如果翻译有错误的地方请指正,更欢迎大家用美金支持原文作者的这种分享,使用上方的PayPal链接就可以给原文作者进行捐赠支持了。

如何在Unity3D Editor脚本中使用Coroutine进行异步操作

今天在做编辑器中一个功能时,碰到一个这样的需求,Unity3D Editor脚本中会通过System.Diagnostics.Process这个类来调用外部的系统命令来执行一个操作,其实就是调用系统的Python命令执行一个Python脚本,来将Excel文件转换成文本数据。

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
}

但是这样执行完ImportExcel菜单项之后,可能会出现我们通过外部的脚本修改了Unity3D需要使用到的文本文件,但是Unity3D未能及时更新AssetDatabase,导致Unity3D中读取到的文本文件还是执行ImportConfig菜单项之前的数据。那么我们能否直接在这个外部程序执行完毕的回调中执行AssetDatabase.Refresh()方法呢?不行,因为这些函数都只能在主线程里头调用,而这个回调函数是在启动外部程序的线程中执行的,所以在ConvertProcessExited()方法中访问AssetDatabase对象会抛出异常。这个时候我们可以通过EditorApplication的update委托来配合Coroutine机制来完成我们想要的功能。

关于EditorApplication如何支持Coroutine,这个是在GitHub上找到了一个很棒的参考,地址在这儿,链接页面中是一个完整的独立的EditorCoroutine的实现,借助于这个漂亮的工具,我们修改了代码,然后就能达成我们的需求了,在执行完外部转化的Python程序之后,及时通过AssetDatabase刷新Unity3D中需要使用到的文本文件,代码如下:

private static bool sConvertDone = false;

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
    sConvertDone = false;
    EditorCoroutine.Start (RefreshAssetDatabase ());
}

static IEnumerator RefreshAssetDatabase ()
{
    while (!sConvertDone) {
        yield return 0;
    }
    AssetDatabase.Refresh ();
    Debug.Log ("Refresh Asset Database Done");
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
    sConvertDone = true;
}

 

AnimatorStateInfo中nameHash值

AnimatorStateInfo.IsName(sting name)这个方法是用来判断当前Animator播放的否是某个动画,而这个方法不只是比对动画名称的nameHash值,通过测试我们会发现传入纯粹的动画状态名称或者传入状态所在层名称和状态名称的组合,都是可以成功匹配的。

AnimatorStateInfo.IsName(string name)方法不仅会比对这个FullPath Name,还会比对Simple Name,例如我们在Base Layer中有一个名为Idle的状态。

那么通过AnimatorStateInfo.IsName(“Idle”)和AnimatorStateInfo.IsName(“Base Layer.Idle”)都是True,但是如果我们要直接比对nameHash的值,那就必须使用Animator.StringToHash(“Base Layer.Idle”) 和AnimatorStateInfo的nameHash属性值进行比对,而不能使用简称的nameHash值Animator.StringToHash(“Idle”)来进行比对。

在Unity4.6中AnimatorStateInfo中得nameHash属性值是通过Animator.StringToHash(“Base Layer.StateName”)获得的,参数是全路径名,全路径由状态所在层名称和动画状态名称组成,格式为“[LayerName].[StateName]”,请自行使用对应的LayerName和StateName替换方括号中的内容。


升级到Unity5之后,AnimatorStateInfo中就木有nameHash这个属性了,不过新增了两个字面意思更明确的属性,shortNameHash和fullPathName,两者的区别就是fullPathHash传入的Name参数是带上State所在的Layer的名字的,例如:Base Layer.Idle,而shortNameHash就是不带Layer名字的,例如:Idle。这样更明确了显然是更好的,看到Unity3D的文档一天天往完善了发展,还是深感幸福啊。

用Python写一个简单的Alfred Workflow用于颜色值的转换

由于自己是程序员,而且比较痴迷于使用各种工具,虽然可能这些工具并没有给我的效率带来太大的提高,但是我就是喜欢折腾这些软件,因为能给我带来快乐和满足感,你说谁不想爽呢。

Alfred是我使用Mac之后安装的第二个工具软件(正常工作需要的开发软件和浏览器之类的通用软件不算),第一个是Dash,第二个是Alfred。Dash是在试用了5分钟不到,查看Android帮助文档如飞的响应速度,让我直接购买了正版授权。Alfred是在当做Launch软件使用了快1年之后,看了很多人提到的关于Workflow的增强功能之后,在某个非常想购物的日子里头买了一个PowerPack,从此用上了Workflow的功能,当然在这一年里头实际上我也就只用了有道词典这么一个Workflow,其他的几乎不怎么使用,主要还是习惯问题。

这几天手痒,总觉得自己不像个能折腾的程序员,刚好看到一个用于转化数字的Workflow感觉还不错,试用了一下之后,我发现其实我自己也有一个比较强的需求可以通过这个Workflow来完成。在做Unity3D的开发过程中,我们时常会需要使用到调色板来给界面上的文字进行着色,就是设置颜色值。但是美术一般提供过来的就是效果图,或者是RGB值,通常格式是这样的(234,120,54),但是在实际的开发中我们并不是每次都是针对一个UILabel(我们的UI使用NGUI开发)进行字体颜色设置,因为有的时候我们只是想突出显示一段文字中的某一部分,这个时候依赖于NGUI UILabel的BBCode机制,我们可以通过 [ffbbcc]需要指定颜色显示的文字内容[-] 这样的方式来实现同一个UILabel下的文字使用不同的颜色进行显示。

那么这个时候有一个能快速转换(234,120,54)为#ea7836就有一些必要了,参考那个Number Convertor我就自己开始造轮子了。

Alfred Workflow设置页面

找到Number Convertor这个Workflow,选中任意一个Script Filter右键,弹出菜单之后点击Configure,或者直接双击也行,然后弹出一个配置页面,配置页面的右下方有一个Open workflow folder的按钮,点击之后会打开当前Workflow对应的所在目录,然后我们会发现这个目录下面有以下的内容:

Alfred Workflow对应目录

仔细查看一下,我们会发现所有的转化逻辑都是通过number_convertor.py文件来完成的,但是这个脚本依赖于一个叫Workflow的Python库,这个目录里头的workflow就是干这个用的。这个Python的库可以在这里找到。

以下是的脚本内容:

# -*- coding: utf-8 -*-
import sys
from workflow import Workflow

def main(wf):
    cmd = wf.args[0]
    input_str = wf.args[1]

    values_str = ""
    if cmd == "2rgb":
        values_str = conver_hex_to_rgb(input_str)
        desc = u'将%s转化为RGB格式' % (input_str)
    elif cmd == "2hex":
        values_str = convert_rgb_to_hex(input_str)
        desc = u'将%s转化为HEX格式' % (input_str)
    wf.add_item(title=values_str, subtitle=desc, arg=values_str, valid=True)
    wf.send_feedback()

def ignore_exception(IgnoreException=Exception,DefaultVal=None):
    """ Decorator for ignoring exception from a function
    e.g.   @ignore_exception(DivideByZero)
    e.g.2. ignore_exception(DivideByZero)(Divide)(2/0)
    """
    def dec(function):
        def _dec(*args, **kwargs):
            try:
                return function(*args, **kwargs)
            except IgnoreException:
                return DefaultVal
        return _dec
    return dec

sint = ignore_exception(ValueError,0)(int)


def convert_decimal_to_hex(numstr):
    decimal_var = sint(numstr)
    return "{0:#0{1}x}".format(decimal_var,4)[2:]

def convert_rgb_to_hex(rgb_str):
    if rgb_str.startswith("(") and rgb_str.endswith(")"):
        splits = rgb_str.strip("()").split(",")
        if (len(splits) == 3):
            red_decimal_str = splits[0]
            red_hex = convert_decimal_to_hex(red_decimal_str)
            green_decimal_str = splits[1]
            green_hex = convert_decimal_to_hex(green_decimal_str)
            blue_decimal_str = splits[2]
            blue_hex = convert_decimal_to_hex(blue_decimal_str)
            hex_str = "#" + red_hex + green_hex + blue_hex
            return hex_str
    	else:
    		return ""
    else:
        return ""

def conver_hex_to_rgb(hex_str):
	if hex_str.startswith("#"):
		hex_str = hex_str[1:]
        if len(hex_str) == 6:
            red_decimal_var = sint(hex_str[0:2],16)
            green_decimal_var = sint(hex_str[2:4],16)
            blue_decimal_var = sint(hex_str[4:6],16)
            return "(%d,%d,%d)" % (red_decimal_var, green_decimal_var, blue_decimal_var)
        elif len(hex_str) == 3:
            red_decimal_var = sint(hex_str[0:1] + hex_str[0:1],16)
            green_decimal_var = sint(hex_str[1:2] + hex_str[1:2],16)
            blue_decimal_var = sint(hex_str[2:3] + hex_str[2:3],16)
            return "(%d,%d,%d)" % (red_decimal_var, green_decimal_var, blue_decimal_var)
	else:
		return ""

if __name__ == u"__main__":
    wf = Workflow()
    sys.exit(wf.run(main))


然后我们就可以通过Workflow -> Add Templates -> Essentials -> Script Filter to Script创建一个Workflow,然后通过上面提到的方法进入设置页面打开Workflow所在目录,按照alfred-workflow的Python库的安装方式安装设置好alfred-workflow,然后将写好的Python脚本放到相应的位置,最终目录里头的内容如下:

目录结构

然后再回到Workflow设置页面设置一下keyword和调用Python脚本的方法和参数就OK了。

QQ20150511-7@2x

同理可以再设置一个HexToRGB的行为:

QQ20150511-8@2x

转换成功到了之后,我们把结果保存到剪贴板中,并且通过鼠标拖拽Script Filter节点上的小圆点到Copy to Clipboard节点上,这样就能把Script Filter中获得的结果保存到系统的剪贴板中了。

Screen Shot 2015-05-11 at 11.35.16 PM

最后来个实际使用的截图吧:

Screen Shot 2015-05-11 at 11.38.12 PM

折腾完毕。

Unity3D插件之Behavior Designer Movement Pack For Apex Path的巨坑

在目前的项目中,我们有使用到一个叫Apex Path的插件来做自动寻路的事情,这个插件整体来说还是棒棒哒,完全对得起$65的价格,我在写这篇文章的时候,刚好这货又在做半价促销了,链接在此

今天先不展开聊这个Apex Path插件了,今天主要是要吐槽一下Behavior Designer插件的增强包Movement Pack,在初次接触到Behavior Designer插件之时,顿时觉得这货能帮忙解决AI行为树的大部分问题,立马买回来尝试了几把,发现真心不错,可以解决很多问题。然后在他们网站上发现还有一个额外的用于处理移动的增强包,果断再次入手。拿回来简单地测试了一番,感觉还不错。但是在后续的开发中,总是会发现一些奇怪的问题,大体的表现就是NPC完全在没有移动到应该移动到的攻击位置就开始攻击了以及类似的问题,而且这个问题是在有多个使用了Behavior Designer控制的NPC出现在场景中的时候才会出现。

这个真心奇了怪了,作为一个朝内程序猿,我一直都奔着崇洋媚外的态度来看待国外程序员大大们,一直认为他们好牛逼好牛逼,然后先入为主地认为是自己用得肯定有问题,多次检查了自己的代码,依然没能找到问题。最终我就只能搞了一个空场景,就放了两个NPC,让NPC做最简单的行为,把Log加上,尼玛最后知道真相的我,眼泪掉下来啊,有木有!

我们来看看Behavior Designer Movement Pack For Apex Path中Patrol脚本中关于Apex中UnitNavigationEventMessage回调的处理。

 // Add the waypoint back on the stack when the destination is reached
public override void Handle(UnitNavigationEventMessage message)
{
    switch (message.eventCode) {
        case UnitNavigationEventMessage.Event.WaypointReached:
            movableAgent.MoveTo(waypoints[waypointIndex].position, true);
            waypointIndex = (waypointIndex + 1) % waypoints.Length;
        break;
    }
}

然后我们对比一下Apex Path插件中自带的一个PatrolBehaviour脚本是如何处理这个UnitNavigationEventMessage的回调的。

        
void IHandleMessage.Handle(UnitNavigationEventMessage message)
{
    if (message.entity != this.gameObject || message.isHandled)
    {
        return;
    }

    if (message.eventCode == UnitNavigationEventMessage.Event.WaypointReached)
    {
        message.isHandled = true;
        MoveNext(true);
    } else if (message.eventCode == UnitNavigationEventMessage.Event.DestinationReached)
    {
        message.isHandled = true;
        StartCoroutine(DelayedMove());
    }
}

细心对比一下,我们就会发现,在后者的处理逻辑中,加入了一个关于这个消息的entity对象是否为当前脚本所绑定的GameObject对象的判断,如果当前接收到的消息不是当前这个GameObject发出的,是不做任何逻辑,直接return的。然后再回过头来看看Behavior Designer Movement Pack For Apex Path中的处理,你就知道问题出在哪儿了。

因为Apex Path的GameServices中的MessageBus实现机制是任何对象都可以通过GameServices.messageBus访问到这个全局静态的MessageBus对象,并且可以通过subscribe和unsubscribe方法来进行消息的订阅和取消订阅。也就因为这个任何MessageBus的全局机制,任意注册了的对象都会收到回调,所以我们需要在处理消息回调的时候自行判断这个消息是否是我们需要的,或者说需要判断这个消息是否是发给自己的。MessageBus的机制就是一个广播,只要有任何一个对象触发了事件,注册了消息的所有对象都会收到广播,所以需要收到广播消息的对象自行甄别这货是否是发给自己的。

Behavior Designer Movement Pack For Apex Path脚本中对于回调的处理显然是忽略了这个甄别的过程,所以就会出现一些完全不在意料之中的事情,一切都能解释得通了。

Opsive的这个团队在Behavior Designer的工作确实非常惊艳,让人竖大拇哥,但是在这种问题上犯错误只能让我觉得这是实习生做的,或者就是完全没有真正地使用过Apex Path,然后就直接推出了一个似是而非的Movement增强包,我是很不以为然的。今天看到这个Movement的插件还升级了,Update需要花费$10,鉴于这个问题,我想暂时还是不升级了,虽然我很想看看他们升级的版本中是否已经修复了这个低级错误,等我今天闲下来了,我得给他们发个邮件问问。


给Opsive团队发过邮件了,邮件回复说是新的版本中已经加入这个判断了,问题已经修复,回复内容如下:

Hello,

Thank you for letting me know. Have you imported the most recent version of the Apex Path integration off of the integrations page? This integration doesn’t include an ApexPathSteeringBase file. It only includes ApexPathMovement as the base class. Within that class there is a check for the GameObject:

        public virtual void Handle(UnitNavigationEventMessage message)

        {

            if (!message.entity.Equals(gameObject)) {

                return;

            }

            switch (message.eventCode) {

                case UnitNavigationEventMessage.Event.DestinationReached:

                    arrived = true;

                    break;

            }

        }

I am not setting message.isHandled to true because all I am doing when destination is reached is setting a flag so a new destination hasn’t been set yet.

Thank you,

Justin