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都可以随意使用了。

在下一篇教程里我们将一起做角色立绘切换和角色立绘的表情切换。



TOP

访客数: 3128303
aa