游戏开发之路小结(一):关于太空射击游戏开发实战小结
学习Unity3d游戏开发已经有一年的时间了,一路上磕磕碰碰,也看过很多游戏开发的书籍,这些书籍确实在我学习游戏开发的路上给了我很大的帮助,让我这个初学者对游戏的开发有了一个初步的认知,慢慢我发现游戏开发是一件比较不容易的事情,可以说游戏开发涉及的面非常的广泛,有时候我真的很佩服那些独立的游戏开发者,他们仅仅靠自己的一己之力便完成了一个游戏的开发,这是一件了不起的事情,虽说有些游戏可能涉及的东西不是那么的多,不过要开发一个原创的游戏也是需要动很大脑筋的。于是我便开始学习了一些小游戏的开发,这些游戏虽说都是比较基础的相对简单的游戏,但是里面涉及的一些方法和思想是非常值得我学习的,对别人游戏的研究可以帮助我们了解一些游戏开发的流程,学习到这些对我们以后的游戏开发是非常重要的,下面便是我关于学习太空射击游戏开发的小结,这里说是小结,也是对此次学习的一个记录吧,希望以后再次翻阅此篇随笔的时候会有一些启发或是便于以后再见的时候能够加深自己的理解,促进自己的成长吧。
(一)游戏的主要逻辑和游戏关卡设置
这个游戏简单说就是两个场景,一个是开始场景,另一个是游戏的主场景,开始场景顾名思义是处理玩家进入的时候的逻辑,大致有一个开始按钮,当玩家按下的时候开始进入主场景,开始设计模式,玩家操纵主场景中的飞机来射击敌人的飞机,敌人的种类可以分为几种,一种是普通的敌人,一种是超级敌人,超级敌人除了继承普通的敌人外,还具有普通敌人不具备的技能,例如伤害更高,生命值更高等,(这里用到面向对象中的继承,为普通敌人加入一个脚本,控制敌人的一些行为,然后再为超级敌人写一个脚本,让他直接继承普通敌人,然后重写部分普通敌人中的方法,这里注意了重写的方法必须是public或者是protected的,包括一些属性也是,不能设置为private这样是没法在继承的脚本中修改或继承的),然后有一个控制玩家输入控制的脚本,用来操作游戏中的玩家对象,还有一些敌人或者玩家附带的处理脚本,最后整个游戏的控制脚本,用来控制游戏的主要逻辑,和协调各个游戏脚本之间的消息传递等等。
(二)游戏的创建
这个游戏中虽说简单,但是也不可能面面俱到,只能把游戏中我感觉比较重要或者能学到知识的地方记录下来,便于以后自己查看,废话不多说,现在进入正题,就是脚本的编写。
1.游戏操作方面的操作控制
if(Input.GetKey(KeyCode.UpArrow)) { //注意这里为什么是-=,由于场景中的对象的局部坐标系和世界坐标系是相反的 vMove -= playerSpeed * Time.deltaTime; } //下移 if(Input .GetKey(KeyCode.DownArrow)) { vMove += playerSpeed * Time.deltaTime; } //左移 if(Input.GetKey(KeyCode.LeftArrow)) { hMove += playerSpeed * Time.deltaTime; } //右移 if(Input.GetKey(KeyCode.RightArrow)) { hMove -= playerSpeed * Time.deltaTime; }
这里主要是处理一些处理玩家移动的脚本,通过键盘上面的上下左右来控制游戏中玩家的移动,这里有一点要注意的是,一定要和上下左右的移动一定要和游戏中玩家的上下左右移动同步,注意主角对象的本地坐标。然后通过Transform类中的Translate方法实现玩家的移动。
//变换位置信息 playerTraform.Translate(new Vector3(hMove,0,vMove));
2.玩家发射子弹
//发射子弹,设置子弹的发射速度 rocketRate -= Time.deltaTime; //发射子弹 if (rocketRate < 0) { rocketRate = 0.1f; if (Input.GetMouseButton(0) || Input.GetKey(KeyCode.Space)) { Instantiate(rocket, playerTraform.position, playerTraform.rotation); //CachedObject ch = CachedObject.Create("PlayerRcoket", rocket); //ch.CreateAsset(playerTraform.position, playerTraform.eulerAngles); //播放发射声音 m_audio.PlayOneShot(m_shootClip); } }
这段代码主要想达到的效果就是当我点下空格键或者鼠标的左键的时候,能让玩家的飞机发射一颗子弹,然后就是一个rocketRate控制玩家发射子弹的速率,间隔多少秒可以发射一次,让画面看起来更加自然。
3.敌人的脚本
//处理敌人的随机移动 protected virtual void EnemyMove() { hMove = Mathf.Sin(Time.time)*Time.deltaTime; enemyTransfrom.Translate(new Vector3(hMove, 0, -enemySpeed*Time.deltaTime)); }
这里需要注意的一个比较重要的是设置为一个虚方法(virtual)这个用到了C#中的方法的继承与重写一些知识点,后面重写的时候要用到关键字override,,敌人和超级敌人的移动方式可能是不一样的,所以这里要声明一个虚方法,直接看看后面重写的方法。
protected override void EnemyMove() { enemyTransfrom.Translate(new Vector3(0,0,-enemySpeed*Time.deltaTime)); delaySengTime -= Time.deltaTime; if (delaySengTime <= 0) { delaySengTime = 2f; // if (playerTransform != null) { Vector3 directionPos = playerTransform.position - enemyTransfrom.position; Instantiate(enemyRocket, enemyTransfrom.position, Quaternion.LookRotation(-directionPos)); } } }
这个重写的超级敌人的移动主要是处理自身的移动,间隔一定时间捕捉游戏玩家的方位,然后向玩家发射一颗子弹。
4.碰撞的检测,主要用于处理一些碰撞,然后处理一些逻辑事件
protected void OnTriggerEnter(Collider cli) { //当被打中 if(cli.tag=="Rocket") { Debug.Log("1111"); Rocket rocketSpript = cli.GetComponent<Rocket>(); if (rocketSpript != null) { Debug.Log("222"); enemyLiftTime -= rocketSpript.rocketLiveTime; if(enemyLiftTime<=0) { GameManager.Instance.AddScore(10); //播放特效 Instantiate(exposionFX,enemyTransfrom.position,Quaternion.identity); Destroy(this.gameObject); } } } //当撞到主角 if(cli.tag=="Player") { Debug.Log("44"); //播放特效 Instantiate(exposionFX,enemyTransfrom.position,Quaternion.identity); Destroy(this.gameObject); } // if(cli.tag=="Bound") { Debug.Log("333"); Destroy(this.gameObject); } }
由于用到的碰撞检测的地方很多,包括子弹与敌人,子弹与超极敌人,玩家与敌人和超极敌人,超极敌人发射的子弹和玩家的碰撞等,这里只拿出敌人脚本中的碰撞检测的方法,这里用到了一个 OnTriggerEnter(Collider cli )方法,这是mono中带的一个方法,用于检测碰撞用的,然后将cli 传入,通过判断cli对象的tag来实现一些逻辑上的判断和事件的处理。(关于碰撞的注意事项可以参考这篇文章)
5.不定时的生成敌人
using UnityEngine; using System.Collections; [AddComponentMenu("MyGame/EnemySpawn")] public class EnemySpawn : MonoBehaviour { public GameObject enemyObj; //敌人的生成时间jiange protected float timeSpawn=5; protected Transform enemyTransform; void Start() { enemyTransform = this.transform; } void Update() { timeSpawn -= Time.deltaTime; if (timeSpawn <= 0) { timeSpawn = Random.Range(5,10); Instantiate(enemyObj,enemyTransform.position,Quaternion.identity); } } //绘制sence中的gizmos的显示 void OnDrawGizmos() { Gizmos.DrawIcon(transform.position,"item.png",true); } }
其中OnDrawGizmos()方法是unity中的,用来绘制Icon的,需要新建一个Gizmos的文件夹,里面放入item.png文件,用来做敌人的生成位置,其中Update中是一个Random类用于不定时的生成一些敌人。
6.游戏的主管理器脚本
using UnityEngine; using System.Collections; [AddComponentMenu("MyGame/GameManager")] public class GameManager : MonoBehaviour { public static GameManager Instance; //得分 public int score = 0; //纪录 public static int hisScore = 0; //主角脚本 protected Player playerScript; //背景音乐 public AudioClip backgroundMusic; //声音源 protected AudioSource m_audio; void Awake() { Instance = this; } void Start() { m_audio = this.gameObject.GetComponent<AudioSource>(); GameObject obj= GameObject.FindGameObjectWithTag("Player"); if(obj!=null) { playerScript = obj.GetComponent<Player>(); } } void Update() { if(!m_audio.isPlaying) { m_audio.clip = backgroundMusic; m_audio.Play(); //m_audio.PlayOneShot(backgroundMusic); } //暂停游戏 if(Time.timeScale>0&&Input.GetKeyDown(KeyCode.Escape)) { Time.timeScale = 0; } } void OnGUI() { //暂停游戏 if (Time.timeScale == 0) { //继续按钮 if (GUI.Button(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.4f, 100, 30), "继续游戏")) { Time.timeScale = 1; } //推出游戏按钮 if (GUI.Button(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.6f, 100, 30), "退出游戏")) { Application.Quit(); } } //生命显示 int playerLife = 0; if (playerScript != null) { //获取主角的生命值 playerLife = (int)playerScript.playerLife; } else//游戏对象Player消失 gameover { //放大字体 GUI.skin.label.fontSize = 50; //显示游戏失败 GUI.skin.label.alignment = TextAnchor.LowerCenter; GUI.Label(new Rect(0, Screen.height * 0.2f, Screen.width, 60), "游戏失败"); GUI.skin.label.fontSize = 20; //显示按钮 if (GUI.Button(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.5f, 100, 30), "再试一次")) { //重新读取关卡 Application.LoadLevel(0); } } //显示分数 GUI.skin.label.fontSize = 15; GUI.Label(new Rect(5, 5, 100, 30), "装甲" + playerLife); //显示最高分 GUI.skin.label.alignment = TextAnchor.LowerCenter; GUI.Label(new Rect(0, 5, Screen.width, 30), "记录" + hisScore); //显示分数 GUI.Label(new Rect(0, 25, Screen.width, 30), "得分" + score); } public void AddScore(int point) { score += point; //更新最高分数 if (hisScore < score) { hisScore = score; } } }
这个脚本中主要的内容就是OnGUI()函数,其中OnGUI()里面又来绘制我们游戏中的一些UI界面,UI的主要内容包括显示玩家的生命值,玩家的得分,和最高得分,通过一获取玩家的脚本可以读取到玩家的生命值,同时也是通过获取玩家的脚本,看玩家是否存在,即判断玩家是否死亡,如果脚本不存在玩家就死亡,然后将Time类中的timeScale设置为0,即暂停游戏的进度,然后显示GUI,显示游戏失败,再来一局的UI .核心的东西就是通过找到玩家对象然后获取玩家的Player脚本。
7.游戏开始界面的UI
using UnityEngine; using System.Collections; [AddComponentMenu("MyGame/TitleScreen")] public class TitleScreen : MonoBehaviour { void OnGUI() { //文字大小 GUI.skin.label.fontSize = 48; //UI中心对其 GUI.skin.label.alignment = TextAnchor.LowerCenter; //显示标题 GUI.Label(new Rect(0,30,Screen.width,100),"太空大战"); //开始 if(GUI.Button(new Rect(Screen.width*0.5f-100,Screen.height*0.7f,200,30),"开始游戏")) { //duqu Application.LoadLevel(1); } } }
用来显示游戏开始界面,提供玩家进入游戏的一个视图,然后可以点击开始游戏开始。
(三)游戏控制的另一种实现(鼠标控制玩家的位置)
void MoveTo() { if(Input.GetMouseButton(1)) { Debug.Log("右键单击了"); // // Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if(Physics.Raycast(ray,out hit,1000,inputMask)) { mouseTarget= hit.point; Debug.Log(hit.point); } } this.playerTraform.position = Vector3.MoveTowards(this.playerTraform.position, mouseTarget, playerSpeed * Time.deltaTime); }
//声明的碰撞层和三维向量
protected Vector3 mouseTarget; public LayerMask inputMask;
通过单击鼠标的右键来控制玩家的位置,主要是通过设置一个碰撞层,原理是通过摄像机向鼠标点击的位置发射一条射线,当射线碰到这个层上面的时候,通过Raycast获取一个hit,然后可以通过hit.point获取这个点的位置,然后让游戏对象移动到这个位置,即达到了鼠标点击控制玩家移动的功能。
(四)最后便是游戏的优化和发布了
using UnityEngine; using System.Collections; public class CachedObject : MonoBehaviour { //是否在使用中 public bool inUse { set; get; } //资源 protected GameObject _asset = null; //资源中包括的粒子 protected ParticleSystem[] _ps; //创建一个缓存资源 public static CachedObject Create(string assetname,GameObject prefab) { GameObject go = new GameObject(); go.name = "cache_" + assetname; CachedObject co = go.AddComponent<CachedObject>(); co.Init(assetname, prefab); return co; } //缓存一个游戏体 void Init(string assetname,GameObject prefab) { inUse = false; //创建一个资源并移动到看不见的位置 Vector3 pos = new Vector3(10000,10000,10000); _asset = (GameObject)GameObject.Instantiate(prefab,pos,Quaternion.identity); _ps = _asset.GetComponentsInChildren<ParticleSystem>(); //隐藏这个资源 _asset.SetActive(false); } //在游戏中创建资源(将隐藏的资源显示出来) public GameObject CreateAsset(Vector3 pos,Vector3 euler) { if(_asset==null) { return null; } inUse = true; this.transform.position = pos; this.transform.eulerAngles = euler; //显示资源 _asset.SetActive(true); foreach(ParticleSystem ps in _ps) { ps.Play(true); } return _asset; } //清除缓存资源 public GameObject DestroyAsset() { if(_asset==null) { return null; } inUse = true; this.transform.position = new Vector3(10000, 10000, 10000); this.transform.eulerAngles=Vector3.zero; //隐藏 _asset.SetActive(false); //停止粒子运动 foreach(ParticleSystem ps in _ps) { ps.Stop(true); } return _asset; } }
//初始化载入资源 CacheObject obj=CacheObject.Create("assetName",tarPrefab); //在需要的时候缓存资源 obj.CreateAsset(Vector.zero,Vector.zero); //在不需要的时候隐藏 obj.DestroyAsset(); //在用C#的GC,来垃圾回收,防止类存泄露 //强行每30帧清理一次 if(Time.frameCount%30==0) { System.GC.Collect(); }
这里主要测试游戏的一些Bug和优化部分了,优化主要通过优化资源的加载和销毁,游戏中尽量避免大量销毁和生成GameObject这样会带来很大的性能开销,可以把一部分销毁的对象采取影藏的功能,当用到的时候便把它显示出来,这样就可以减轻一部分的开销,游戏性能的优化是一个大的学问,需要不断的学习。
游戏的发布这里就不再说明,可以参考网上其他的说明,应该看一遍就大致会明白了。
读到这里这篇随笔也就接近尾声了,这里要感谢金玺曾老师的《Unity3d/2d手机游戏开发》这本书,发这篇随笔主要是想记叙学习的一个过程,以后再翻阅的时候也能游戏开发的环节有一个巩固吧。帮助提高