十二、敌人受攻击时的闪烁和Player的生命值的修正
上一篇中,我们利用Controller2D中的IEnumerator TakenDamage接口,使得我们的Player受到攻击时会进行闪烁,我们同样地也希望在我们的敌人身上可以实现相同的效果。所以我们现在需要复制Controller2D脚本里面的两个内容到我们的Enemy2D脚本里面去:
第一个内容:
//显示角色当前正受到攻击
float takenDamage = 0.2f;
第二个内容:
public IEnumerator TakenDamage(){
renderer.enabled = false;
yield return new WaitForSeconds(takenDamage);
renderer.enabled = true;
yield return new WaitForSeconds(takenDamage);
renderer.enabled = false;
yield return new WaitForSeconds(takenDamage);
renderer.enabled = true;
yield return new WaitForSeconds(takenDamage);
renderer.enabled = false;
yield return new WaitForSeconds(takenDamage);
renderer.enabled = true;
yield return new WaitForSeconds(takenDamage);
}
接下来我们需要对Bullet脚本进行处理,使其在碰撞到我们的敌人时,向敌人发送一个闪烁的信号:(修改内容如下)
void OnTriggerEnter(Collider other){
if (other.gameObject.tag == "Enemy") {
Destroy(gameObject);
other.gameObject.SendMessage("EnemyDamaged",damageValue,SendMessageOptions.DontRequireReceiver);
other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
}
if (other.gameObject.tag == "LevelObjects") {
Destroy(gameObject);
}
}
其实就是增加了一句:
other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
解决了敌人的闪烁问题之后,我们接下来要处理另外一个问题了。这个问题就是,我们的角色每消灭一些敌人就会增加经验,而经验的增加会自动补充HP,这个有点不对头和不合理,我们升级的时候增加的应该是最大HP,在每个等级下,我们应该设置一个当前的最大HP,不能无限制地增加HP才对,所以呢,我们需要将原来的GameManager脚本里面的playersHealth变量改名为curHealth,并且增加一个maxHealth变量,同时设置为3,将原本的playersHealth++修改为maxHealth++,这样的话,消灭敌人只会增加你的最大HP,而不会增加你当前的HP,至于升级之后是不是要让HP全满呢,这个功能很容易实现,我暂时不想加进去。
接下来我们就需要考虑下一个问题了,那就是角色的战斗力必须随着等级的提升而得到提升才比较合理。不能每次攻击都只扣敌人一滴血啊。而且敌人只有3到6滴血的设定不太合理,其实反正敌人的血是不显示出来的,完全可以设置一些大一点的数值,比如100,500之类的,这个就到后面有需要再改吧。现在暂时先不动。
十三、补血药的设置
在这一讲里面,首先要处理一个问题:我们的Player应该是有战斗力的,而不是每次只向敌人发送1滴的伤害值。利用某某++的方法可以很容易实现升级时增加战斗力,这个就不说了。关键是怎么弄成用我们的战斗力去减敌人的血值。首先,我们在GameManager脚本里面添加这么一行:
static public int bulletDamage = 1;
然后把我们的curHealth,也就是当前HP的值也改成static public。这里顺便补充一下,static在c#中的作用。
静态分配的,有两种情况:
1. 用在类里的属性、方法前面,这样的静态属性与方法不需要创建实例就能访问,
通过类名或对象名都能访问它,静态属性、方法只有“一份”:即如果一个类新建有N个
对象,这N 个对象只有同一个静态属性与方法;
2. 方法内部的静态变量:
方法内部的静态变量,执行完静态变量值不消失,再次执行此对象的方法时,值仍存在,
它不是在栈中分配的,是在静态区分析的, 这是与局部变量最大的区别;
如果这个说得不具体的话,那么可以看一下下面这个,红黑联盟里面讲的,非常形象,保证一看马上就明白了:(我也是在看视频教程的过程中碰到了static不懂,然后看下面这个理解透彻的)
提起static,一般理解为静态、全局。
何为static?我理解的static属于程序的直属单位,而非static就是非直属单位。
举一个非常常见的例子,中国有4个直辖市,北京、上海、天津、重庆,这些相当于static,而广州、南京、杭州等就是非static,中央可以直接管理北京、上海、天津、重庆,而广州、南京、杭州应由各省政府管理,Main方法可以直接调用static,而调用非static需要实例化。
class City()
{
//4个直辖市static 静态全局类型
public static void Beijing(){}
public static void ShangHai(){}
public static void Tianjin(){}
public static void Chongqing(){}
//其他城市 非静态
public void Guangzhou(){}
public void Nanjing(){}
}
void Main()
{
//调用static类型的方法
City.Beijing();//调用北京
City.Shanghai();//调用上海
//调用非static类型的方法
//没有直接调用权利,必须先实例化
City chengShi=new City();
chengShi.Guangzhou();//调用广州
}
好啦,然后我们继续。接着我们打开我们的Bullet脚本,把里面的内容修改成这样:
using UnityEngine;
using System.Collections;
public class Bullet : MonoBehaviour {
//用于碰撞时摧毁两个物体
void OnTriggerEnter(Collider other){
if (other.gameObject.tag == "Enemy") {
Destroy(gameObject);
other.gameObject.SendMessage("EnemyDamaged",GameManager.bulletDamage,SendMessageOptions.DontRequireReceiver);
other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
}
if (other.gameObject.tag == "LevelObjects") {
Destroy(gameObject);
}
}
void FixedUpdate(){
Destroy (gameObject, 1.25f);
}
}
其实上面的内容只是将
other.gameObject.SendMessage("EnemyDamaged",GameManager.bulletDamage,SendMessageOptions.DontRequireReceiver);
这一行进行了处理,我们原本的那个damageValue变量已经被删掉,然后替换成我们GameManager里面的bulletDamage,因为我们的这个bulletDamage已经修改为static,所以现在可以这样调用了。以后如果想要升级增加角色的杀伤力的话,就容易多了。
接下来我们想要在运行游戏中按C的时候顺便显示我们的战斗力,这个太简单了,只需要对GameManager里面做一点点修改:
if (playerStats) {
statsDisplay.text = "等级:" + level + "\n经验:" + curEXP + "/" + maxEXP + "\n攻击力:" + bulletDamage;
}
这一部分大家可以随便修改,你想显示什么就修改什么。至于在画面中的位置,可以调整GUIStats的transform,这个就不再赘述。
我稍微做了点修改。
接下来我们需要在场景中新增一个quad,去掉Mesh Collider,加上Box Collider,然后Box Collider的size全部改成1,transform里面的position的z值别忘了设成0,名字随便起,是用来做成补血药的,我命名为HealthPotion,然后为它增加一个同名的tag。为了区分,我顺便弄了一个同名的material扔上去,将颜色调成粉红色。(呵呵……感觉是毒药而不是血药啊……)
接着我们打开上次弄的那个StickToPlatform脚本,把里面的东西复制到我们的Controller2D上面,其实因为两个脚本都是绑定在Player身上,就没必要弄两个脚本了,直接合并成一个就成了。
然后我们再增加一段小代码来使得我们的Player碰到带有HealthPotion的物体时,将这个物体摧毁并且curHealth值加1。
void OnTriggerStay(Collider other){
if (other.tag == "Platform") {
this.transform.parent = other.transform;
}
}
void OnTriggerExit(Collider other){
if (other.tag == "Platform") {
this.transform.parent = null;
}
}
void OnTriggerEnter(Collider other){
if (other.tag == "HealthPotion") {
GameManager.curHealth++;
Destroy(other.gameObject);
}
}
很简单吧,现在补血药就已经做好了。想要做出补多少血的补血药都已经不是什么问题了。想要做出补蓝、增加战斗力什么的补血药,也不是什么问题了哈哈。
有个地方需要大家注意一下,那就是curHealth必须加上static,否则会出现An object reference is required to access non-static member这样的报错显示。在全局静态函数里面是不可以使用非全局静态变量的。
吃药前(注意左上角两个红心)
吃药后,哈哈,疗程短见效快,一粒补一滴血~
关于Box Collider的范围的问题,可以参考上一篇讲到的Player的Character Controller组件的问题,所以我们可以顺便把药物的Box Collider的size的X值改为1.3。
十四、游戏暂停和游戏存档
游戏存档看似简单,不过也是个比较蛋疼的问题。我们打开GameManager脚本:
首先,我们增加一个布尔值:
//用于暂停的布尔值
bool pauseMenu;
然后我们增添以下代码:
if (pauseMenu) {
if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.4f,Screen.width*.5f,Screen.height*.1f),"保存游戏")){
print ("已保存");
PlayerPrefs.SetInt("Player Level",level);
PlayerPrefs.SetInt("Player EXP",curEXP);
}
if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.6f,Screen.width*.5f,Screen.height*.1f),"显示保存的数据")){
print ("显示保存的数据");
print("当前等级:"+ PlayerPrefs.GetInt("Player Level"));
print("当前经验:"+ PlayerPrefs.GetInt("Player EXP"));
}
}
然后保存脚本。
现在我们在运行游戏的过程中按P键,就可以调出一个这样的画面:
当我们点击保存游戏时,就会保存相应的数据。我们可以先杀掉一只小怪,然后保存,然后重新运行,然后点击“显示保存的数据”,这个时候就会看到:
但是这个时候有两个问题,第一个问题是,虽然在这里我们可以看到保存的数据,但是实际上在游戏里面,如果我们按C键查看角色的属性时,发现经验值并没有保存下来。第二个问题是,我们的存档还不知道怎么删除……
接下来马上解决这个问题:
十五、载入游戏和删除存档
好了,我们继续修改我们的GameManager脚本,首先我们要增加一个void Awake()函数,这个东西和void Start()有什么不同呢?我顺便摘录了一段网上搬过来的笔记:
Unity3D初学者经常把Awake和Start混淆。
简单说明一下,Awake在MonoBehavior创建后就立刻调用,Start将在MonoBehavior创建后在该帧Update之前,在该Monobehavior.enabled == true的情况下执行。
1. void Awake (){
2. }
3. //初始化函数,在游戏开始时系统自动调用。一般用来创建变量之类的东西。
4.
5. void Start(){
6. }
7. //初始化函数,在所有Awake函数运行完之后(一般是这样,但不一定),在所有Update函数前系统自动条用。一般用来给变量赋值。
我们通常书写的脚本,并不会定义[ExecuteInEditMode]这个Attribute,所以Awake和Start都只有在Runtime中才会执行。
例1:
1. public class Test : MonoBehaviour {
2. void Awake () {
3. "Awake");
4. false;
5. }
6.
7. void Start () {
8. "Start");
9. }
10. }
以上代码,在Awake中我们调用了enabled = false; 禁止了这个MonoBehavior的update。由于Start, Update, PostUpdate等属于runtime行为的一部分,这段代码将使Start不会被调用到。
在游戏过程中,若有另外一组代码有如下调用:
1. Test test = go.GetComponent<Test>();
2. test.enabled = true;
这个时候,若该MonoBehavior之前并没有触发过Start函数,将会在这段代码执行后触发。
例2:
player.cs
1. private Transform handAnchor = null;
2. void Awake () { handAnchor = transform.Find("hand_anchor"); }
3. // void Start () { handAnchor = transform.Find("hand_anchor"); }
4. void GetWeapon ( GameObject go ) {
5. if ( handAnchor == null ) {
6. "handAnchor is null");
7. return;
8. }
9. go.transform.parent = handAnchor;
10. }
other.cs
1. ...
2. GameObject go = new GameObject("player");
3. player pl = go.AddComponent<player>(); // Awake invoke right after this!
4. pl.GetWeapon(weaponGO);
5. ...
以上代码中,我们在player Awake的时候去为handAnchor赋值。如果我们将这步操作放在Start里,那么在other.cs中,当执行GetWeapon的时候就会出现handAnchor是null reference.
总结:我们尽量将其他Object的reference设置等事情放在Awake处理。然后将这些reference的Object的赋值设置放在Start()中来完成。
当MonoBehavior有定义[ExecuteInEditMode]时
当我们为MonoBehavior定义了[ExecuteInEditMode]后,我们还需要关心Awake和Start在编辑器中的执行状况。
当该MonoBehavior在编辑器中被赋于给GameObject的时候,Awake, Start 将被执行。
当Play按钮被按下游戏开始以后,Awake, Start 将被执行。
当Play按钮停止后,Awake, Start将再次被执行。
当在编辑器中打开包含有该MonoBehavior的场景的时候,Awake, Start将被执行。
值得注意的是,不要用这种方式来设定一些临时变量的存储(private, protected)。因为一旦我们触发Unity3D的代码编译,这些变量所存储的内容将被清为默认值。
下面再来看看Unity圣典中的解释。
Awake()
当一个脚本实例被载入时Awake被调用。
Awake用于在游戏开始之前初始化变量或游戏状态。在脚本整个生命周期内它仅被调用一次.Awake在所有对象被初始化之后调用,所以你可以安全的与其他对象对话或用诸如 GameObject.FindWithTag 这样的函数搜索它们。每个游戏物体上的Awke以随机的顺序被调用。因此,你应该用Awake来设置脚本间的引用,并用Start来传递信息。Awake总是在Start之前被调用。它不能用来执行协同程序。
Start()
Start仅在Update函数第一次被调用前调用。Start在behaviour的生命周期中只被调用一次。它和Awake的不同是Start只在脚本实例被启用时调用。
你可以按需调整延迟初始化代码。Awake总是在Start之前执行。这允许你协调初始化顺序。
好了,这样就清楚为什么要用Awake()函数了吧。
接下来我们就这样弄,首先我们设置一个int saved,让它等于零。(如果不赋值的话,它也会默认等于零)
//用于判断是否是否保存
int saved = 0;
接下来我们就写下这么一个Awake函数:
void Awake(){
saved = PlayerPrefs.GetInt ("Game Saved");
if (saved == 1) {
curEXP = PlayerPrefs.GetInt ("Player EXP");
level = PlayerPrefs.GetInt ("Player Level");
maxEXP = level * 50;
maxHealth = level + 2;
curHealth = maxHealth;
}
}
在运行这个Awake函数的时候,就会让saved先获取我们保存的值。(等一下在下面存档的那部分脚本里面,我们会保存先把saved赋值为1,再进行保存),因为PlayerPrefs不能保存布尔值,所以我们用一个int的0和1来代替就行了,一样的。如果我们的游戏没有存档的话,saved读取不到任何数据,就会默认为零,那么就相当于不会接下去读取我们保存的数据了,反之,如果读取到了1,就相当于读取到了“已经有保存的数据”的情况,就需要继续执行。
在保存数据部分,我是这样弄的:
if (pauseMenu) {
//“保存游戏”按钮
if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.4f,Screen.width*.5f,Screen.height*.1f),"保存游戏")){
print ("已保存");
saved = 1;
PlayerPrefs.SetInt("Player Level",level);
PlayerPrefs.SetInt("Player EXP",curEXP);
PlayerPrefs.SetInt("Game Saved",saved);
}
//“显示保存的数据”按钮
if(GUI.Button(new Rect(Screen.width*.25f,Screen.height*.6f,Screen.width*.5f,Screen.height*.1f),"显示保存的数据")){
print ("显示保存的数据");
print("当前等级:"+ PlayerPrefs.GetInt("Player Level"));
print("当前经验:"+ PlayerPrefs.GetInt("Player EXP"));
print("是否保存:"+ PlayerPrefs.GetInt("Game Saved"));
}
}
我们可以看到,在保存之前,我们先将saved赋值为1,然后再保存,这样的话,当我们重新载入游戏时,就会进行一个是否保存了游戏的判断。
至于下面那个“显示保存的数据”的按钮功能,只是我用来debug.log的,没什么用,纯属调试,可以无视。
可能有人会说,保存游戏就直接保存,然后直接读取数据不就行了,为什么还要弄一个saved来判断呢?我一开始也是没有弄这个玩意的,后来发现了一个问题,在这里解释一下:假设我们现在删除了游戏存档,而没有这个用来判断是否有存档的saved值的话,那么我们的脚本自然就不管三七二十一,你没有存档它也会当成你是有存档的,这样会出现什么问题呢?这样的话,我们的curEXP = PlayerPrefs.GetInt ("Player EXP");和level = PlayerPrefs.GetInt ("Player Level");这两句话就会得不到任何数据,那么就会默认为零,那么,下面的maxEXP = level * 50;还有maxHealth = level + 2;这两句的计算就肯定会出问题了。maxEXP会变成0,而maxHealth会变成2,最大经验值变成零也就算了,我们的血值还从3变成了2,这不是坑爹么……哈哈,所以,这下子明白为什么要做一个是否saved的判断了吧。对于我这样的小游戏来说,就已经需要制作一个是否saved的判断了,对于需要保存大量数据的大游戏来说那就更是如此。希望大家如果是制作RPG类型的游戏的话,也可以养成类似的习惯。
接着要解决删档的问题。这个也是RPG游戏的一个重点内容。我们打开MainMenu脚本,然后在if (showGUIOutline)里面加入以下内容:
//“删除存档”按钮
if (GUI.Button (new Rect (Screen.width * guiPlacementX3, Screen.height * guiPlacementY3, Screen.width * .5f, Screen.height * .1f),
"删除存档")) {
PlayerPrefs.DeleteAll();
print ("已删除存档");
}
其实主要就是添加一个PlayerPrefs.DeleteAll();而已,没什么复杂的。
当然,为了方便我们在窗口中调整GUI的位置,我们也增加了guiPlacementX3还有guiPlacementY3这两个public的float值。这里就没必要贴出来了。
接着在Mainmenu场景里面的Maincamera上面调整好三个GUI按钮的位置:
接着我们来测试运行一下,我们进入Mainmenu场景,然后运行,点击“删除存档”按钮:
我们会看到print出了一句已删除存档的提示。
接着我们载入游戏,来到我们那个丑丑的游戏场景,然后按C,查看一下我们当前的各项状态。
现在我们还是初始状态,我们去随便刷掉两只怪,然后顺便去作死一下,扣掉一滴血。然后按P键调出保存菜单。
现在我已经按了保存,然后我按了“显示保存的数据”按钮,现在我们可以从右边的Console列表里面看到我们保存的数据。
好了,我们退出,重新进来~,再查看一下:
细心一点的朋友应该注意到,这次稍微有了点不同。那就是我又恢复成3滴血了。这是因为我们重新加载的时候,curHealth会变成当前等级的maxHealth,所以我们重新载入之后不是2滴血,而是3滴血。
现在我们重新进入Mainmenu场景,然后删除存档,再重新进来一遍:
可以看到,我们的角色的全部资料都清零了。(右手边的Console显示了我刚刚有进行“删除存档”按钮的点击操作,没有造假,哈哈)
好了,现在我们已经解决了角色存档的问题。这部分可能自己实际操作的时候会碰到一些问题,需要大家多做几次,特别是不同的游戏,情况肯定不一样,这个没办法有统一的标准,这里只是提供一个思路。
十六、自动存档
前面我们提到了存档的方法了。有些游戏是即时存档的,就是在Update每一帧都进行一次存档的操作,对于小游戏来说,这种做法无可厚非,但是对于类似宠物小精灵这种有庞大数据的游戏来讲,即时存档是不太可行的。也许有其他优化的方法,比如在独立游戏Terraria中,它就是支持大数据即时存档的,目前我尚不知道这种方法要如何实现。
下面,我们可以利用类似技能冷却的原理,设置一个定时自动存档器,比如说每隔五秒钟或者十秒钟自行存档一次,为了让玩家知道有自动存档的情况,我们可以调用一些GUI来显示(我这里就直接print了)。同理,我们也可以在场景中布置一些特殊物体,进行自动的存档,比如说某一个关卡末尾的大门,我们的Player碰到大门之后就会跳到下一个关卡,同时自动保存我们Player身上的全部数据。(至于这个场景跳转门或者说是位置跳转门能不能双向跳动,这个就要看你的游戏是怎么布置的了。)
废话不多说,马上开始:
首先我们需要在GameManager脚本里面创建一个叫做SaveGame()的函数,然后改为public类型(为什么要这样做后面会解释),然后把我们前面
在if(pauseMenu)里面的部分内容移到这个函数里面去:
public void SaveGame(){
saved = 1;
PlayerPrefs.SetInt("Player Level",level);
PlayerPrefs.SetInt("Player EXP",curEXP);
PlayerPrefs.SetInt("Game Saved",saved);
print ("已保存");
}
只有加上public,才能在外部进行调用嘛。好了,现在我们增加下面的内容:
if (other.tag == "Door") {
string thisLevel = Application.loadedLevelName;
int intThisLevel = int.Parse(thisLevel);
int intNextLevel = intThisLevel+1;
string nextLevel = intNextLevel.ToString();
Application.LoadLevel(nextLevel);
}
想要跳到下一个关卡有个很简单的函数可以使用,就是Application.LoadLevel(),可是有个问题,就是如果我们直接选择加载某一个关卡的话,我们这个脚本就不能重复利用了。也就是说,我们想要做成一个只要一碰到就会自动跳转到下一关的功能的脚本,这样就不必在每一个关卡里面都来写一个特定的脚本。首先,我们用string thisLevel = Application.loadedLevelName;这句话获取当前关卡的名字,我们之前将关卡命名为Scene1,现在直接改成1就行了。这样的话,我们就相当于得到了关卡的序列号。理论上来讲,我们只需要Application.LoadLevel(thisLevel+1);就应该是可以跳转到下一个关卡的了。如果这样的话那就很方便了。可是实际上有个很大的问题,那就是关卡的名字是字符串,字符串可不能直接做加减法的,所以我们需要将字符串强制转换为int类型。C#里面有Convert.ToInt32的转换方法,但是我在unity里面没办法使用(难道是我的打开方式不对?)所以我采用了另一种方法,也就是上面脚本的int intThisLevel = int.Parse(thisLevel);这样,我们获取当前的字符串名称1就会变成整数1(为了实现这个功能,我们必须将除了Mainmenu关卡之外的其他关卡全部命名为阿拉伯数字名称,即1、2、3……)然后我们对这个整数1进行加1的处理,即:int intNextLevel = intThisLevel+1;接着,我们将得到的这个新的整数重新转换为字符串string nextLevel = intNextLevel.ToString();到这里,加载下一个关卡的任务就完成了,最后我们加上这么一句:Application.LoadLevel(nextLevel);即可跳转到下一关卡。
C#,int转成string,string转成int
1,int转成string
用toString
或者Convert.toString()如下
例如:
int varInt = 1;
string varString = Convert.ToString(varInt);
string varString2 = varInt.ToString();
2,string转成int
如果确定字符串中是可以转成数字的字符,可以用int.Parse(string s),该语句返回的是转换得到的int值;
如果不能确定字符串是否可以转成数字,可以用int.TryParse(string s, out int result),该语句返回的是bool值,指示转换操作是否成功,参数result是存放转换结果的变量。
例如:
string str = string.Empty;
str = "123";
int result=int.Parse(str);
string str = string.Empty;
str = "xyz";
int result;
int.TryParse(str, out result);
接下来处理下一个问题,我们在刚刚的if (other.tag == "Door") {}里面插入一句gameManager.SaveGame();当然,在此之前,我们需要在这个脚本里面加上这么一行:
//引用GameManager
public GameManager gameManager;
这个应该很好理解,就不再赘述了。使用public的原因是为了在外部进行拖拽操作,如果不将其设置为public的话,就会出现NullReferenceException: Object reference not set to an instance of an object字样的报错,这个我们在上一篇学习笔记里面已经有提到过了。出现这个的原因是因为我们此处引用了GameManager里面的一个SaveGame()函数,但是unity并不知道你到底在用哪个GameManager,所以会报Null,最简单的解决方法就是拖拽大法,也可以用GetComponent<>的方法。
看下面,这个是我的Player身上的Controller2D脚本的设置,我已经把GameManager物体拖放到相应位置了。
刚开始的时候,原作者并没有使用这个方法,而是直接复制了Bullet脚本里面的other.gameObject.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);这句进行修改。现在我们顺便来想想为什么不可以采用这种方法。因为这种方法是对着other.gameObject发送信号,而我们是对着自己发送信号啊。
可能有人会说,那还不简单,我们直接把other.gameObject.删掉不就可以了吗?原视频的作者也试了一次,不行。但是他没解释。我想了一段时间,终于明白了。因为SendMessage不可以把内容发送给自己这个脚本。发送给同个物体身上的不同脚本还是行得通的,但是自己发送给自己就不行了。所以,这种方法是行不通的,就只能用上面提到的那种方法了。
现在我们已经实现了场景跳转和按键存档功能,接下来就是自动存档功能了。我们在GameManager脚本的Update函数里面进行如下增添:
//自动存档功能
autoSaveTimer += Time.deltaTime;
if (autoSaveTimer >= 1000f) {
SaveGame();
print ("保存啦~~");
autoSaveTimer =0;
}
上面的autoSaveTimer是自定义的一个int变量,我顺便设置成public,方便在外面修改。我将它改为10,也就是每隔10秒就会自动保存一次,前面我们将保存的功能整合成一个SaveGame()函数就是为了这个目的。那句print(“保存啦~~”)只是用来卖萌和提示自己的,嘿嘿~
每次保存之后autoSaveTimer这个自动保存的计时器就会清零,然后重新进入下一轮的计时,这样就可以实现循环保存的功效了。因为我们的游戏数据比较小,所以可以每隔一段时间就自动保存一次,大数据的游戏就不建议这样弄了。
这一讲很长,也涉及到了很多问题。所以我花了很长时间整理。如果大家有什么不太清楚的话,最好多看几遍,然后自己再试着操作一下试试。当然,这个是针对新手说的,各路大神可以无视哈。
十七、关卡选择
在这一讲里面,我们首先创建一个新的场景(为了方便测试,我把上一次弄的场景2直接重命名为场景LevelSelect,你要新建一个当然也没问题,这个不是重点,哈哈),这个场景的作用就是在进入时可以用来选择进入各个关卡的。很多横版的闯关游戏由于关卡比较多,所以在游戏开始的时候都会有这样的选择画面,让玩家自行选择这次要玩哪一关,就不必要每次都从第一关开始玩起了。而且由于Unity在游戏保存这一块非常给力,所以我们完全不需要担心数据丢失方面的问题。
接着我们先点击进入我们的Mainmenu场景,在摄像机的MainMenu脚本里面,把Level里面原本的Scene1改成现在的LevelSelect,这样我们就可以通过点击按钮直接跳转到我们现在要用到的这个LevelSelect场景了。
接着进入这个场景,然后创建一个同名脚本扔到摄像机上面,然后我们开始编辑:
public class LevelSelect : MonoBehaviour {
int sw = Screen.width;
int sh = Screen.height;
public string Level;
void OnGUI(){
//进入第一关的按钮
if (GUI.Button (new Rect (0, 0, sw * .5f, sh), "Level: 1")) {
Application.LoadLevel(Level);
}
}
}
内容很简单,我就不做解释了。前面两个int是因为觉得打screen.width和screen.height很麻烦,所以才那样弄的,至于那个public string Level,之前介绍过了就不再解释为什么要这样做了。我这里只是随便弄了一个出来,大家可以在这个LevelSelect的脚本里面设置一堆不同的按钮,每个按钮连接到你的各个不同关卡里面去,这样就达到了关卡选择的效果啦。
接下来看看我们之前的Controller2D脚本,我们在这个脚本里面有这么一段:
void OnTriggerEnter(Collider other){
if (other.tag == "HealthPotion") {
GameManager.curHealth++;
Destroy(other.gameObject);
}
if (other.tag == "Door") {
gameManager.SaveGame();
string thisLevel = Application.loadedLevelName;
int intThisLevel = int.Parse(thisLevel);
int intNextLevel = intThisLevel+1;
string nextLevel = intNextLevel.ToString();
Application.LoadLevel(nextLevel);
}
}
看了原作者的视频,他是打算做一个叫做Door的脚本,把上面的那部分功能转移到这个脚本里面去,然后再去把这个东西拉到我们的Door物体上,其实这一步可做可不做。我想来想去,这么做的好处大概就是可以为每一个Door弄一个public string Level,然后设置那些穿越什么的方便一些吧。
视频里面的Door脚本里面的GameManager脚本还是采用将它改为public,然后再在外面进行拖拽的方法。那么有没有什么方法可以不进行这些拖拽呢?当然可以的啊。只要利用GetComponent就可以了。下面贴出我写的脚本:
public class Door : MonoBehaviour {
public string Level;
GameManager gameManager;
GameObject gameObject;
void Start() {
gameObject = GameObject.FindGameObjectWithTag("GameManager");
gameManager = gameObject.GetComponent<GameManager>();
}
void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
gameManager.SaveGame();
Application.LoadLevel(Level);
}
}
}
我们首先要定义一个GameObject来利用FindGameObjectWithTag得到它(使用这种方法需要增加tag,其实和拖拽方法大同小异,这个就看个人喜好了。两种方法大家都可以使用的)当然,我们要给GameManager加上一个同名的tag。
接着我们就利用这个:gameManager = gameObject.GetComponent<GameManager>();很简单,我就不解释了,不过需要注意一下格式。
如果在函数上有什么不太明白的,建议大家自己去查查手册。另外呢,在实际操作过程中,如果大家按照我上面的脚本照抄一遍,就会出现这样的黄字警告:Assets/Scripts/Door.cs(8,16): warning CS0108: `Door.gameObject' hides inherited member `UnityEngine.Component.gameObject'. Use the new keyword if hiding was intended
上面的这个问题,我们只需要把gameObject改成gameObjectGM,就不会报错了。
好了,那么这一讲想要实现的功能就都实现了。我们可以接着下一讲的内容了:
(顺便说一下,我不是很喜欢unity4.3版本里面自带的monodevelop脚本编辑器,所以我改成了VS2013,这篇写完之后我会写一篇替换编辑器的内容。)
这是我处理过的VS2013的界面,感觉还是挺和谐的嘻嘻。下一篇我们再聊这个。
由于排版方面出了很奇怪的问题,所以第二篇就只能先到这里结束了。大概是因为我弄了太多天的同一篇,又有些东西是复制进来的,导致排版的时候总是出错吧。先这样吧,下一篇我会将没有说完的补完。