作者归档:贺 利华

关于贺 利华

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

Unity3D中Mecanim动画切换与AnimationEvent的关系

在我们的游戏中,我们使用了Mecanim动画中的Generic动画类型,几乎所有的动画切换和融合,我们都直接交给Animator自行处理了。省事倒是省事了,随之也带来了一些不可预知(其实是自己能力不足好吧)的问题。

今天我要分享的是一个这样的问题,例如主角正在进行A动作,此时受到攻击通过Trigger设置,切换到B动作,主角的AnimatorController中并没有直接从A动作过渡到B动作的Transition,A动作和B动作都是可以从AnyState直接切换的,也就意味着AnimatorController会自动根据当前动画的状态从当前播放的动画过渡到目标动画中去。

在A动作还没有播放完的情况下,我们通过设置Trigger的方式将A动作切换到B动作,而A动作上又绑有一些AnimationEvent,这些AnimationEvent会在动画执行到对应的时间轴的时候触发回调,用图来表达,最好不过了。mecanim_transition_animation_event图一中的这种情况,OnSkillEnd这个AnimationEvent是不会被回调的,而在图二中,OnSkillEnd这个AnimationEvent就会被回调。因为每个从AnyState出发的Transition都是有过渡时间的,如果绑定的AnimationEvent所处的时间轴在动画过渡时间之内,那么就会触发这个Event,否则就不会回调。

关于Animator.SetTrigger的一些迷思

近期一直在修改各种战斗中碰到的问题,其中有一个问题,让我困扰了很久,大概场景简单描述一下,有一天我们的策划同学给我反馈了一个问题,问题是这样的:

如果主角在战斗副本中一直按住攻击按钮进行攻击的话,貌似一直都处于霸体状态,小怪命中主角之后,能看到主角在掉血,但是主角几乎不会播放受击后仰的动画,让人觉得主角很Bug,小怪完全不是对手。

好,这是一个很好的问题,在我听到这个反馈的第一时间里,我的反应是“擦,这不可能啊”,好吧,程序猿总是这般自信和分裂。实际上这肯定是可能的,而且正在发生,可是我们总是想选择不相信事实,在看着我们的策划同学演示了大概3分钟,我发现这尼玛就是有这个问题。那么肿么解呢?

先检查代码呗,各种检查,各种推演,最终发现有可能存在一种情况,那就是Animator同时调用了多个SetTrigger方法,设置了多个不同的Trigger的值,可能会导致这个问题,那么接下来就是验证了,因为实际的工程中触发个情况肯定不是那么必然和容易的,那么我们验证的时候就直接非常直接地通过代码的方式来验证就好了。

我们直接就在某一个按钮按下的事件回调中,直接通过Animator.SetTrigger()方法设置了两个动作差别非常大的状态对应的Trigger,最终会发现Unity3D会自动融合这两个过程,例如我们在Idle状态的时候通过设置Attack和BackOff两个状态的Trigger:Attack和BackOff,然后我们就会发现,主角会先往后仰几帧,然后继续Attack的动作直到其完整播放完毕。unity3d_mecanim_set_trigger_myth测试结果是,会以当前触发的所有动画中播放时长最大的为主,其余的动画会在整个时间轴中对主角的动画综合地产生影响,所以一定要注意尽量不要让多个不同的Trigger在同一时间触发,因为Unity4.6版本未提供获取Animator参数列表的接口,所以我们得自己维护好Trigger列表,如果我们的实际需求中,不存在需要多个Trigger同时被触发的情况,那么记得在设置新的Trigger之前(当然也可以考虑优先级和权重值)将其他已经设置过但是还没有被Animator应用的Trigger Reset掉。

使用脚本在Unity3D Editor中完全删除Prefab中的ParticleSystem组件

今天碰到一个小问题需要通过脚本来批量将我们工程中的所有例子特效Prefab根节点的粒子发射器给干掉,那么好吧,开始动手吧。

ParticleSystem ps = go.GetComponent<ParticleSystem> ();
if (ps != null) {
    if (ps.renderer.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
    } else {
        var matName = ps.renderer.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (ps, true);
        }
    }
}

执行一下,发现ParticleSystem组件确实木有了,但是留了一个小尾巴,就是我们在Unity3D Editor中查看被处理的Prefab时,还能在Inspector中看到一个Shader的小尾巴,如图:

default_particle_shader_tail

这个让我情何以堪啊,你知道的,我有轻微强迫症的,虽然貌似并不会影响程序的功能,但是我要是不知道原因,我岂不是很不爽,好吧,那么就对比一下手动将ParticleSystem组件Remove和通过我们这个脚本Destroy之后的Prefab文件之间究竟有甚么区别呢,这个时候就需要借助一下Unity3D提供的binary2text命令行工具了,Mac OSX上这个命令还在这个位置[Unity3D安装目录]/Unity.app/Contents/Tools/binary2text,通常就是/Applications/Unity/Unity.app/Contents/Tools/binary2text。使用这个工具将两个Prefab文件转成Text文件之后我们就可以使用文件对比工具来进行对比了(对比二进制神马的,真的让人很蛋疼啊,幸好Unity3D还提供了这么个鬼啊),对比就会发现,这个Shader是因为啥而存在的了。

compare_1 compare_2

左侧有红色文本显示的是通过脚本Destroy ParticleSystem组件之后生成的Prefab的Text文件,右侧的是手动通过右键Remove Component删除ParticleSystem组件之后生成的Prefab的Text文件,对比之下我们能发现左侧文件中说明了一些问题:

  • 第一处红色文本表示多了一个内部资源的引用,看起来很像是Shader;
  • 第二处红色文本更是直接说明了问题,左侧表示有两个组件,组件的Id是19935810,刚好与第三处红色文本描述对应;
  • 第三处红色文本描述的是一个ParticleSystemRender组件的信息。

这么一看就知道原来还有一个隐藏的ParticleSystemRender组件没有被移除掉,因为这个Render是依附于ParticleSystem才存在的,所以不会在Inspector中单独显示,而是作为ParticleSystem的子组件Render进行显示的。那么我们就再修改一下我们的代码就好了。

ParticleSystem ps = go.GetComponent<ParticleSystem> ();
if (ps != null) {
    if (ps.renderer.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
    } else {
        var matName = ps.renderer.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (ps, true);
        }
    }
}

ParticleSystemRenderer psr = go.GetComponent<ParticleSystemRenderer> ();
if (psr != null) {
    if (psr.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
        DestroyImmediate (psr, true);
    } else {
        var matName = psr.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (psr, true);
        }
    }
}

也就是需要独立地将这个ParticleSystemRender组件也给DestroyImmediate一下就好了。搞定,洗洗睡了。

Unity3D中使用Socket.Poll方法遇到的小坑

昨天下班之前提交了一个小小的改动,就是把Socket.Poll(int microSeconds, SelectMode mode)方法中的microSeconds参数改成了-1,原因是我看到了.NET Framework文档中的这么一句话。

Poll will block execution until the specified time period, measured in microseconds, elapses. Set the microSeconds parameter to a negative integer if you would like to wait indefinitely for a response.

好吧,我承认我不矜持了,我看到这段描述的时候顿时一尿啊,赶紧把我之前设置的1000微秒改成了-1,我想这个1000微秒还是很有可能会出现没有得到实际的结果的情况啊,所以统统都搞成-1吧,我就死等你返回结果还不行吗?好吧,改完了,赶紧测试一下,在Editor和自己的iPhone 5S上分别测试了一下,貌似木有问题,然后就提交了。

晚上回到家,同事就发来消息说是新版本无法进入游戏,卸载重新装了一个中午的包就OK了,当时心里头咯噔一下,难道真的是这个改出来的问题。今天早上一到公司,策划同学打开Editor(Windows下)正要进游戏发现一直就卡在那儿。跟服务器的同学折腾了一番,WireShark和tcpdump都用上了,最终还是没有找到为啥。好吧,那就试试把昨天改的这点代码一点点恢复,最终发现就是因为这个鬼导致的。

bool pollError = TcpClientConnection.TcpClientConnection.Poll (-1, SelectMode.SelectError);

就是这个SelectMode.SelectError配合-1导致的,SelectMode.SelectRead和SelectMode.SelectWrite配合-1并不会出现这个问题,所以好吧,那就果断先把SelectError这货的时间参数修改回来吧。再仔细一看,原来这个时间参数是用微秒作单位的,所以1000是不是有点太短了呢?至少也传个500毫秒吧,在目前的这种移动网络环境下,这个时间是不是会更合适一些呢?测试一下呗,这回在Android和Windows Editor上都OK了。

记录一下这个坑,使用Socket.Poll(int microSeconds, SelectMode mode)这个方法的时候,在Android平台和Windows Editor平台下会出现因为将microSeconds设置为-1导致程序会Hang住在Poll方法这儿,这个线程就算是杯具了,这也是我的应用场景下出现所有网络请求都超时,无法收到服务器的返回(实际上服务器每次都成功返回了,而且每次超时之后重试发送的消息也都能发送出去),就是因为我在读取服务器返回的线程中调用了Poll方法查询当前这条Socket连接是否出错了导致的。


修改完了之后,我又再看了一遍.NET Framework中关于Poll方法的文档,然后看到了这么一段话:

This method cannot detect certain kinds of connection problems, such as a broken network cable, or that the remote host was shut down ungracefully. You must attempt to send or receive data to detect these kinds of errors.

尼玛啊,好吧,那还得想办法来针对这两种非正常断开与服务器连接的情况进行处理,好吧,等着我啊。

[译文]使用Coroutine进行编程

原文链接在这里[Unity Patterns | Scripting with Coroutines]

序言

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

使用Coroutines进行编程

注意:这是关于Coroutine的教程中的第二篇,如果你还没有读过这个教程中的第一篇文章的话,建议你看一下,地址在这里

还是计时器

上一篇教程中,我们学习了如何利用Coroutine的特性来暂停和继续一个方法的执行,并且让其在执行的过程中完成我们所需的操作,最终我们完成了一个非常棒的计时器。Coroutine最棒的一个特性就是,通过这些通用的程序逻辑,我们可以很容易地进行抽象和代码重用。

Coroutine参数

抽象一个Coroutine的第一步就是给它定义几个参数。因为Coroutine就是方法,所以理所当然的,它们都可以有参数。下面就是一个栗子,这个栗子会根据我们传入的频率来输出消息:

using UnityEngine;
using System.Collections;

public class TimerExample : MonoBehaviour
{
    void Start()
    {
        //Log "Hello!" 5 times with 1 second between each log
        StartCoroutine(RepeatMessage(5, 1.0f, "Hello!"));
    }

    IEnumerator RepeatMessage(int count, float frequency, string message)
    {
        for (int i = 0; i < count; i++)
        {
            Debug.Log(message);
            for (float timer = 0; timer < frequency; timer += Time.deltaTime)
                yield return 0;

        }
    }
}

嵌套Coroutine

直到现在我们在Coroutine中总是在yield return 0 或者 null,这意味着Coroutine会等到下一帧再从此处开始继续执行Coroutine的方法体。然而Coroutine牛逼之处远不止于此,这货还有一个最牛掰的技能,那就是可以通过yield return语句实现Coroutine的嵌套。

我们来看看这个嵌套Coroutine在实际代码中是如何应用的,首先让我们创建一个简单的Coroutine:Wait(),这货实际上啥也不做,就只是消耗传入时长的时间,然后结束并返回。

IEnumerator Wait(float duration)
{
    for (float timer = 0; timer < duration; timer += Time.deltaTime)
        yield return 0;
}

接下来我们再写一个Coroutine:

using UnityEngine;
using System.Collections;

public class TimerExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(SaySomeThings());
    }

    //Say some messages separated by time
    IEnumerator SaySomeThings()
    {
        Debug.Log("The routine has started");
        yield return StartCoroutine(Wait(1.0f));
        Debug.Log("1 second has passed since the last message");
        yield return StartCoroutine(Wait(2.5f));
        Debug.Log("2.5 seconds have passed since the last message");
    }

    //Our wait function
    IEnumerator Wait(float duration)
    {
        for (float timer = 0; timer < duration; timer += Time.deltaTime)
            yield return 0;
    }
}

在这个新的方法中,我们没有再使用yield return 0或者null来返回,而是直接yield return Wait()方法,这个意思就是说在Wait()方法成功执行返回之前不继续执行SaySomeThings()方法中的代码,等到Wait()方法执行结束之后再继续执行。

接下来就是见证Coroutine真正牛逼之处的时刻。

控制对象的行为

接下来的这个栗子,我们会演示如何像创建计时器般,方便地通过Coroutine来控制目标对象的行为。Coroutine并非只能用于如此简单的计时器,结合嵌套Coroutine,我们能够随心所欲地控制游戏中各个对象的各种状态。

移动到指定位置

下面的这段代码中,我们有一个简单的脚本,脚本中有targetPosition和moveSpeed两个可以设置的变量。当我们运行游戏的时候,挂载这个脚本的对象会在场景中以我们设置的moveSpeed速度向targetPosition移动。

using UnityEngine;
using System.Collections;

public class MoveExample : MonoBehaviour
{
    public Vector3 targetPosition;
    public float moveSpeed;
    void Start()
    {
        StartCoroutine(MoveToPosition(targetPosition));
    }

    IEnumerator MoveToPosition(Vector3 target)
    {
        while (transform.position != target)
        {
            transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
            yield return 0;
        }
    }
}

这个Coroutine就不再是等待某个计时器结束或者死循环了,它会在目标对象移动到指定目标位置之后结束。

按路径移动

我们还可以再扩展一下上面的这个栗子,例如不只是移动到某个指定的位置,而是让目标对象按照指定的路径在场景中移动,我们可以通过一组点来设定对象移动的路径。我们可以顺序遍历路径中的每个坐标点,然后依次调用MoveToPosition方法,让目标对象依次通过这些路径点。

using UnityEngine;
using System.Collections;

public class MoveExample : MonoBehaviour
{
    public Vector3[] path;
    public float moveSpeed;

    void Start()
    {
        StartCoroutine(MoveOnPath(true));
    }

    IEnumerator MoveOnPath(bool loop)
    {
        do
        {
            foreach (var point in path)
                yield return StartCoroutine(MoveToPosition(point));
        }
        while (loop);
    }

    IEnumerator MoveToPosition(Vector3 target)
    {
        while (transform.position != target)
        {
            transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
            yield return 0;
        }
    }
}

另外,我还加了个bool变量loop用于控制是否需要循环按照这个路径移动,如果loop为true,那么目标对象就会在移动到路径的重点之后,再次回到路径起点,然后再按照路径走一遍。

当然我们还可以用Wait()方法来给这个移动的方法做些锦上添花(或者说添枝加叶?)的事情,例如在每次移动到路径关键点之后调用一下Wait()方法来让目标对象休息一会儿,这样是不是就很类似于我们游戏中常见的巡逻的AI捏?

注意事项

如果你是刚接触到Coroutine这货,希望这两篇教程能帮助你对Coroutine的运行机制以及它能做些啥有个初步的了解。下面是一些在使用Coroutine的过程中可能需要注意的事项:

  • 你只能通过字面常量的Coroutine方法名来调用StopCoroutine()方法结束一个指定的Coroutine(译者注:不知道作者写这边文章的时候使用的Unity3D是哪个版本,Unity3D 4.6中已经可以通过Coroutine方法变量来结束一个指定的Coroutine了);
  • 可以同时有多个Coroutine在运行,它们会按照启动的顺序依次执行;
  • Coroutine可以无限嵌套,想怎么折腾就怎么折腾(在栗子中我们只嵌套了1层);
  • 如果你想在多个脚本中访问到某个Coroutine,你只需要将Coroutine方法声明为静态的就可以了;
  • 尽管这货看起来很像多线程的玩意儿,但实际上Coroutine并不是多线程的,它们跟你的脚本运行在同一个线程中;
  • 如果你的方法需要进行大量的计算,那么你可以考虑使用Coroutine来分步完成它;
  • IEnumerator方法不能有ref或out的参数,但是可以通过引用传入;
  • 目前没有什么好用的方法可以用来检测当前有多少个Coroutine在一个对象上执行;

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

欢迎打赏

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


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