Galgame我很早就陆陆续续的接触过,但大都是别人推荐或名声很大、梗很多的,比如催泪的有clannad,狗血的有School Days,还有胃疼的白学。但自己对Galgame和AVG真正产生兴趣是从EVER17开始的,被最后一章解谜篇震撼到了,通关后在电脑前呆坐了几分钟,久久不能忘怀。然后就爆肝把KID社三部曲一下全推完,再接下来是5pb的石头门,InnocentGrey的壳女虚女…逐渐感受到了这种在市场上处于劣势的游戏类型的魅力。
这个Galgame框架我整个大一下学期一直都在酝酿,可惜当时的我就是一个麻瓜,什么东西都不会,只有各种空的目标和想法,都是虚的。现在,我终于掌握了一些C#和Unity的技能,虽说还是很生疏,许多深层的东西都还没碰到,但终于可以开始一点点实现自己以前脑子里的东西了,感谢自己保有的那么一点自律,在暑假玩游戏之余学到了点东西。
我之前也发过用Unity实现gal的尝试,企图不用控件完全靠GUI代码来作显示,以及定义Dialogue类来装一个对话场景等等,结果在存档问题上走到了死胡同。(以前的文章还是留着,方便对比)向一些B站的大佬们请教了一番,我明白了这类游戏设计中脚本的重要。在GALGAME中,脚本回归了其最初的定义—剧本。它需要被用来指导游戏的进行,即文字,背景,立绘,音效,bgm等的显示,大多数时候还需要指示选择支,实现多条线的剧情。来看一看书上的一些例子:
(源自佐佐木智广《游戏剧本怎么写》)
脚本如此重要,我们先来设计一套泛用Galgame脚本语言。(仅作参考,可以根据需求设计适合自己的),脚本是需要传进游戏解析的,便于解析,最好设定分隔符来把每一行内容丢进数组。我这样设计:
1.剧情对话脚本Dialogue(用标识符为D)
D| 说话人名字| 说话内容 | (语音)(还没实现到这种地步)
2.命令脚本Command (标识符为C)
格式:C| 命令 |...| 相关参数 |...|
具体的命令:
C|setbackground|背景名 (或路径) //设定目前的背景
C|setbgm|bgm名 (或路径)//当前bgm
C|stopbgm //停止bgm
C|setcharacter|立绘名|绘制位置(我设的left,center和right)//绘制立绘
C|erasecharacter|立绘位置 //隐藏某位置的立绘
之后还会有更多
3.注释脚本Note (标识符为*)
*| 注释内容 //用于写注释,游戏不会去解析
4 .选择支脚本Selection(本回不实现)
S|按钮1文字|按钮1跳转行|按钮2|按钮2跳转行....
大概就是这样,怎么舒服怎么来就行了
随便写了一个测试用的脚本:
*|格式:本行标识 语句
*|普通脚本:D|名字|对话
*|命令脚本:C|setbackground|background(路径)|0
*| C|setbgm|bgm(名字)| 播放bgm|0
*| C|setcharacter|立绘名|绘制位置(left,center,right)
*| C|erasecharacter|position|.|.
*| C|stopbgm|.|.|.
*|注释脚本:*在最前,不解析
*|选择脚本: S|按钮1文字|按钮1跳转行|按钮2文字|按钮2跳转行....
C|setbackground|street1
C|setcharacter|Girl1|center
D|Me|你好,这是一个普通脚本的测试
D|Me|这是下一句话,怎么样,解析成功了吗
C|setbgm|Bgm1
D|少女|这是一座很普通的小镇噢!跟我来,我来带你参观吧!
C|erasecharacter|all
D|我|就这样跟着少女在小镇中转了一个多小时,所见之物大都是司空见惯的,没有什么特别的感受
C|setbackground|Black
D|我|这种感觉没持续多久......直到......
D|我|走进了那一间房子......
C|setbackground|room1
设计了一套语法后,接下来我们要让游戏按行把它们解析出来:
首先,我们需要读入txt文件,然后把它处理成数组交给其他object处理。定义一个FileHandler脚本来读取,一个TextParser脚本来解析,这种类的定义我们最好都附带一个全局对象instance,方便其他脚本来用它。先来处理FileHandler:
//initialization
public static FileHandler FileHandler_Instance;//static object
public string Director_Path;
// on start:
FileHandler_Instance = this;
Director_Path = Application.streamingAssetsPath;//find streaming assets' path
要处理的文件都放在streamingAssets文件夹中,这个文件夹Unity是认得到的,通过Application.streamingAssetsPath来获取。然后是用ReadAllLines返回一个数组,交给TextParser处理。
public string[] ReadTxtFile(string filename)
{
string Path = Director_Path;
//Get the file
Path = Director_Path + "/" + filename;//definate path
//Debug.Log("readfile:" + Path);
//debug to test
string[] AllText = File.ReadAllLines(Path);//split by lines
return AllText;
}
然后来写Parser,算是全部脚本中最核心的地方了,因为后面还会定义更多东西,所以这个parser会经常更新。(LineIndex用来记录脚本解析到了第几行)
//objects
//static:
public static TextParser TextParser_Instance;
//public:
public string[] TextLines;//get returned string[]
public string[] TempScript;
public string TextFileName;//
public int LineIndex;//line
//on awake:
TextParser_Instance = this;//get instance
定义和初始化部分目前是这样,然后要获得字符串数组
public void GetText(string name,int index)
{
Debug.Log("Start reading FileText");
this.TextFileName = name;
this.LineIndex = 0;
TextLines = FileHandler.FileHandler_Instance.ReadTxtFile(TextFileName + ".txt");
//Get the Text
ParsingText(0);//start parsing the first line
}
我们获取到的每一行,用split将其分为几段,装进数组,先判断数组首元素的标识符来确定其类型,然后进行相关操作,相关函数后面来陆续添加,先用注释表示清楚。
public void ParsingText(int num)
{
TempScript = TextLines[num].Split('|');
//start parsing:
if(TempScript[0]=="*")
{
//the line is a note line
//(skip this )
LineIndex++;
return;
}
if(TempScript[0]=="C")
{
//the line is a command line
if(TempScript[1]== "setbackground")
{
//Background switch process
// (Set Background Method)
}
else if(TempScript[1]== "setbgm")
{
//Set current BGM
//(set BGM Method)
}
else if(TempScript[1]== "setcharacter")
{
if (TempScript[2] != "null")
{
//(Set Character Method)
}
}
else if(TempScript[1]== "erasecharacter")
{
//(erase Character Method)
}
else if (TempScript[1] == "stopbgm")
{
//(stop BGM Method)
}
ParsingText(num + 1);
LineIndex++;
return;
}//process command line
if(TempScript[1]=="S")
{
//Selection branch line
return;
}//selection end
//process a Dialogue line
//(Dialogue play Method)
把脚本都挂到空GameObject上,可以检查一下文本传入成功没有。
(传入成功的样子)
然后开始编写UI相关的东西了
创建UI的画布(Canvas),然后附上相关的按钮,窗口。
我们写一个脚本来控制所有的按钮操作(让按钮调用脚本中的方法ButtonClickEvent),用了很多setActive和Object.Find的方法,可是在Active为false(即物体未被激活)时,Find是找不到它的,该咋办?解决方法是令创建一个对象和脚本,把所有要用到的对象挂上去就找的到了.(ps:用到了SceneManagement和UI相关的控件,记得using UnityEngine.UI和UnityEngine.SceneManagement):
//Start_Menu Manager (用来挂对象)
public class StartMenu_Manager : MonoBehaviour {
public GameObject InfoPanel;
public GameObject Button_Start;
public GameObject Button_Exit;
public GameObject Button_About;
public GameObject Effect;
}
public class StartScene_Buttons : MonoBehaviour {
//public
public GameObject ChangeSceneEffect1;//to delay scene change
//private
private float AlphaSign = 0;//to display effects
// Use this for initialization
void Start () {
ChangeSceneEffect1 = GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Effect;
}
public void ButtonClickEvent()
{
//Debug.Log("clicked");
//Debug.Log(gameObject.name); (used for test)
switch(gameObject.name)
{
case "BTN_StartGame":
ChangeSceneEffect1.SetActive(true);
GameObject.Find("Menu_Particle").SetActive(false);
ChangeSceneEffect1.GetComponent<Image>().color = new Color(0, 0, 0, 0);
StartCoroutine("SwitchScene");
break;
case "BTN_About":
//Display Info Window
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().InfoPanel.SetActive(true);
//Set other Buttons's Active to false
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_Start.SetActive(false);
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_Exit.SetActive(false);
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_About.SetActive(false);
break;
case "BTN_ExitGame":
Application.Quit();
break;
case "BTN_BackToMenu":
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_Start.SetActive(true);
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_Exit.SetActive(true);
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().Button_About.SetActive(true);
GameObject.Find("StartMenu_GUI_Manager").GetComponent<StartMenu_Manager>().InfoPanel.SetActive(false);
break;
default:
break;
}
}
这个按钮脚本需要每一个按钮都挂一个,这样脚本就可以判断是谁触发了这个方法。
来写一个协程加一点场景转换时的渐变过渡,不然太生硬了
IEnumerator SwitchScene()
{
yield return new WaitForSeconds(0.01f);
while(AlphaSign<1)
{
yield return new WaitForSeconds(0.02f);
AlphaSign += 0.01f;
ChangeSceneEffect1.GetComponent<Image>().color = new Color(0, 0, 0, AlphaSign);
}
SceneManager.LoadScene(1);
}
还可以来一些骚操作,比如加点粒子效果,将Canvas改为camera来render,然后在相机前面加一个粒子系统,稍作调整效果就出来了。
(测试主菜单)
(测试窗口)
好像要讲的有点多,今天就先到这里吧,目前已经把基本的命令操作都实现了,正在搞存档的管理,还得学JSON,慢慢来吧