Unity的Write Defaults->从一个例子谈起



Write Defaults是什么?



在Unity的Animator中点击任何一个手动创建的State,我们就会在Inspector面板中看到下图的WriteDefaults选项

先说说Write Defaults的作用(文档中语焉不详,下面解释掺杂一些本人的理解,不一定完全正确):
任何一款引擎内部处理动画,都是处理的一个个具体的动画曲线。对于Unity来说,每一个动画曲线只改变一个浮点数,比如位置的修改,其实是包含了三条动画曲线,这里就不展开了,日后有机会写一些相关的Blog。
每个动画文件都描述了他要修改哪些物体的哪些属性.Unity中的Animator会统计整个状态机中一共修改了哪些属性。如果各个动画之间修改的属性不一致的时,比如动画A修改了属性X,Y,Z.动画B修改了属性X,Y,W.当状态机从动画A过渡到动画B的时候,对于属性Z的处理就是Write Defaults来决定的。此时若其勾选(默认勾选)则播放动画B的时候,属性Z会被设置为该属性在Animator组件运行前的值(如绑定姿势)。若不勾选则属性Z会继承动画A播放后修改的值。



例子描述



先描述我遇到的问题:项目中有一个骷髅弓箭手的小怪,正常情况下从他的出生到开始攻击的动画流程应该如下图:

不过偶尔会出现小怪弓的位置出错的问题,出错的程度也并不一致。



问题分析



现在就来分下一下原因吧,这个小怪的简化版状态机如下,其中只有AnyState->Attack01的条件是一个Trigger,其他状态间切换都是正常过渡:

1.在我们的项目中所有状态默认勾选的WriteDefault都被手动的去掉了。这是因为有时候需要从前一个状态继承一些当前状态没有的属性值。
2.通过检查max文件又发现美术在制作的时候除了Birth动作以外,小骷髅的其它动作针对弓箭挂点的骨骼都没有相应的动画曲线。这就意味着,这些动作的弓箭挂点的位移和旋转是依赖于Birth状态的。
3.可见如果从Birth状态到后面状态过渡出现问题的话,就会导致弓箭挂点永远错下去,因为Birth只会播放一次,并且后续状态都不包含对弓箭挂点的属性修改。

针对第三点我再多说几点:
我发现Stand动作也没有弓箭挂点的相关动画曲线,但是Birth->Stand的过渡是没有问题的。为什么呢?原因就是下图:

可以看到Birth->Stand的动画过渡区间配置的比较合理,保证了两个动画在插值过程覆盖了Birth动画的全部长度。在Max中发现,大概Birth动作到达上图白线位置的时候,弓箭挂点就已经到达了正确的旋转和位移,之后不再有变化。所以理论上只要动画过渡区间覆盖到白线就能保证不出错。

既然弓箭丢失不是Birth->Stand过渡引起的,那就应该是从AnyState到Attack01这个状态过渡的问题了,因为AnyState的特殊性,所以没有勾选HasExitTime,而此时的过渡情况就很微妙了,在下图中所看到的过渡区间并不是真正的过渡时间,由于官方没有相关的说明,自己做了些测试,假设下图中过渡区间占了Birth动作条的15%,而在触发AnyState->Attack01切换条件的时候,Birth已经播放到了10%.那么实际上接下来Birth动作参与动作融合的部分是其动作条的10%到25%的阶段,也就是实际的融合取决于前置状态的播放进度。

因为Attack01状态的WriteDefault是没有勾选的,那就意味着它对于弓箭挂点的位置和旋转完全依赖于前置动作在融合过程中最后一帧的值。接下来就能合理的解释,为什么有的时候弓箭不会脱手,而有的时候脱手,脱手的程度又不一致的问题了。正是因为AnyState->Attack01的触发条件Attack01的触发时间并不完全一致导致的。如果碰巧触发的时机合适,其融合区间包含了弓箭挂点到达正确位置和朝向的部分,那么看上去就是对的了,

以上的这些分析都是基于一个前提,就是Birth动作没有播放到合适的时机就被切换了。你也许会奇怪为什么不保证Birth播放完了再进行状态切换呢。这就得问策划了。在我们的游戏里,小怪有一个出生的范围,到达了出生范围就会生成小怪,状态机自动播放出生动作;还有一个攻击范围,如果进入了攻击范围,就会播放攻击动作。很显然如果角色的移动速度很快,或者生成范围和攻击范围相差太小,就会导致小怪刚一播放出生动作还没播完,Attack01的触发器就会被激活。也就可能出现弓箭脱手的问题了。

另外,这个问题只出现在远程兵身上,近战兵从来没有发现过武器脱手的。大家可以想想为什么。



解决方案



既然为分析出来了,解决的办法有很多,但是如果要治本,还是要从动作资源本身上解决这个问题。勾选WriteDefault是可以解决这个问题的,但又会导入新的问题,这个不行,我们在文章开始解释WriteDefault的那个例子中,我们只要让动作B也包含Z属性的曲线,这样WriteDefault这个选项就失去了意义,不过这样会让Attack01动作引入新的动作曲线。但由于在这个本例中,我们并不需要在Attack01动作过程中对弓箭挂点有什么修改,所以只要让他有一个绑定姿势的常量值就好了。Unity也会对常量曲线进行优化,所以问题并不大。

啰嗦了这么多,不知道有没有把这个问题讲清楚。Unity的Animator这块内容,用起来上手很快,但它实际涉及的内容量还是很大,Unity尝试对个各个概念去包装,让大家能容易理解,但就我的经验来看,他们做的还不够,无论是文档,还是使用界面都还有待优化。不管是国内国外,相关的资料都很少。也不知道国内有没有同行遇到过这个问题,希望大家多多研究,多多分享,功能营造一个好的氛围。

尊重他人智慧成果,若要转载,请注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/Unity_WriteDefaults.html 

原文地址:https://www.cnblogs.com/Esfog/p/Unity_WriteDefaults.html

时间: 2024-10-10 00:56:38

Unity的Write Defaults->从一个例子谈起的相关文章

iOS 中 #error宏 的使用的一个例子

头文件中如下定义宏: /** * @brief 默认空间名(必填项) */ #error 必填项 #define DEFAULT_BUCKET @"" /** * @brief 默认表单API功能密钥 (必填项) */ #error 必填项 #define DEFAULT_PASSCODE @"" 类的实现中使用: self.bucket = DEFAULT_BUCKET; self.passcode = DEFAULT_PASSCODE; 这样,如果没有初始化这两

一个例子理解threadLocal用法

ThreadLocal可以使对象达到线程隔离的目的.话不多说直接上代码: /** * 一个例子理解threadLocal * * 一个单例模式的类 */ public class SingleThreadLocalTest { private static SingleThreadLocalTest single = new SingleThreadLocalTest(); private ThreadLocal<String> threadLocal = new ThreadLocal<

subsys_initcall宏定义的一个例子,acpi/bus.c里面。

static int __init acpi_init(void) { int result; if (acpi_disabled) { printk(KERN_INFO PREFIX "Interpreter disabled.\n"); return -ENODEV; } acpi_kobj = kobject_create_and_add("acpi", firmware_kobj); if (!acpi_kobj) { printk(KERN_WARNING

js的prototype扩展的一个例子,模仿C#的StringBuilder功能,数组组合字符串,效率大于+拼凑

function StringBuilder() { this._strings_ = new Array;}StringBuilder.prototype.append = function (str) { this._strings_.push(str);};StringBuilder.prototype.toString = function () { return this._strings_.join("");}; js的prototype扩展的一个例子,模仿C#的Strin

unity行为树制作AI简单例子(1)

用行为树来制作AI是非常方便的,今天就给大家简单介绍一下行为树的强大之处. 所用插件 Behavior Designer v1.421 最开始 我使用过Rain插件,不过用过Behavior Designer后感觉界面更为直观,更容易设计出AI 新建一个项目,先导入该插件 新建一个Cube,命名为Floor,作为本场景的地板,Scale设置为(20, 0.1, 20),Tag设置为Floor 创建一个Capsule,命名为Player,Tag设置为Player,添加一个简单的Player脚本控制

javascript闭包的一个例子

<html> <head> <title>elementFromPoint</title> <script type="text/javascript"> window.onload = function(){ for(var i=0; i<6; i++){ var alink = document.createElement('a'); var titleText = document.createTextNode('

线程间共享数据的一个例子

[申明:本文仅限于自我归纳总结和相互交流,有纰漏还望各位指出. 联系邮箱:[email protected]] 题目:输入一个整形数组,数组里有正数也有负数. 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和. 求所有子数组的和的最大值.要求时间复杂度为O(n). 题目分析: 一.如果数组中全部为负数,则返回最大负数值即可 二.当既有正数也有负数的时候: (1)从左往右叠加,如果当前叠加值小于或者等于0,则放弃,叠加总和清0(加一个负数或者0是毫无意义的),从此位置继续重新叠加 (

用一个例子读懂 RequireJS

用一个例子读懂 RequireJS 例子来自官方,我稍微改造了一下,如下: // project.html <!DOCTYPE html> <html>     <head>         <title>requirejs</title>         <!-- data-main attribute tells require.js to load              scripts/main.js after require.

一个例子教你理解java回调机制

网上很多例子都写的很难理解,笔者刚开始都已经弄晕菜了. 这个例子,应该是再简单,再简洁不过的了,例子目的是测试某个方法的执行时间.这里就写三个java类,一个接口,一个实现,还有一个用于测试时间的类. 要测试的方法,尽量占用执行的时间,这样明显一些,这里测试循环1000000次,并且打印出来. 测试类: public class MyMethod { public void mytest() { // TODO Auto-generated method stub for (int i = 0;