好友,大家好,欢迎关注我的博客。我是秦培,我的博客地址blog.csdn.net/qinyuanpei。
今天,我想分享的是,现在在移动平台上的检查点系统更受欢迎,机游戏如《愤慨的小鸟》、《保卫萝卜》中对游戏内容的组织形式,玩家可通过已解锁的关卡(默认第一关是已解锁的)获取分数进而解锁新的关卡,或者是通过付费购买解锁新的关卡。那么好了,在今天的文章中博主将带领大家高速实现一个可扩展的关卡系统,这个实例的灵感来自博主近期的工作经历,希望对大家学习Unity3D游戏起到一定帮助性的作用。
原理
在本地配置一个Xml文件。在这个文件里定义当前游戏中关卡的相关信息。通过解析该文件并和UI绑定终于实现一个完整的关卡系统。
1、定义关卡
首先我们来定义一个关卡的基本结构:
public class Level
{
/// <summary>
/// 关卡ID
/// </summary>
public string ID;
/// <summary>
/// 关卡名称
/// </summary>
public string Name;
/// <summary>
/// 关卡是否解锁
/// </summary>
public bool UnLock = false;
}
在这里,我们假定关卡的名称和该关卡在Unity3D中场景名称一致。当中最为重要的一个属性是UnLock。该值是一个布尔型变量,表明该关卡是否解锁。由于在游戏中。仅仅有解锁的场景是能够訪问的。
2、定义关卡配置文件
从关卡的基本结构Level能够定义出例如以下的配置文件,这里使用Xml作为配置文件的存储形式:
<?xml version="1.0" encoding="utf-8"?>
<levels>
<level id="0" name="level0" unlock="1" />
<level id="1" name="level1" unlock="0" />
<level id="2" name="level2" unlock="0" />
<level id="3" name="level3" unlock="0" />
<level id="4" name="level4" unlock="0" />
<level id="5" name="level5" unlock="0" />
<level id="6" name="level6" unlock="0" />
<level id="7" name="level7" unlock="0" />
<level id="8" name="level8" unlock="0" />
<level id="9" name="level9" unlock="0" />
</levels>
和关卡结构定义相似,这里使用0和1来表示关卡的解锁情况。0表示未解锁。1表示解锁,能够注意到默认情况下第一个关卡是解锁的。这符合我们在玩《愤慨的小鸟》这类游戏时的直观感受。那么好了,在完毕了关卡的结构定义和配置文件定义后,接下来我们開始思考怎样来实现一个关卡系统,由于此处并不涉及到Unity3D场景中的详细逻辑,因此我们在关卡系统中基本的工作就是维护好主界面场景和各个游戏场景的跳转关系,我们能够注意到这里要完毕两件事情,即第一要将配置文件里的关卡以一定形式载入到主界面中,并告诉玩家哪些关卡是已解锁的、哪些关卡是未解锁的。当玩家点击不同的关卡时能够得到不同的响应,已解锁的关卡能够訪问并进入游戏环节,未解锁的关卡则须要获得很多其它的分数或者是通过付费来解锁关卡;第二是要对关卡进行编辑,当玩家获得了分数或者是支付一定的费用后能够解锁关卡进入游戏环节。
这两点综合起来就是我们须要对关卡的配置文件进行读写,由于我们注意到一个关卡是否解锁仅仅取决于unlock属性。那么好了,明确了这一点后我们来动手编写一个维护关卡的类。
3、编写一个维护关卡的类
这里直接给出代码。由于从严格的意义上来说。这段代码并不是我们此刻关注的重点。可能这让大家感到难以适应,由于文章明明就是在教我们实现一个关卡系统。但是此刻博主却说这部分不重要了,请大家稍安勿躁。由于这里有比代码更为深刻的东西。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
public static class LevelSystem
{
/// <summary>
/// 载入Xml文件
/// </summary>
/// <returns>The levels.</returns>
public static List<Level> LoadLevels()
{
//创建Xml对象
XmlDocument xmlDoc = new XmlDocument();
//假设本地存在配置文件则读取配置文件
//否则在本地创建配置文件的副本
//为了跨平台及可读可写,须要使用Application.persistentDataPath
string filePath = Application.persistentDataPath + "/levels.xml";
if (!IOUntility.isFileExists (filePath)) {
xmlDoc.LoadXml (((TextAsset)Resources.Load ("levels")).text);
IOUntility.CreateFile (filePath, xmlDoc.InnerXml);
} else {
xmlDoc.Load(filePath);
}
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
//初始化关卡列表
List<Level> levels = new List<Level>();
foreach (XmlElement xe in levelsNode)
{
Level l=new Level();
l.ID=xe.GetAttribute("id");
l.Name=xe.GetAttribute("name");
//使用unlock属性来标识当前关卡是否解锁
if(xe.GetAttribute("unlock")=="1"){
l.UnLock=true;
}else{
l.UnLock=false;
}
levels.Add(l);
}
return levels;
}
/// <summary>
/// 设置某一关卡的状态
/// </summary>
/// <param name="name">关卡名称</param>
/// <param name="locked">是否解锁</param>
public static void SetLevels(string name,bool unlock)
{
//创建Xml对象
XmlDocument xmlDoc = new XmlDocument();
string filePath=Application.persistentDataPath + "/levels.xml";
xmlDoc.Load(filePath);
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
foreach (XmlElement xe in levelsNode)
{
//依据名称找到相应的关卡
if(xe.GetAttribute("name")==name)
{
//依据unlock又一次为关卡赋值
if(unlock){
xe.SetAttribute("unlock","1");
}else{
xe.SetAttribute("unlock","0");
}
}
}
//保存文件
xmlDoc.Save (filePath);
}
}
这里我们首先将关卡配置文件levels.xml放置在Resources文件夹下,这是由于我们能够使用Resources.Load()这样的方式来载入本地资源。这样的方式对于Unity3D来说有着得天独厚的优势:
* 它使用相对于Resources文件夹的相对路径,所以在使用的时候不用考虑是相对路径还是绝对路径的问题
* 它使用名称来查找一个本地资源。所以在使用的时候不用考虑扩展名和文件格式的问题
* 它能够是Unity3D支持的随意类型。从贴图到预制体再到文本文件等等。能够和Unity3D的API完美地结合
说了这么多它的长处,我们自然要痛心疾首地说说它的缺点。它的缺点是什么呢?那就是不支持写入操作。这当然不能责备Unity3D,由于当Unity3D导出游戏的时候会将Rsources文件夹下的内容压缩后再导出,我们当然不能要求在一个压缩后的文件里支持写入操作啦,所以我们是时候来总结下Unity3D中资源读写的常见方案了,那么Unity3D中常见的资源读写方案由哪些呢?
1、Resources.Load:仅仅读,当我们的资源不须要更新且对本地存储无容量要求的时候能够採用这样的方式
2、AssetBundle:仅仅读。当我们的资源须要更新且对本地存储有容量要求的时候能够採用这样的方式
3、WWW:仅仅读。WWW支持http协议和file协议,因此能够WWW来载入一个网络资源或者本地资源
4、PlayerPrefs:可读可写,Unity3D提供的一种的简单的键-值型存储结构,能够用来读写float、int和string三种简单的数据类型,是一种较为松散的数据存储方案
5、序列化和反序列化:可读可写,能够使用Protobuf、序列化为Xml、二进制或者JSON等形式实现资源读写。
6、数据库:可读可写,能够使用MySQL或者SQLite等数据库对数据进行存储实现资源读写。
好了,在了解了Unity3D中资源读写的常见方案后,我们接下来来讨论下Unity3D中的路径问题:
1、Application.dataPath:这个路径是我们常常使用的一个路径,但是我们真的了解这个路径吗?我看这里要打个大大的问号。为什么这么说呢?由于这个路径在不同的平台下是不一样的。从官方API文档中能够了解到这个值依赖于执行的平台:
* Unity 编辑器:<project文件夹的路径>/Assets
* Mac:<到播放器应用的路径>/Contents
* IOS: <到播放器应用的路径>/
4、编写入口文件
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using System.Xml.Serialization;
public class Main : MonoBehaviour
{
//关卡列表
private List<Level> m_levels;
void Start ()
{
//获取关卡
m_levels = LevelSystem.LoadLevels ();
//动态生成关卡
foreach (Level l in m_levels)
{
GameObject prefab=(GameObject)Instantiate((Resources.Load("Level") as GameObject));
//数据绑定
DataBind(prefab,l);
//设置父物体
prefab.transform.SetParent(GameObject.Find("UIRoot/Background/LevelPanel").transform);
prefab.transform.localPosition=new Vector3(0,0,0);
prefab.transform.localScale=new Vector3(1,1,1);
//将关卡信息传给关卡
prefab.GetComponent<LevelEvent>().level=l;
prefab.name="Level";
}
//人为解锁第二个关卡
//在实际游戏中玩家须要满足一定条件方可解锁关卡
//此处仅作为演示
LevelSystem.SetLevels ("level1", true);
}
/// <summary>
/// 数据绑定
/// </summary>
void DataBind(GameObject go,Level level)
{
//为关卡绑定关卡名称
go.transform.Find("LevelName").GetComponent<Text>().text=level.Name;
//为关卡绑定关卡图片
Texture2D tex2D;
if(level.UnLock){
tex2D=Resources.Load("nolocked") as Texture2D;
}else{
tex2D=Resources.Load("locked") as Texture2D;
}
Sprite sprite=Sprite.Create(tex2D,new Rect(0,0,tex2D.width,tex2D.height),new Vector2(0.5F,0.5F));
go.transform.GetComponent<Image>().sprite=sprite;
}
}
在这段脚本中,我们首先载入了关卡信息,然后将关卡信息和界面元素实现绑定。从而实现一个简单的关卡选择界面,并人为地解锁了第二个关卡。好吧。假设这是一个正式游戏的配置关卡配置文件。相信大家都知道怎么免费玩解锁的关卡了吧。哈哈!当然。我不推荐大家这样做。由于作为一个程序猿,当你全身心地投入到一个项目中的时候,你就会明确完毕一款软件或者游戏须要投入多少精力。所以大家尽量还是不要想破解或者盗版这些这些事情,毕竟作为开发人员可能他的出发点是想做出来一个让大家都喜欢的产品,但是更现实的问题是开发人员一样要生活,所以请善待他们吧。好了。言归正传,这里的UI都是基于UGUI实现的,不要问我为什么不用NGUI,由于我就是喜欢UGUI!
我们知道我们须要为每一个关卡的UI元素绑定一个响应的事件。因此我们须要为其编写一个LevelEvent的脚本:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class LevelEvent : MonoBehaviour
{
//当前关卡
public Level level;
public void OnClick()
{
if(level.UnLock){
//假设关卡的名称即为相应场景的名称
//Application.LoadLevel(level.Name);
Debug.Log ("当前选择的关卡是:"+level.Name);
}else{
Debug.Log ("抱歉!当前关卡尚未解锁!");
}
}
}
记得在本文開始的时候,博主提到了一个假设,就是关卡的名称和其相应的游戏名称一致的假设,相信到此处大家都知道为什么了吧!为了让每一个关卡的UI元素知道自己相应于哪个关卡。我们设置了一个level变量。这个变量的值在载入关卡的时候已经完毕了初始化,所以此时我们能够在这里知道每一个关卡的详细信息,从而完毕事件的响应。
好了。今天的内容就是这样了。我们来看看终于的效果吧。
能够注意到在第二次打开游戏后,第二个关卡已经解锁了,说明我们在最開始设计的两个目标都达到了,那么内容就是这样子啦,假设大家有什么好的想法或者建议。欢迎在文章后面给我留言,谢谢大家!
2015年12月1日更新:
近期有朋友反映须要我给出IOHelper这个类的实现。但是其实这个类仅仅是对System.IO这个命名空间下的相关方法进行了简单的封装,我一直认为无论学习什么技术。我们都要有一个完整的系统性的学习路线。
尽管Unity3D入门十分简单,可说究竟它本质上仍然是C#的技术范畴,所以我们不能把眼光局限在Unity3D这个引擎和它丰富的插件上面,学插件、使用插件只是是邯郸学步。真正能让你成长的却是自己掌握的知识。我知道我说这些,大部分人都不会看,由于你认为来看我博客是给我面子,直接抄你博客里的代码是看得起你。
呵呵,我写这个博客的目的可不是为了给懒人提供代码库,懒人真正的代码库是Github。罢了。给这些懒人给出源码吧!
/*
* Unity3D脚本(C#)
* Author:
* Date:
*/
using UnityEngine;
using System.Collections;
using System.IO;
public static class IOUntility
{
/// <summary>
/// 创建文件夹
/// </summary>
/// <param name="path">文件夹路径</param>
public static void CreateFolder(string path)
{
if(!Directory.Exists(path)){
Directory.CreateDirectory(path);
}
}
/// <summary>
/// 创建文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="content">文件内容</param>
public static void CreateFile(string filePath,string content)
{
//文件流
StreamWriter writer;
//推断文件文件夹是否存在
//不存在则先创建文件夹
Debug.Log (filePath);
string folder = filePath.Substring (0, filePath.LastIndexOf ("//"));
CreateFolder (folder);
//假设文件不存在则创建。存在则追加内容
FileInfo file=new FileInfo(filePath);
if(!file.Exists){
writer=file.CreateText();
}else{
file.Delete();
writer=file.CreateText();
}
//写入内容
writer.Write(content);
writer.Close();
writer.Dispose();
}
/// <summary>
/// 推断文件是否存在
/// </summary>
/// <param name="path">文件路径</param>
public static bool isFileExists(string path)
{
FileInfo file=new FileInfo(path);
return file.Exists;
}
public static void DeleteFile(string fileName)
{
if(!File.Exists(fileName)) return;
File.Delete(fileName);
}
}