05立绘的切换和表情,眨眼动画

 

本教程由水螅制作,教程内所有提供下载的练习素材皆为水螅制作,只可用于练习,不可用于其他用途。

如果立绘也全部是静态图片的话,也可以用和背景同样的方式来切换,多人同屏的话,先在场景中摆好不同位置的fg123就好,但是一般来说……AVG里的立绘至少也要换个表情的……所以这一套就不好使了呢。

假设我们现在有fg1的两个表情fg1_face0fg1_face1

fg1_face0.png

fg1_face1,png

​我们把这两张图片放入assets,将fg1_face1拖到hierarchy里边,作为fg1的子object

​然后移动这张图的位置,

​这样,角色的表情就改变了。

说到这里我顺便详细说一下图片的遮挡顺序吧。在camera所照射的地方,就像正常的摄像机一样,离摄像机比较近的图片会挡住比较远的图片。​我们的main camera在场景中默认的位置是这样

​也就是说,它现在是从-10的位置向着0的方向在照射。​所有在它的框框里,z坐标大于-10的sprite都能被照到(因为他的拍摄长度默认是1000,一般也不会把图片放到那么远啦),离他近的会挡住离他远的,z坐标为-9的图片就会遮住z坐标为-8的图片。总得来说,z值小的图片会挡住z值大的图片。这套规则只有摄像机照到的东西是这样,canvas负责的ui就不是这样,ui的遮挡规则我们之后再说吧。

如果没有意外的话,我们的bg1的坐标应该是​这样的,在000的位置呢。如果你的bg1不在这里也没有关系,因为只要保证场景中相对的位置对就行了。

立绘当然是一直要在场景前方的,所以我们的fg1应该差不多在这里​​

xy的零零散散的数值是我把立绘放在画面中间时候手动拖出来的,你的值很有可能和我的并不一样,但是z值一定要保持在小于0的数字上,才能保证fg1挡在bg1的前边。

​fg1_face1的z值是-0.1,因为子object的坐标是相对于父object来说的,也就是说,fg1的位置是fg1_face1的000,fg1_face1相对于fg1的z值少0.1,保证了fg1_face1在fg1的前边。如果你把hierarchy里边的fg1_face1拖到和fg1并列的位置,而不再是它的子object,就可以看到fg1_face1相对世界的位置是-1.1

​​那么,face的x和y是如何确定的呢?绘师在给出表情切片的时候,给的多半是ps里边的xy的值,和unity使用的完全不同。要如何把ps里边的xy值转换为unity里边的xy值呢。

unity在计算相对位置的时候,是以两个图片的中心相对位置来计算的,ps则是以左上角的位置来计算的。unity使用的是第一象限,也就是越往上y越大,越往右x越大,ps使用的是第四象限,越往下y越大,越往右x越大。

那么很容易就能得到一个换算公式


 unity需要的x = -(底图宽 / 2 - ps里的x - 表情图宽/ 2)/100;

 unity需要的y = (底图高 / 2 - ps里的y - 表情图高 / 2)/100;

一般来说我会让程序自己来算,不过这里我们就手动把算好的坐标输进去吧。

把fg1_face0也拖进去,放在fg1_face1的z值更少一些的位置。

我们将fg1_face0淡入就可以营造出从face1的表情切换到face0的表情(教程2的内容)。

为了代码的通用性,我们将fg1_face0和fg1_face1改成face0和face1。

如果我们有更多的表情,那么通过传递参数更改face1和face0上挂载的图片,就可以达成从任意一个表情切换到另一个表情的目的。

所以我们给face0录制一个淡入动画,然后来写一个更换表情的function叫做changeface,因为只要淡入切换表情就可以了,淡出动画就不用做了


void changeface(Dictionary<string, string> elm)

{

        GameObject temp = GameObject.Find("fg1");

        GameObject temp1 = temp.transform.Find("face0").gameObject;

        //将下层的表情图层赋值为旧表情

        temp.transform.Find("face1").gameObject.GetComponent<SpriteRenderer>().sprite = temp1.GetComponent<SpriteRenderer>().sprite;

        //将上层的表情图层赋值为新表情

        temp1.GetComponent<SpriteRenderer>().sprite= (Sprite)Resources.Load("fg/" + f["fg1_face" + elm["n"], typeof(Sprite));

        //开始播放淡入动画

        temp1.GetComponent<Animator>().Play("facea1", -1, 0);

        //执行下一行脚本

        startscript();

}


这里没有用animator的SetInteger函数而是用了Play函数,因为只有一个动画的时候不用考虑动画的过渡,直接播放就是了。

animator的play的

第一个参数是动画名,就是你录制动画时候起得那个名字

第二个参数是layer名,-1表示默认

第三个参数是从动画的百分之几开始放,如果写0就是从一开始开始放,写0.5就是从动画的50%开始放,写1就是从动画的末尾开始放(如果这是个不循环的动画,那就一直停留在末尾那一帧了)

脚本里就可以使用@changeface n=1 这样来更换表情

同时也应该修改一下fg1fadein将表情加上


public void fg1fadein(Dictionary<string, string> elm)

{

GameObject temp = GameObject.Find("fg1");

Animator an = temp.GetComponent<Animator>();

an.SetInteger("a_num", 1);

StartCoroutine(waittime(1f));

 temp.transform.Find("face0").gameObject.GetComponent<SpriteRenderer>().sprite = (Sprite)Resources.Load("fg/fg1_face" + elm["n"], typeof(Sprite));

}


脚本可以用@fg1fadein n=1这样来设定立绘出现时候的表情

但是这样会有点单调,假如我们的表情上还要放眨眼动画呢。

我们有3帧眨眼动画fg1_face1_e1.png fg1_face1_e2.png fg1_face1_e3.png

相对fg1_face1.png的位置是32,29(请自行运算在unity里的坐标)

将fg1_face1_e1拉成face0的子物件,放置到算好的位置,z值设为-0.05盖住底图face0,现在是睁着眼睛的所以和face0原本的表情没有任何分别。将子物件的fg1_face1_e1改名为e1,

将e1拖到fg1之外为它新增动画fg1_face1_ea1(不拖出去的话,想要新增动画需要操作的步骤就多一点)

创建动画之后再拖回去。开始录制它的动画。

我们希望他3秒眨一次眼睛,在时间轴3秒的位置点击增加关键帧然后在3.01秒的位置改变它的sprite为fg1_face1_e2,3.02秒的位置改为fg1_face1_e3,3.03秒改为fg1_face1_e2,3.04秒改为fg1_face1_e1。

现在播放一下动画可以看到

我们将e1的active勾掉,让它平时出于未激活状态,

因为我们的设计时当face0的动画播放到完全淡入后再激活它的active,让它开始播放眨眼动画。所以我们要写一个function用来放在face0的动画里。

我们在project里的script文件夹里右键新建一个c#文件。起名叫spritecontrller,为它增加两个函数,一个用来将所有子物件激活,一个用来将所有子物件反激活


 public void activechildrentrue()

    {

        for (int n = 0; n < transform.childCount; n++)

        {

            transform.GetChild(n).gameObject.SetActive(true);

        }

    }

    public void activechildrenfalse()

    {

        for (int n = 0; n < transform.childCount; n++)

        {

            transform.GetChild(n).gameObject.SetActive(false);

        }

    }


将project里边的spriteconller拖曳到hierarchy里边的face0上

可以看到face0增加了一个spriteconller的组件。

然后我们来看face0的动画时间轴,在完全淡入的地方右键 add animation event

会出来一个小方块。选中这个小方块看他的inspector,将他的function选为我们刚写好的activechildrentrue

这样这个表情就做好了。

事实上,不光是眨眼动画,我们如果开拓思路的话,也可以加入各种其他动画在表情上,比如

但是我们意识到,现在我们无法通过只是更换face0的sprite来完成更换表情了,因为不是每个表情都有眨眼动画,也不是每个眨眼动画都一样。现在的face0,就是写死的fg1_face0这个表情和它的眨眼动画的组合。

所以我们要将表情切换换一个思路。当我们需要face0的时候,我们将face0临时增加为fg1的子物件,放在比fg1高0.2的位置,播放淡入动画,激活眨眼动画,然后我们需要face1的时候,我们将face0往下放一点,放在比fg1高0.1的位置,然后将face1放在比fg1高0.2的位置,播放淡入动画,激活眨眼动画(如果有的话),我们再需要face0的时候,将旧的face0删除,将face1往下放一点到-0.1,然后将新的face0放在-0.2淡入……

那么控制face0淡入的机制也不需要了,因为他只要被载入到屏幕上就应该开始播放淡入动画,所以我们选中face0,在animator界面,将它的淡入动画facea1右键set as layer default state

我们在project里边新建一个fg文件夹,将hierarchy里的face0拖曳进去。相当于将我们在scene里边组好的GameObject作为prefab储存在了硬盘上。这样以后我们随时可以从resources里load它来使用。将project里边的face0改名为“fg1_face0”。

那么fg1_face1的表情也需要同样做一个prefab,face0的sprite改为fg1_face1的图片,将e1删除,这就是一个淡入fg1_face1的图片的GameObject了,将face0改名为fg1_face1,拖到project里作为prefab

因为两个表情都已经做成了prefab,我们切换表情的时候就直接从resources里load做好的prefab就好,不用再load原本的png文件了,所以将表情的png文件从resources里边挪出来挪到assets的fg文件夹里。resources文件夹里请保持只放置会用resources.load来load的文件,其他文件不要放进去会影响游戏启动速度。

现在我们可以准备将fg1上边的face1和face0都删除了,不过在这之前,我们在face0上右键新建一个空的GameObject,

将它的坐标设为000

然后将它拖出face0,成为fg1的子GameObject

将它改名叫face

这一步操作的目的是用这个空的GameObject记录fg1上face的位置,这样我们不用每次都自己再算一遍了。只要把表情的prefab设定为这个空的GameObject的子GameObject,坐标设为000就是对的坐标了。

准备工作都做好了,我们来修改一下changeface


void changeface(Dictionary<string, string> elm)

    {

        //获取fg1上的face,接下来的表情文件都会作为face的子文件来往上挂

        GameObject temp = GameObject.Find("fg1").transform.Find("face").gameObject;

        //如果有face0存在就删掉face0

       if (temp.transform.Find("face0"))

        {

            Destroy(temp.transform.Find("face0").gameObject);

        }

        //获取face1,将face1改名为face0,放低一点,将上边的动画都停掉

        GameObject temp1 = temp.transform.Find("face1").gameObject;

        temp1.name = "face0";

        temp1.transform.localPosition = new Vector3(0, 0, 0);

        temp1.GetComponent<spriteconller>().activechildrenfalse();

        //读取要求的表情文件,为它生成一份克隆体放到场景上,起名叫face1,放的比face0要高一点,因为被放如了场景所以会自动开始播放表情动画

        GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/fg1_face" + elm["n"], typeof(GameObject)));

        temp2.name = "face1";

        temp2.transform.SetParent(temp.transform);

        temp2.transform.localPosition = new Vector3(0, 0, -0.1f);

        temp2.transform.localScale = new Vector3(1, 1, 1);

        //执行下一行脚本

        startscript();

    }


这段代码里,Destroy是MonoBehaviour的函数,功能是从场景里删除()里的gameobject或者component

Instantiate也是MonoBehaviour的函数,功能是克隆()里的object,我们读取的prefab需要克隆了之后把克隆体放到场景里,然后对克隆体进行各种操作,不会影响到硬盘上的原件。

transform.localPosition和transform.localScale就是设定这个GameObject的相对于父GameObject的位置和缩放,和在界面

这里设置是一个意思。

关于transform的函数都有什么请看https://docs.unity3d.com/ScriptReference/Transform.html

我们看到我们将自己之前写的spritecontorller作为一个组件获取并执行了他的function


GetComponent<spriteconller>().activechildrenfalse();

其实unity提供给我们的组件也都是这样一个一个c#文件写出来的,所以用法是一样的。

同样的,fg1fadein里边载入表情的部分也需要修改一下


public void fg1fadein(Dictionary<string, string> elm)

{

GameObject temp = GameObject.Find("fg1");

Animator an = temp.GetComponent<Animator>();

an.SetInteger("a_num", 1);

StartCoroutine(waittime(1f));

 GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/fg1_face" + elm["n"], typeof(GameObject)));

 temp2.name = "face1";

 temp2.transform.SetParent(temp.transform.Find("face"));

 temp2.transform.localPosition = new Vector3(0, 0, -0.1f);

temp2.transform.localScale = new Vector3(1, 1, 1);

//这一句的意思是播放位置放到facea1的最后一帧也就是已经完全淡入进来的那个位置

 temp2.GetComponent<Animator>().Play("facea1", -1, 1);

}


这样我们的表情控制就做好了。

但是我们很快可以发现,当fg1淡入淡出的过程中,也就是fg1变成半透明的时候,上边重叠的表情图片是会穿帮的=口=

这是绝对无法接受的。

通常的解决方案是为表情和底图写不同的shader来避免穿帮,但是这篇教程里我不想介绍shader这么麻烦的东西……

所以我们用另外一种猥琐的方式来解决这个问题——render texture

将fg1移动到main camera可以看到的区域之外。

我们要为它建一个单独的摄影棚。

在fg1的layer那里选择add layer

增加一个叫做fg的layer

重新选中fg1,在它的inspector那里把它的layer设定为fg,在询问是否同时改变所有子物件的layer的时候选yes

这样我们可以看到,fg1和face的layer都变成了fg。

然后我们来新建一个只能照到fg这个layer,其他东西一概照不到的摄像机。

在hierarchy空白的地方右键新建一个camera,就起名叫rtcamera好了。将它inspector里边的各项设定改成这样

从上往下来说

transform里边的值不用管它,只要把z值修改为小于fg1的z值就好,等下我们要根据自己的需要在场景里把它手动拖到合适的位置。

clear flags和background保持原状就好。

culling mask决定了它会录制哪些layer的图片,我们当然是选择只录fg的,这是立绘专用的摄影棚嘛。

projection里边有两种选择,orthographic是我们2D游戏使用的,没有近大远小效果的正交摄像机。

size我填了和屏幕大小一样的5.4,但是这个等下可能还会有需要修改的时候。

剩下的就用默认的就好啦。虽然它提示说MSAA不工作,你可以把勾选掉,不过不管他也行。

把flare layer和audio listener勾掉或者右键删除,我们摄影棚里用不到这些。

在scene里拖放rtcamera到可以完整照到我们的fg1的位置,在选中rtcamera的时候右下角会显示出它摄像机里正在照着什么

如果你可能会有很大的立绘图片,那么你就需要调整rtcamera的size的大小,让它照到足够你需要的尺寸。

然后,我们在assets里右键新建一个render texture,就叫他rt1吧。

在hierarchy里边选中rtcamera,将rt1拖放到它的target texture里。

可以看到project里边的rt1的缩略图变成了rtcamera看到的东西

而同时rtcamera的取景框变成了和rt1的比例一样的正方形。

在project里边选择rt1来看他的inspector

我们将它的尺寸改成我们需要的大小,我的rtcamera是5.4,所以rt1我改成1080x1080,这样大概可以满足我大部分立绘不出框的需求。如果你的立绘用正方形放不下,可以根据自己的需要更改rt1和rtcamera的尺寸。rtcamera的尺寸决定了你相机的取景框有多大,rt1的尺寸决定了你照出来的照片有多大和是什么样的长宽比,我们当然可以让他不必生成和原图一比一的尺寸来减少游戏对内存的占用,但是那样的话,照出来的照片在拉伸到1比1的尺寸时会看起来有点糊,计算起来也不够方便,所以教程里我按照1比1的尺寸来生成立绘的摄影,实际操作的话请大家根据自己的需求来进行调整。

现在我们已经有了rt1这样一个照片了,现在我们想要把它用在场景里。

在canvas上右键,建立一个rawimage,就叫他rfg1好了。将rt1拖到它的texture那个格子里,就可以看到,它变成了rt1的图片。我们点一下set native size让它自己变成rt1本身的大小1080x1080,就会发现…………在屏幕上的尺寸不对=口=

在我这里直接出框了这么多呢。

我们来选中canvas,可以看到它的canvas scaler里默认的是constant pixel size

这个模式的意思是ui的分辨率适配你game那边的窗口大小,每个人的屏幕分辨率不一样,分配给game的显示窗口大小也不一样,这就导致ui这里可能我们每个人的同一张图等像素放上去,显示出来都不是同样的大小…………

所以我们把这个模式改为

这样我们针对1080的窗口做好的图,在ui里也能正常比例的显示了。

放在rfg1里的立绘也终于可以正常的显示了

而我们在淡入淡出的时候,对rfg1做淡入淡出动画就可以了

再也不会有图层重叠的穿帮了~

可能有人会问,既然反正都要用到ui了,那我一开始把立绘都用ui里的image来做,然后用canvas group来做淡入淡出不是也不会穿帮吗?(都能问出这种问题的人为什么会来看这个教程)

事实上我执着于用sprite来做立绘的原因还是因为现在AVG非常流行用动态立绘,这里用sprite做的立绘可以直接替换为各种有骨骼动画的立绘,而ui的image是不能做骨骼动画的……

如果立绘完全就是这样的静态图片的话,那么用image和canvas group配合做演出也是完全没有问题的,还能省掉一张render texture的内存。

总之我们的立绘测试显示是没有问题了。这时候我们发现,改了canvas模式之后,原来做的ui_dialog的尺寸又微妙的不对了。那么重新手拉一下吧,以后我们的ui都会在这个模式下进行编辑了。

现在我们canvas下的object可能是这样的

大家会发现,对话框被rfg1挡住了呢!

canvas的渲染方式是这样,和z值无关,在hierarchy里边靠下的物件会挡住上边的物件。我们将rfg1拖到ui_dialog上边,就可以解决这个问题了。

但是,再次话说回来,我们真的不可能只有fg1这一个立绘呢,假如我们还有fg2.png,fg2_face0.png,fg2_face1.png呢?(这个链接里是这三张图片的zip包,请自己解压后放到项目中去)

fg2.png

fg2_face0.png

fg2_face1.png

表情在ps里的坐标是160,164,请大家用之前讲到的转换公式来转换一下在unity里应有的坐标吧。

我假设你在屏幕上已经将fg2也拼好了,现在我们有fg1和fg2两个立绘放在场景里,我们希望能够用脚本随时控制是fg1进摄影棚还是fg2进摄影棚。

性急的小伙伴可能会说“为什么要在场景里做两个立绘呢?我们就像背景一样,在需要的时候将fg2和fg2的表情图片作为sprite挂在fg1上,然后用代码调整一下face的位置不就好了吗?”

如果是静态立绘的话,其实的确可以这样做呢。但是如果是有骨骼动画的动态立绘的话,两个角色的骨骼动画肯定是不一样的,所以我们还是把它在场景中分别作为fg1和fg2做出来,而不是像bg1一样只是放一个占位。

我们把fg1和fg2都放在摄影棚镜头最合适的位置

可以注意到,因为fg1.png和fg2.png的图片尺寸本身不一样,他们在摄影棚里合适的坐标值也是不一样的。

这非常不便于我们在需要的时候放他们进摄影棚,我可不想单独记住他们每个人最合适的位置在哪。

所以我们在rtcamera上右键建立一个相对于rtcamera位置为000的空gameobject,我们把它命名为fg1,然后将原本的fg1拖进去

再将原本的fg1改名为fg

这样,我们fg1立绘的根gameobject就是现在这个叫做fg1的相对于摄像机是000的空gameobject了,以后我们需要fg1进摄影棚的时候就放在rtcamera下000的位置,它就可以保证摄入的是我们现在摆放好的样子。

通常来说我们在游戏里肯定不会只有两个角色,可能会有十几个角色,当然不可能都把他们摆在场景里,我们希望在游戏过程中用到谁再载入谁。

所以我们把fg1从hierarchy里拖到resources里的fg文件夹里做成prefab。

可以看到hierarchy里边的fg1变成了蓝色,说明它已经被作为一个profab被保存在了assets里边。这样我们哪怕现在将hierarchy里边的fg1删掉,也随时可以将project里边保存好的fg1拖回去新建一个,甚至新建多个。如果我们在场景里边对fg1做了什么修改,只要将它再次拖到project里边的fg1上边就能够保存更新后的版本了。

现在为了屏幕的清爽我们就把hierarchy里边的fg1删掉吧。

对fg1所做的操作对fg2也重复的做一遍。

现在屏幕上已经一个立绘也没有了,非常的清爽。

我们的目标是将fg1fadein改为可以这样使用

@fg1fadein storage=fg2 n=1

所以我们就来修改fg1fadein和fg1fadeout吧


   public void fg1fadein(Dictionary<string, string> elm)

   {

        

                //如果摄影棚里已经有立绘在了,就先销毁它。

                Destroy(GameObject.Find("fgroot"));

                //我们从assets里边的fg1或者fg2(看脚本里指定用哪个了)复制一个克隆体fgroot用于放在场景中,之后的所有操作都是针对这个克隆体的,对我们储存好的assets里的fg1不会有任何影响

                GameObject fgroot = (GameObject)Instantiate(Resources.Load("fg/"+elm["storage"], typeof(GameObject)));

                //给它起个名字叫做fgroot,这样我们方便之后用GameObject.Find找它

                fgroot.name = "fgroot";

                //我们把它作为rtcamera的子物件

                fgroot.transform.SetParent(GameObject.Find("rtCamera").transform);

                //然后设定它的坐标为000

                fgroot.transform.localPosition = new Vector3(0, 0, 0);

                //为了防止拿出来的时候被碰坏了(误)把它的缩放值也回归到111

                fgroot.transform.localScale = new Vector3(1, 1, 1);

                //这样立绘就在摄影棚里摆好了,接下来我们只要淡入照片rfg1就行了。

                //这里从获取fg1改为了获取rfg1,请别忘了把rfg1的淡入淡出动画和动画控制都做好呢。

                GameObject temp = GameObject.Find("rfg1");

                Animator an = temp.GetComponent<Animator>();

                an.SetInteger("a_num", 1);

               StartCoroutine(waittime(1f));

        

        //设定好表情

GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/”+elm[“storage”]+”_face" + elm["n"], typeof(GameObject)));

temp2.name = "face1";

temp2.transform.SetParent(temp.transform.Find("fg/face"));

temp2.transform.localPosition = new Vector3(0, 0, -0.1f);

temp2.transform.localScale = new Vector3(1, 1, 1);

temp2.GetComponent<Animator>().Play("facea1", -1, 1);

    }

    public void fg1fadeout()

    {

                //这里也要从获取fg1改为获取rfg1,

                GameObject temp = GameObject.Find("rfg1");

                Animator an = temp.GetComponent<Animator>();

                an.SetInteger("a_num", 2);

                StartCoroutine(waittime(1f));

    }


由于显示立绘的机制改了,changeface也需要在获取GameObject等地方修改对应的名字,脚本也改为@changeface n=1 fg=fg1


void changeface(Dictionary<string, string> elm)

    {

        //获取fg1上的face,接下来的表情文件都会作为face的子文件来往上挂

                GameObject temp = GameObject.Find("fgroot").transform.Find("fg/face").gameObject;

        //如果有face0存在就删掉face0

               if (temp.transform.Find("face0"))

                {

                    Destroy(temp.transform.Find("face0").gameObject);

                }

        //获取face1,将face1改名为face0,放低一点,将上边的动画都停掉

               GameObject temp1 = temp.transform.Find("face1").gameObject;

                temp1.name = "face0";

                temp1.transform.localPosition = new Vector3(0, 0, 0);

                temp1.GetComponent<spriteconller>().activechildrenfalse();

GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/“+elm[“fg”]+”_face" + elm["n"], typeof(GameObject)));

                temp2.name = "face1";

                temp2.transform.SetParent(temp.transform);

                temp2.transform.localPosition = new Vector3(0, 0, -0.1f);

                temp2.transform.localScale = new Vector3(1, 1, 1);

                

//执行下一行脚本

                startscript();

    }


脚本也加上参数,改成这样


@bg1fadein storage=bg2

@fg1fadein storage=fg2 n=0

@dialogfadein

你吃不吃包子?

@textfadein

@textfadeout

@changeface fg=fg2 n=1

你不吃的话我就一个人都吃掉了。

@textfadein

@textfadeout

@dialogfadeout

@fg1fadeout

@bg1fadeout


点play的话,就可以看到不管是背景还是角色,都按照我们脚本选择的图片进行了

那么下次的教程将加入选择支的内容。

扩展阅读:如何绘制眨眼动画



TOP

访客数: 1192205
aa