04简单的脚本控制
本教程由水螅制作,教程内所有提供下载的练习素材皆为水螅制作,只可用于练习,不可用于其他用途。
让我们来设计一组演出。我希望游戏开始后
先淡入背景bg1(执行bg1fadein)
然后淡入立绘fg1(执行fg1fadein)
然后淡入对话框,显示“你吃不吃包子?”(执行dialogfadein,textfadein)
等待点击
点击屏幕后对话切换为“你不吃的话,我就一个人都吃掉了。”(执行textfadeout,textfadein)
等待点击
再点击屏幕的话淡出对话框(执行textfadeout,dialogfadeout)
然后淡出fg1(执行fg1fadeout)
最后淡出bg1(执行bg1fadeout)
这些要顺序执行并且后边的等待前边执行一段时间再执行。所以我们要制作一个简单的脚本控制系统。
接下来不可避免的要写一些代码,虽然会是非常简单的代码,但是对于完全没有编程基础的小伙伴来说,可能会觉得很难懂。我个人建议先将代码copy到自己的项目中跑通了之后,针对自己的需求,试着去修改它,用得多了就会明白的……
将上边我们对演出的需求写成一个脚本
我们设定这个脚本是按行执行的,行首是@的情况下说明是个function,行首不是@的情况下说明这一行是要出现在对话框中的文字。那么我们的脚本应该长成这样
@bg1fadein
@fg1fadein
@dialogfadein
你吃不吃包子?
@textfadein
@textfadeout
你不吃的话,我就一个人都吃掉了。
@textfadein
@textfadeout
@dialogfadeout
@fg1fadeout
@bg1fadeout
请尽量通过想象每一行执行后的画面理解这个脚本顺序,这非常重要。
我们把这几行文字copy到一个txt文件里,就命名为start.txt好了。
在assets文件夹下建立一个resources文件夹。
将这个txt放在resources文件夹中。之所以要这样,是因为resources文件夹是一个unity特别指定可以在游戏运行过程中直接载入里边文件的文件夹。
然后我们要写一个function,用来将这个txt文件里的脚本内容读出来。
//设定一个放脚本的容器
string[] myscript;
//设定脚本读到的行数
int scriptnum = 0;
//从txt里读取脚本然后分行放入myscript里
void readscript()
{
//读取start.txt
TextAsset t1 = (TextAsset)Resources.Load("start", typeof(TextAsset));
//分行放入容器
myscript = Regex.Split(t1.text, "\r\n", RegexOptions.IgnoreCase);
//将脚本执行行数放在最开始
scriptnum = 0;
}
我们在上边这段代码里用到了新的类型string[](字符串数组),int(整数),TextAsset,前两者是c#自己的类型,请看c#语法,TextAsset是unity的一个类型,具体功能请看unity文档。
Resources是UnityEngine里边定义的类,它的静态函数load函数就是从项目assets/resources文件夹里边载入文件。
第一个参数是文件名,也就是我们start.txt的文件名“start”
第二个参数是要取出来的文件的类型,txt文件我们要取出来需要用TextAsset类型来取,所以第二个参数就这么填。
(类型)
这种用法是强行转换后边的东西的类型。Resources.Load("start", typeof(TextAsset))得到的是一个获取了start.txt内容的object,需要强行用(TextAsset)把它转成TextAsset类型才能把它用等号赋值给TextAsset类型的t1。强类型语言就是这样,类型不同是不能用等号的。
Regex是System里的一个类,想要直接用它需要在文件头加上using System.Text.RegularExpressions;不然就必须要写全名字System.Text.RegularExpressions.Regex这样才能用……
Split是Regex用于分割字符串的一个静态函数,它会把一个长字符串分割成字符串数组(string[])返回回来
第一个参数是字符串,我们填的是t1.text,t1是我们刚从start.txt里边读取了内容生成的一个TextAsset,text是TextAsset的属性,功能是把内容作为一个字符串(string)取出来。
第二个参数是长字符串用什么字符串来分割,比如我们有一个长字符串是”aabccbdd”,我们将分割字符串指定为”b”,分割后就得到了”aa””cc””dd”三个字符串。现在我们想要把写好的脚本按行来分割,所以就将分割字符串指定为换行符”\r\n”。在mac下换行符可能不是”\r\n”而是”\n”,如果你是mac的话请改一下。
第三个参数是字符串的匹配方式,种类见https://docs.microsoft.com/zh-cn/dotnet/api/system.text.regularexpressions.regexoptions?view=netframework-4.7.2
这样我们就是将写在start.txt里的脚本文件按行放在了myscript这个字符串数组里,
现在myscript[0]就是”@bg1fadein”
myscript[1]就是”@fg1fadein”
…………以此类推
然后将我们要开始读取的行数设为第一行也就是0。
你可能注意到了,我们将myscript和scriptnum定义在了function的外边。在c#(和很多其他语言)里边,变量只在它存在的那个{}内有效,比如说我们有一个class
class a1
{
int v1=1;
void b1()
{
v1=v1+1;
int v2=2;
}
}
v1被定义在a1的{}里边,也就是说在这花括号里边的所有位置,v1都是有效存在的,你什么时候都可以直接用它,包括在b1这个function里边也可以直接用它
但是v2是定义在b1的{}里边的,出了b1的花括号就不再存在v2。
大部分时候我们的变量就只在function内部使用,执行完function就希望他们被销毁,以免影响到我们其他的东西。
不过像是要一直执行的脚本文字这样的数据,我们读取完之后还会在别的function里用到它,所以我们将它定义在function之外,这样在整个root_script的class里边,都随时可以使用这些数据。
现在我们来写一个一行一行的依次读取脚本并执行的function
//开始执行脚本
void startscript()
{
while(onscript())
{
}
}
//设定一个存放对话框要放什么文字的变量
string dtext = "";
bool onscript()
{
if(scriptnum>=myscript.Length)
{
//当执行行数超过脚本总行数的时候返回false停止继续执行
return false;
}
//将当前要执行的那一行的脚本赋值给叫做line的变量
string line = myscript[scriptnum];
//因为已经读取了当前要执行的那行,所以执行行数增加1,这样下次就会读取下一行了。scriptnum++相当于scriptnum=scriptnum+1
scriptnum++;
//将这行脚本在console里输出出来
Debut.Log(line);
if(line.Length==0)
{
//如果这行为空就跳过这行
return true;
}
else if(line.Substring(0,1)=="@")
{
//如果行首有@就作为一个function执行
SendMessage(line.Substring(1));
return false;
}
else
{
//如果行首没有@就把文字赋值给dtext等待对话框里显示
dtext = line;
return true;
}
}
这些代码里while是一个循环判断的语句,它会执行一下后边()里的句子,然后看返回的值是true还是false,当是true的时候就再执行一遍直到返回值是false的时候就会停下来。具体请看c#语法https://docs.microsoft.com/zh-cn/dotnet/csharp/tutorials/intro-to-csharp/branches-and-loops?tutorial-step=3
我们放在里边反复执行的语句是onscript(),也就是我们在接下来定义的那个function。
可以看到onscript()这个function的定义的类型是bool,这个类型只有两种值true或者false,我们每次执行onscript会根据执行的结果不同得到一个true或一个false
我们之前写的函数都是void类型的,不需要有任何返回值,如果是bool这种需要返回值的类型的函数,函数内部一定要有return语句将对应的值返回回来才行。
上边的代码里之前没有用到过的类型是string(字符串),前边我们用到过string[](字符串数组),string[]是一组string的意思
还用到了Debut.Log这个静态函数,这个函数的功能是把()内传入的值在console里显示出来,它不影响游戏本身的运行,主要是为了给制作者自己看游戏运行途中都发生了什么。当你遇到bug的时候,这是个非常重要的debug手段。我们在这里写的是每当执行一行脚本之前就在console里写一下这行脚本的内容,这样除了错很容易判断是错在了哪一行。
onscript里边我们用到了if结构,因为太基础了我不是很想啰嗦了……请看c#语法https://docs.microsoft.com/zh-cn/dotnet/csharp/tutorials/intro-to-csharp/branches-and-loops?tutorial-step=1
里边还用到了string这个类型的一个函数Substring,这个函数将这个字符串截取要求的一段来返回,
第一个参数是从第几个字开始截取
第二个参数是截取多少个字,如果不填的话就是截取到最后。
另外一个之前没用过的叫做SendMessage的function是MonoBehaviour自己的函数,因为root_script继承了MonoBehaviour,所以可以直接这样使用MonoBehaviour的函数。这个函数的功能是调用作为字符串传入的名字的function。SendMessage(“bg1fadein”);相当于bg1fadein();
然后我们注意到,想要实现我们计划的演出效果,@textfadein除了要将文字显示出来外,还包含着将dtext里边的文字赋值给ui_dialog_text,以及等待点击的功能,所以我们将原来的textfadein修改一下
//设定一个判断是否在等待点击的变量
bool onwaitclick = false;
public void textfadein()
{
GameObject temp = GameObject.Find("ui_dialog_text");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
//将dtext里边存的文字赋值给画面,为了能够使用Text,需要在文件头加上using UnityEngine.UI;
temp.GetComponent<Text>().text = dtext;
//设定开始等待点击
onwaitclick = true;
}
然后我们在空着的Update()里加上对于鼠标左键点击的判断
void Update () {
//当正在等待点击的时候鼠标点击了就继续读下一行脚本
if (onwaitclick && Input.GetMouseButton(0))
{
onwaitclick = false;
startscript();
}
}
其中Input.GetMouseButton(0)就是鼠标是否点击了左键的判断,如果是就返回true,否就是false,这个判断放在每一帧执行一次,在游戏运行的途中不管什么时候玩家如果点击了左键并且onwaitclick是true就可以执行if里边的内容了。
Input是unity里管理输入事件的类,具体见https://docs.unity3d.com/ScriptReference/Input.html
我们还需要写一个“等待多少时间然后执行下一行脚本”的function,用来在每个淡入淡出的function里调用,比如开始执行淡入背景之后,我们应该等待1秒待背景完全淡入之后,再执行下一行淡入立绘的脚本
IEnumerator waittime(float time)
{
yield return new WaitForSeconds(time);
startscript();
}
IEnumerator这个类型的函数里,我们可以用yiele return来设定等待时间。
通常的函数比如
void b1()
{
int a=1;
int b=2;
}
里边执行完a=1就会立刻执行b=2,不会有任何时间间隙在里边
而IEnumerator函数
IEnumerator b1()
{
int a=1;
yield return new WaitForSeconds(1f);
int b=2;
}
的话,a=1之后会等待1秒钟再执行b=2
要调用IEnumerator类型的函数不能直接写waittime(0.1f);而是要开个线程StartCoroutine(waittime(0.1f));这样使用
另外我们好像是第一次写一个可以传入参数的function,(float time)这个部分表示,我在调用waittime的时候会在括号里传入一个float(浮点数)类型的值并把它放进叫做time的变量里,在c#里边浮点数后边要加一个f,比如0.1就写作0.1f。
把调用waittime的语句放进每个fadein fadeout的function里。
最后我们在start()里调用readscript()和startscript()
整个root_script就是这样的
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text.RegularExpressions;
using UnityEngine.UI;
public class root_script : MonoBehaviour {
// Use this for initialization
void Start ()
{
readscript();
startscript();
}
// Update is called once per frame
void Update ()
{
//当正在等待点击的时候鼠标点击了就继续读下一行脚本
if (onwaitclick && Input.GetMouseButton(0))
{
onwaitclick = false;
startscript();
}
}
public void bg1fadein()
{
GameObject temp = GameObject.Find("bg1");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(1f));
}
public void bg1fadeout()
{
GameObject temp = GameObject.Find("bg1");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(1f));
}
public void fg1fadein()
{
GameObject temp = GameObject.Find("fg1");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(1f));
}
public void fg1fadeout()
{
GameObject temp = GameObject.Find("fg1");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(1f));
}
public void dialogfadein()
{
GameObject temp = GameObject.Find("ui_dialog");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(1f));
}
public void dialogfadeout()
{
GameObject temp = GameObject.Find("ui_dialog");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(1f));
}
bool onwaitclick = false;
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;
}
public void textfadeout()
{
GameObject temp = GameObject.Find("ui_dialog_text");
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 2);
StartCoroutine(waittime(0.5f));
}
//设定一个放脚本的容器
string[] myscript;
//设定脚本读到的行数
int scriptnum = 0;
//从txt里读取脚本然后分行放入myscript里
void readscript()
{
//读取start.txt
TextAsset t1 = (TextAsset)Resources.Load("start", typeof(TextAsset));
//分行放入容器,为了使用Regex,需要在文件头加上using System.Text.RegularExpressions;
//这里的分隔符如果是mac系统可能要改成"\n"才能正常运行
myscript = Regex.Split(t1.text, "\r\n", RegexOptions.IgnoreCase);
//将脚本执行行数放在最开始
scriptnum = 0;
}
//开始执行脚本
void startscript()
{
while(onscript())
{
}
}
//设定一个存放对话框要放什么文字的变量
string dtext = "";
bool onscript()
{
if(scriptnum>=myscript.Length)
{
return false;
}
string line = myscript[scriptnum];
print(line);
scriptnum++;
if(line.Length==0)
{
//如果这行为空就跳过这行
return true;
}
else if(line.Substring(0,1)=="@")
{
//如果行首有@就作为一个function执行
SendMessage(line.Substring(1));
return false;
}
else
{
//如果行首没有@就把文字赋值给dtext等待对话框里显示
dtext = line;
return true;
}
}
IEnumerator waittime(float time)
{
yield return new WaitForSeconds(time);
startscript();
}
}
红字是在上次的基础上改掉的部分,可以看到其实我们并没有用到很多代码,而且大部分代码都非常的简单。
因为要用脚本来控制淡入淡出了,之前测试淡入淡出的按钮们可以都删掉了。
现在我们点play按钮
但是,一个游戏不可能只有一个背景和一个立绘的……假如我们有多张背景和多张立绘呢……现在我们有第二张背景bg2.jpg了。
我想要@bg1fadein的时候可以自己控制淡入bg1还是bg2。这意味着我们要在游戏运行途中读取bg1或者bg2,所以我们需要把这两张jpg放入resources文件夹(在游戏中读取资源并不是只有这一种方法,但是这是比较直观简单的一种,所以教程里我会主要用这种方法)
在resources里边建立一个bg文件夹,将bg1和bg2的图片拖进去,bg1的动画文件和动画管理文件就不必了,resources文件夹里请确保只放了会在游戏途中用代码直接读取的资源,被读取的资源可能引用的资源则不用放在里边。
我们将显示背景的那行脚本改为
@bg1fadein storage=bg2
曾经用过krkr的小伙伴会对这种格式的脚本非常亲切吧。现在我们要将解析脚本的function里边加一点对后边的参数的解析。
bool onscript()
{
if(scriptnum>=myscript.Length)
{
return false;
}
string line = myscript[scriptnum];
scriptnum++;
if(line.Length==0)
{
//如果这行为空就跳过这行
return true;
}
else if(line.Substring(0,1)=="@")
{
//如果行首有@就作为一个function执行
//建立一个叫做elm的字典来放置脚本传进来的参数
Dictionary<string, string> elm = new Dictionary<string, string>();
//先将这一行脚本用空格分割开。
string[] temparr = Regex.Split(line.Substring(1), " ", RegexOptions.IgnoreCase);
//分割出来的第一个字串是function的名字,放到叫做macroname的变量里等会用。
string macroname = temparr[0];
//如果后边还有参数的话就再用=分割开它们,把它们放进elm里
if (temparr.Length > 1)
{
for (int i = 1; i < temparr.Length; i++)
{
string[] temparr1 = Regex.Split(temparr[i], "=", RegexOptions.IgnoreCase);
elm[temparr1[0]] = temparr1[1];
}
}
//按照有参数和没参数两种来使用SendMessage
if (elm.Count > 0)
{
SendMessage(macroname,elm);
}
else
{
SendMessage(macroname);
}
return false;
}
else
{
//如果行首没有@就把文字赋值给dtext等待对话框里显示
dtext = line;
return true;
}
}
红色的字是修改的部分。事实上这样分割脚本其实不是很牢靠,比如手滑脚本里多写个空格就会崩了,正式使用的话用正则来分割会更好一些,我为了让代码足够简单好懂所以就写得比较死板。
这段代码里之前没有用过的是Dictionary字典类型,具体见https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.generic.dictionary-2?view=netframework-4.7.2
更改了解析脚本的地方之后,我们还要修改一下bg1fadein,让它能够接受参数并且把图片换掉
public void bg1fadein(Dictionary<string, string> elm)
{
GameObject temp = GameObject.Find("bg1");
//读取通过elm传进来的图片名的图片,把它挂在bg1上显示出来
temp.GetComponent<SpriteRenderer>().sprite=(Sprite)Resources.Load("bg/"+elm["storage"], typeof(Sprite));
Animator an = temp.GetComponent<Animator>();
an.SetInteger("a_num", 1);
StartCoroutine(waittime(1f));
}
改完之后play一下,可以看到一开始的背景显示为了bg2
之后哪怕再有bg3 bg4 bg5都可以随意使用了。
在下一篇教程里我们将一起做角色立绘切换和角色立绘的表情切换。
Created by Hydrozoa.2011
不支持IE7以下浏览器
凯恩插件程序:Hydrozoa 美术:Hydrozoa,红渊