博主前些日子和别的学院的同学共同制作了一款小游戏Jumper,现在把其开源出来,希望可以给在Unity初学道路上的同学一些帮助 :)
我们首先看一下游戏的最终截图,效果完成度不高,但是其中代码的基本逻辑是比较齐全的。
我们看到的这只小鸡就是我们的主角了!背景是一个大楼,右上角有一个温度计,会随着时间上升。我们要跳上各种挡板,尽可能地在那些窗户上安装空调,(否则同学们会暴动的!)。提示,右下角有一个药丸,吃了可以大幅度提升跳跃能力,左下角是一个敌人的模型,别被黏黏的蜗牛触碰到哦!
私以为运用Unity,最重要就是场景的设计和游戏主角的逻辑。只要这两点明确下来,那么附属的物品等基本上就是轻车熟路了。包括阅读别人的代码,如果想从一个脚本中分辨出其十多个变量的意义,那么无异于舍本逐末。只要理解了其最最基本的主角逻辑和场景逻辑,那么再在其基础上加上”药物“、”物品“、”属性“、”分数“这些东西,就会事半功倍。
即时是一个最简单的游戏,
让我们看一下主角的行为,Character对象最后那个最最核心的Update()
为了让代码更清晰易懂,我只保留了其中的一部分。
void Update()
{
//Vector3 dir = Vector3.zero;
//dir.x = -Input.acceleration.x * 2;
//dir *= Time.deltaTime;
if (Input.GetKey(KeyCode.LeftArrow))
{
if (!left) {/*主角转向*/
transform.rotation = new Quaternion(0, 180, 0, 1);
left = true;
}
/*向左移动*/
transform.Translate(Vector3.right * -Time.deltaTime * moveSpeed, Space.World);
}
if (Input.GetKey(KeyCode.RightArrow))
{
/*和向左移动的逻辑类似*/
if (left) {
transform.rotation = new Quaternion(0, 0, 0, 1);
left = false;
}
transform.Translate(Vector3.right * Time.deltaTime * moveSpeed, Space.World);
}
/*跳跃*/
if (Input.GetKey(KeyCode.LeftControl))
{
Jump();
}
/*安装空调*/
if (Input.GetKey(KeyCode.LeftShift))
{
OnWorking();
}
有些人喜欢将类似于Jump和OnWorking()这样的函数直接在Update()中完成,我觉得这样做非常不妥当。类中函数调用的开销非常低,而且更有利于以后的重组、修改。而且整体逻辑看起来一目了然。
另一个比较重要的和主角有关的逻辑是死亡判定,我将这一部分写入到了另一个对象Score当中(它保有一个Character的引用)。因为主角的死亡原因可能不光是因为”跳到钉子上面“,还有可能是因为”超过了时间“等这样的全局因素影响。
void Update()
{
if (player.transform.position.y > lastY)
{
score = (int)(player.transform.position.y * 200);
lastY = player.transform.position.y;
}
if (!isDead)
{
gameObject.GetComponent<GUIText>().text = "Score: " + score;
}
if (isDead && !isSet)
{
audioLose.Play();
int highscore = PlayerPrefs.GetInt("highscore");
if (highscore < score)
{
PlayerPrefs.SetInt("highscore", score);
}
GetComponent<GUIText>().anchor = TextAnchor.MiddleCenter;
transform.position = new Vector3(0.5f, 0.5f, 0);
gameObject.GetComponent<GUIText>().text = "You lost\nYour Score: " + score + "\nYour Highscore: " + highscore;
isSet = true;
player.GetComponent<Collider>().enabled = false;
}
}
另一大要素场景的逻辑稍显复杂一些。我们除了背景固定外,各种障碍、跳板的位置是随机的。而且种类很多,有普通板(x),破碎的板子(b),带钉子的板子(s),需要安装空调的窗户(m)。
我写了一个类来专门负责它们的安装工作,它将从一个txt(类似于地图)中读取几个预设,然后返回一个ArrayList来存储类似于‘x’‘x’‘x’‘b’的字符。
private ArrayList readPattern (string path)
{
TextAsset txt = (TextAsset)Resources.Load (path, typeof(TextAsset));
string content = txt.text;
ArrayList patternList = new ArrayList ();
string[] lines = content.Split ("\n" [0]);
char[,] singlePattern = new char[,] {
{'x','x','x','x','x'},
{'x','x','x','x','x'},
{'x','x','x','x','x'}
};
int lineCounter = 0;
foreach (string line in lines) {
if (lineCounter < 3) {
for (int i = 0; i < 5; i++) {
singlePattern [lineCounter, i] = line.ToCharArray () [i];
}
lineCounter++;
} else if (lineCounter == 3) {
patternList.Add (singlePattern);
singlePattern = new char[,] {
{'x','x','x','x','x'},
{'x','x','x','x','x'},
{'x','x','x','x','x'}
};
lineCounter = 0;
}
}
return patternList;
}
我们已经得到了随机的地图,下一步是有方法来调用他。
并且在合适的位置和合适的时机(在离主角足够近时)安装上这些挡板。
void Spawn ()
{
if (player.GetComponent<Rigidbody>().transform.position.y > 42) {
return;
}
float staticLastSpawnY = lastSpawnY;
while (j <= staticLastSpawnY) {
char[,] tempPattern = (char[,])easySpawnPattern [Random.Range (0, easySpawnPattern.Count)];
if (player.GetComponent<Rigidbody>().transform.position.y < 50) {
tempPattern = (char[,])easySpawnPattern [Random.Range (0, easySpawnPattern.Count)];
}
if (player.GetComponent<Rigidbody>().transform.position.y > 50 && player.GetComponent<Rigidbody>().transform.position.y < 100) {
tempPattern = (char[,])mediumSpawnPattern [Random.Range (0, mediumSpawnPattern.Count)];
}
if (player.GetComponent<Rigidbody>().transform.position.y > 100) {
tempPattern = (char[,])hardSpawnPattern [Random.Range (0, hardSpawnPattern.Count)];
}
for (int k = 2; k >= 0; k--) {
j += 5f;
for (int i = 0; i < 5; i++) {
if (tempPattern [k, i].ToString () == "-") {
Instantiate(Resources.Load("Normals"), new Vector3(getWorldCoordinates(i), Random.Range(0, 0.25f) + j, 0), Quaternion.identity);
}
if (tempPattern [k, i].ToString () == "s") {
Instantiate (Resources.Load ("Spikes"), new Vector3 (getWorldCoordinates (i), Random.Range (0, 0.25f) + j, 0), Quaternion.identity);
}
if (tempPattern [k,i].ToString () == "m") {
//这里需要注意的是,由于这个坐标物体没有正确的原点。所以在z轴上进行了偏移
Instantiate (Resources.Load ("Missions"), new Vector3 (getWorldCoordinates (i), Random.Range (0, 0.25f) + j,-0.3f), Quaternion.identity);
}
if (tempPattern [k,i].ToString () == "b") {
Instantiate (Resources.Load ("Brokens"), new Vector3 (getWorldCoordinates (i), Random.Range (0, 0.25f) + j, 0), Quaternion.identity);
}
}
}
Clouds (j, getWorldCoordinates (Random.Range (0, 5)) + Random.Range (-1, 1));
Award (j, getWorldCoordinates (Random.Range (0, 5)) + Random.Range (-1, 1));
}
lastSpawnY += 10;
}
这个源代码中还有几个部分,例如【板子的种类】【药物】【主角属性的变化】【计时操作】,这几个部分我们先将他们和C#文件一一对应起来。再大段粘贴代码的话读者已经很难看下去了~
【板子的种类】
共有MissionTile、SpikeTile、BrokenTile它们共同继承于Tile(普通板)。MissionTile向Score”汇报“任务完成。(如果把这一工作交给了主角Character来做,明显是打破了单一职责原则)
【药物】
包括了Capsule,这个游戏目前只有一种药物。但是药物的效果并没有在里面体现。只说明了这是”哪一种“药物和 OnTriggerEnter()用来触发人物吃药的效果。我之所以这样设计是因为直接让药物这样的类去操纵人物的属性不符合里氏替换原则。(简单地说是请面对接口!)
【主角属性的变化】【计时操作】
主角属性的变化一般都是有时间限制的,我看到过一些写法,是在主角Character中完成这一计时。但是完全有更好的办法就是协程:
(我不得不再粘点代码上来。。)
IEnumerator AffectTimer()
{
yield return new WaitForSeconds(Constants.CAPSULE_TIME);
//todo 修改人物的属性到基本状体
Debug.Log("药效完毕");
//恢复正常状态
GoBackNormal();
yield return null;
}
这就先告一段落了,我上一次使用Unity写的一个空调模拟系统这里也一并贴出来占一下空【请忽略下图】