08完善演出,音乐,音效,语音
本教程由水螅制作,教程内所有提供下载的练习素材皆为水螅制作,只可用于练习,不可用于其他用途。
今天一开始打算放缓脚步,用我们已经熟悉的方法来对演出做一些完善,方便接下来的save load功能的制作。
首先先从同屏多个角色开始。毕竟游戏不可能全程只有一个角色站在中间说话的嘛。在这个教程里我们设定为同屏能出现左中右三个角色。
那么,根据教程5的内容,我们需要制作三个摄影棚,并在canvas下放置3个rawimage。
我们将原来放在project里边的rt1改名为rtc,作为描绘中间立绘的render texture,然后在它旁边新建两个新的render texture分别明明为rtr和rtl,和rtc的参数填成一样的,作为描绘左边和右边的立绘的画布。
接下来我们选中场景中的立绘用摄像机rtCamera,将它改名为rtCamerac,并复制两个相同的摄像机,分别命名为rtCamerar和rtCameral,把三个摄像机拉到相互不碍事的地方,确保放的够远,放在一个摄影棚里的东西不会被别的摄像机照到就行
然后将rtl和rtr分别拖放到对应摄像机的target texture上
再将场景中用来显示立绘的rfg1改名为rfgc,将它复制两份做成rfgl和rfgr,分别放在屏幕左右合适的位置。将rtr和rtl拉到rfgr和rfgl的texture里。为了确定效果可以先在三个摄影棚里都放上立绘。
值得注意的是rfgl和rfgr是直接复制来的,所以淡入淡出动画也用的是同样的,如果想要不同位置的立绘显示用不同的动画,可以另外建立animator拖到他们的controller上边。
我目前只用了简单的透明度变化淡入立绘,不过因为unity有个方便的动画录制,所以想要做一些其他的效果也很容易,并且修改起来不太会影响到别的部分。比如我记得《西格玛和音》的角色出现好像是这样的
很容易就可以做好并且不会有需要改变代码的部分……这是我很喜欢unity的地方。
因为增加了左中右的立绘,显示立绘的部分也需要加上一个左中右的参数。
脚本部分改为这样的格式
@fg1fadein storage=fg1 pos=l n=0
@fg1fadeout pos=l
然后在fg1fadein和fg1fadeout里边对位置pos和表情序号n进行接收
public void fg1fadein(Dictionary<string, string> elm)
{
Destroy(GameObject.Find("fgroot"+elm["pos"]));
GameObject fgroot = (GameObject)Instantiate(Resources.Load("fg/"+elm["storage"], typeof(GameObject)));
fgroot.name = "fgroot"+elm["pos"];
fgroot.transform.SetParent(GameObject.Find("rtCamera"+elm["pos"]).transform);
fgroot.transform.localPosition = new Vector3(0, 0, 0);
fgroot.transform.localScale = new Vector3(1, 1, 1);
//我们用变量记录一下这个位置传入的文件名
f["fg" + elm["pos"]] = elm["storage"];
GameObject temp = GameObject.Find("Canvas").transform.Find("rfg"+elm["pos"]).gameObject;
temp.SetActive(true);
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(1f));
//将传入的表情赋值给立绘
GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/" + f["fg" + elm["pos"]] + "_face" + elm["n"], typeof(GameObject)));
temp2.name = "face1";
temp2.transform.SetParent(fgroot.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(Dictionary<string, string> elm)
{
GameObject temp = GameObject.Find("rfg"+elm["pos"]);
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(1f));
}
现在我们来做一下脚本里边的多人对话
@bg1fadein storage=op1
@opbtfadein
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@textfadeout
@fg1fadein storage=fg2 pos=r n=0
你能不能不要一直都是包子包子。
@textfadein
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
然后发现…………因为对话框上没有角色姓名,所以根本看不出来是谁在说话好嘛……
所以我们来给对话框加个姓名框,并增加一个动画图标用来在等待点击的时候作为提示。动画图标所需的图片是drop.png
给用于做为提示图标的image设定颜色并录制动画,起名叫drop,这个动画是反复播放的所以不需要和以前的淡入淡出动画一样勾选掉动画文件的loop,因为并不是一直要出现的,所以先把水滴动画的active的勾勾掉。
用bt.png作为显示姓名的底图,起名叫nameimage,给他增加一个Text的子object,调整好文字的大小和颜色等。对话框的结构现在大概是这样
给nameimage增加canvas group的component,录制它的淡入淡出动画,写好namefadein和namefadeout两个function
void namefadein(Dictionary<string, string> elm)
{
GameObject temp = GameObject.Find("Canvas").transform.Find("ui_dialog/nameimage").gameObject;
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
//将脚本写的显示在姓名框里
temp.transform.Find("Text").gameObject.GetComponent<Text>().text = elm["name"];
//我们希望姓名显示完整后再显示后边的对话,所以这里等待0.4秒
StartCoroutine(waittime(0.4f));
}
void namefadeout()
{
GameObject temp = GameObject.Find("Canvas").transform.Find("ui_dialog/nameimage").gameObject;
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
//我们希望姓名消去本身不等待就执行下边的内容,所以这里没有用waittime而是直接执行了下一句脚本
startscript();
}
将脚本改为
@bg1fadein storage=op1
@opbtfadein
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@namefadein name=宿彦
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
跑起来是这样
稍微好一点,但是我们做的水滴动画还没有用上。
水滴动画的目标是在ui_dialog_text完全显示出来后开始显示,在玩家点击之后消去。取到动画完全显示的时候执行function可以使用我们之前用过的动画事件来做,不过这次我打算顺便做一下别的效果。
之前为了方便,我们的文字是直接用的淡入,不过事实上,大部分avg的常见文字显示方式是一个一个的蹦字。所以我们来把textfadein修改一下,从淡入动画变成蹦字。
首先把ui_dialog_text的默认透明度拉满,把示例用文字删掉。将淡入动画在动画编辑界面改成直接全部显示的1帧动画,然后修改textfadein
public void textfadein()
{
//原来文字赋值不要了,改成将文字清空
GameObject temp = GameObject.Find("ui_dialog_text");
temp.GetComponent<Text>().text = "";
//dtext = "";
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
//开始等待点击也不要了,留在文字完全出现之后再执行这一句
//onwaitclick = true;
//因为蹦字是有等待时间的,所以我们需要将蹦字写成一个IEnumerator函数,用StartCoroutine来调用它
StartCoroutine(showtext());
}
IEnumerator showtext()
{
GameObject temp = GameObject.Find("ui_dialog");
Text t1 = temp.transform.Find("ui_dialog_text").gameObject.GetComponent<Text>();
t1.text = "";
//这里每显示一个字等待0.1秒
while(dtext.Length>0)
{
t1.text = t1.text + dtext.Substring(0, 1);
dtext = dtext.Substring(1);
yield return new WaitForSeconds(0.1f);
}
//所有文字显示完成后显示水滴动画并设定等待点击
temp.transform.Find("drop").gameObject.SetActive(true);
onwaitclick = true;
}
然后把接受点击那里加上消去水滴动画
void Update () {
//当正在等待点击的时候鼠标点击了就继续读下一行脚本
if (onwaitclick && Input.GetMouseButton(0))
{
onwaitclick = false;
GameObject.Find("ui_dialog").transform.Find("drop").gameObject.SetActive(false);
startscript();
}
}
跑一下,就是这种感觉。
稍微有一点程序基础的小伙伴可以尝试修改代码增加当对话蹦字的时候点击屏幕则立刻显示全部字进入等待点击的状态这样的功能,因为非常简单就不在教程里啰嗦了。
接下来,我们让对话变得长一点,给脚本增加改变角色表情的的句子。对changeface添加一下对左中右位置的接受就好。我们使用这样的脚本格式,用pos来传递位置,用n来传递表情序号。
@changeface pos=l n=1
那么function修改为接受pos
void changeface(Dictionary<string, string> elm)
{
GameObject temp = GameObject.Find("fgroot" + elm["pos"]).transform.Find("fg/face").gameObject;
if (temp.transform.Find("face0"))
{
Destroy(temp.transform.Find("face0").gameObject);
}
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/" + f["fg" + elm["pos"]] + "_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=op1
@opbtfadein
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@namefadein name=宿彦
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@changeface pos=l n=1
@namefadein name=宿彦
你这种人哪里懂得包子的好。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
跑一下可以看到,不同位置立绘的 表情控制已经很好的完成了。
现在我们再完善一下,增加当一个人在说话的时候,旁边人的立绘变暗下去的功能。
我们给@namefadein增加一个当前说话的人的位置的参数,写成这样
@namefadein name=宿彦 pos=l
然后去修改namefadein
void namefadein(Dictionary<string, string> elm)
{
GameObject temp = GameObject.Find("Canvas").transform.Find("ui_dialog/nameimage").gameObject;
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
temp.transform.Find("Text").gameObject.GetComponent<Text>().text = elm["name"];
StartCoroutine(waittime(0.4f));
//将设定的位置的立绘变亮,其他位置的立绘变暗
GameObject.Find("Canvas").transform.Find("rfgr").gameObject.GetComponent<RawImage>().color = new Color32(140,140,140,255);
GameObject.Find("Canvas").transform.Find("rfgl").gameObject.GetComponent<RawImage>().color = new Color32(140, 140, 140, 255);
GameObject.Find("Canvas").transform.Find("rfgc").gameObject.GetComponent<RawImage>().color = new Color32(140, 140, 140, 255);
GameObject.Find("Canvas").transform.Find("rfg"+elm["pos"]).gameObject.GetComponent<RawImage>().color = new Color32(255, 255, 255, 255);
}
因为namefadein修改了图片的暗度,为了防止有些立绘出场就是暗的,我们在fg1fadein里强行将要出场的立绘图片回复正常
public void fg1fadein(Dictionary<string, string> elm)
{
Destroy(GameObject.Find("fgroot" + elm["pos"]));
GameObject fgroot = (GameObject)Instantiate(Resources.Load("fg/"+elm["storage"], typeof(GameObject)));
fgroot.name = "fgroot"+elm["pos"];
fgroot.transform.SetParent(GameObject.Find("rtCamera"+elm["pos"]).transform);
fgroot.transform.localPosition = new Vector3(0, 0, 0);
fgroot.transform.localScale = new Vector3(1, 1, 1);
//我们用变量记录一下这个位置传入的文件名
f["fg" + elm["pos"]] = elm["storage"];
GameObject temp = GameObject.Find("Canvas").transform.Find("rfg"+elm["pos"]).gameObject;
temp.SetActive(true);
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(0.8f));
//将传入的表情赋值给立绘
GameObject temp2 = (GameObject)Instantiate(Resources.Load("fg/" + f["fg" + elm["pos"]] + "_face" + elm["n"], typeof(GameObject)));
temp2.name = "face1";
temp2.transform.SetParent(fgroot.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);
temp.GetComponent<RawImage>().color = new Color32(255, 255, 255, 255);
}
这里我为了简单省事,将立绘变暗做成了立刻变暗的效果,如果你有精力,可以将这个也做成淡入变暗的动画效果。使用我们之前做过多次的动画控制代码即可。
现在将脚本变成这样
@bg1fadein storage=op1
@opbtfadein
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@namefadein name=宿彦 pos=l
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌 pos=r
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@changeface pos=l n=1
@namefadein name=宿彦 pos=l
你这种人哪里懂得包子的好。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
跑起来就差不多是这样了。
已经很像一个正常的文字avg了,接下来我们给它添加上背景音乐
在hierarchy里边右键Audio->Audio Source,给新建的Audio Source起名叫bgm。把play on awake勾掉,loop勾选上
在resources文件夹里建立一个叫做bgm的文件夹,任意找两个你手边的mp3音乐文件,起名叫bgm1和bgm2放进去。如果你手边没有音乐文件,可以到unity assets store里下载一些免费的音乐文件
我们来写一个用来播放背景音乐的function,叫做bgm,再写一个用来停止音乐的function叫做stopbgm
void bgm(Dictionary<string, string> elm)
{
//将当前播放的音乐记录在变量里
f["bgm"] = elm["storage"];
//获取用于播放bgm的audiosource
AudioSource temp = (AudioSource)GameObject.Find("bgm").GetComponent<AudioSource>();
//载入脚本指定的音乐文件
temp.clip = (AudioClip)Resources.Load("bgm/" + elm["storage"], typeof(AudioClip));
//开始播放
temp.Play();
//读取下一行脚本
startscript();
}
void stopbgm()
{
f["bgm"] = "";
AudioSource temp = (AudioSource)GameObject.Find("bgm").GetComponent<AudioSource>();
temp.Stop();
//读取下一行脚本
startscript();
}
然后在脚本里加入控制音乐的语句
@bg1fadein storage=op1
@bgm storage=bgm1
@opbtfadein
@bgm storage=bgm2
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@namefadein name=宿彦 pos=l
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@stopbgm
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌 pos=r
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@changeface pos=l n=1
@namefadein name=宿彦 pos=l
你这种人哪里懂得包子的好。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
跑一下就可以看到,背景音乐已经很好的在我们指定的位置播放了出来然后在我们指定的位置停下了。
那么,和上边的操作相同的建立播放音效文件Audio Source,起名叫se,以及播放语音文件的Audio Source,起名叫voice
这两个Audio Source和bgm的区别在于,loop那里请取消勾选,因为音效和语音都是只播放一次的。
在resources文件夹里分别建立叫做se和voice的文件夹,将你想要测试使用的音效文件和语音文件放进去。我假设你有se1,se2和fg1voice1,fg1voice2等文件
我们先来写播放音效用的fuction se,和bgm差不多,但是音效有时候会有“等待这个音效播放完成再进行接下来的脚本”这样的需求,我们用wait来传递这个参数,也就是说,我们的脚本可能有
@se storage=se1
和
@se storage=se1 wait=1
这样两种,后一种会强制等待到音效播放完成再执行后边的脚本。
所以我们的function要这样写
void se(Dictionary<string, string> elm)
{
//如果脚本层没有写wait,就在这里手动将wait设置为0
if(!elm.ContainsKey("wait"))
{
elm["wait"] = "0";
}
//播放音效
AudioSource temp = (AudioSource)GameObject.Find("se").GetComponent<AudioSource>();
temp.clip = (AudioClip)Resources.Load("se/" + elm["storage"], typeof(AudioClip));
temp.Play();
//判断是否对音效进行等待
if (elm["wait"]=="1")
{
StartCoroutine(waittime(temp.clip.length));
}
else
{
startscript();
}
}
void stopse()
{
AudioSource temp = (AudioSource)GameObject.Find("se").GetComponent<AudioSource>();
temp.Stop();
startscript();
}
然后在脚本里插入播放音效的语句
@bg1fadein storage=op1
@bgm storage=bgm1
@opbtfadein
@bgm storage=bgm2
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@se storage=se1
@namefadein name=宿彦 pos=l
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@stopbgm
@se storage=se2 wait=1
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌 pos=r
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@changeface pos=l n=1
@namefadein name=宿彦 pos=l
你这种人哪里懂得包子的好。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
因为我的测试用se长度都很短,所以并没有在脚本里加入@stopse的测试,如果你的音效长度够长,也可以插入stopse来测试一下
接下来是语音。
void voice(Dictionary<string, string> elm)
{
f["voice"] = elm["storage"];
//我们将语音播放的function独立出来,用于在重放语音的时候也能进行调用
voice1(elm["storage"]);
startscript();
}
void voice1(string storage)
{
//播放语音
AudioSource temp = (AudioSource)GameObject.Find("voice").GetComponent<AudioSource>();
temp.clip = (AudioClip)Resources.Load("voice/" + storage, typeof(AudioClip));
temp.Play();
}
void stopvoice()
{
stopvoice();
startscript();
}
void stopvoice1()
{
AudioSource temp = (AudioSource)GameObject.Find("voice").GetComponent<AudioSource>();
temp.Stop();
}
虽然和播放音效没有什么不同,不过语音会有重放语音的需求。
我们来在对话框上制作一个小喇叭的按钮voicebt用来重放语音。小喇叭素材请点此。
将它的默认active勾掉。
现在对话框结构应该是这样
我们为它写一个重放语音的fuction
public void revoice()
{
voice1(f["voice"]);
}
将这个function挂载在voicebt上
然后我们在voice里边加入显示voicebt的语句
void voice(Dictionary<string, string> elm)
{
f["voice"] = elm["storage"];
voice1(elm["storage"]);
startscript();
GameObject.Find("ui_dialog").transform.Find("voicebt").gameObject.SetActive(true);
}
然后在textfadeout里边加入消去voicebt的语句
public void textfadeout()
{
GameObject temp = GameObject.Find("ui_dialog_text");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(0.5f));
GameObject.Find("ui_dialog").transform.Find("voicebt").gameObject.SetActive(false);
}
现在我们在脚本里加入播放语音的语句
@bg1fadein storage=op1
@bgm storage=bgm1
@opbtfadein
@bgm storage=bgm2
@editnamefadein
@initf
@bg1fadein storage=bg1
@fg1fadein storage=fg1 pos=l n=0
@dialogfadein
@se storage=se1
@namefadein name=宿彦 pos=l
@voice storage=fg1voice1
今天天气不错,
@ch text=name
,你要吃包子吗?
@textfadein
@namefadeout
@textfadeout
@stopbgm
@se storage=se2 wait=1
@fg1fadein storage=fg2 pos=r n=0
@namefadein name=费斌 pos=r
你能不能不要一直都是包子包子。
@textfadein
@namefadeout
@textfadeout
@changeface pos=l n=1
@namefadein name=宿彦 pos=l
@voice storage=fg1voice2
你这种人哪里懂得包子的好。
@textfadein
@namefadeout
@textfadeout
@dialogfadeout
@fg1fadeout pos=l
@fg1fadeout pos=r
@bg1fadeout
跑到有语音重放按钮的时候点一下语音重放按钮,发现………………虽然重放了语音,但是点击同时相当于点击屏幕的执行了下一行。所以我们要在update里边接受点击的地方修改为如果点击在了按钮上,就不算是点击屏幕
void Update () {
//当正在等待点击的时候鼠标点击了就继续读下一行脚本
if (onwaitclick && Input.GetMouseButton(0)&&!EventSystem.current.IsPointerOverGameObject())
{
onwaitclick = false;
GameObject.Find("ui_dialog").transform.Find("drop").gameObject.SetActive(false);
startscript();
}
}
为了能够用EventSystem.current.IsPointerOverGameObject(),我们还需要在文件头加上
using UnityEngine.EventSystems;
顺便一提EventSystem.current.IsPointerOverGameObject()在pc平台有用,但是手机上是没有用的,手机上要用EventSystem.current.IsPointerOverGameObject(Input.touches[0].fingerId)
总之,修改完毕之后来跑一下吧,可以看到,语音播放和我们预期的一样很好的运行了起来。
那么这些部分都完成之后,下一次就可以来做save load系统了。
Created by Hydrozoa.2011
不支持IE7以下浏览器
凯恩插件程序:Hydrozoa 美术:Hydrozoa,红渊