塔防游戏 您所在的位置:网站首页 塔防游戏代码实现原理 塔防游戏

塔防游戏

2024-06-03 02:54| 来源: 网络整理| 查看: 265

思考并回答以下问题:

本章涵盖:

策划 地图编辑器 “格子”数据 在Inspector窗口添加自定义U控件 创建一个自定义窗口 游戏场景 制作UI 创建游戏管理器 摄像机 路点 敌人 敌人生成器 创建敌人生成器 遍历敌人 防守单位 生命条 小结

本章将使用Unity完成一款塔防游戏。我们将使用自定义的编辑器创建场景,创建路点引导敌人行动,对战斗进行配置、动画播放,还涉及摄像机控制和UI界面等。

策划

塔防游戏的基本玩法比较类似,在场景中我方有一个基地,敌人从场景的另一侧出发,沿着相对固定的路线攻打基地。我方可以在地图上布置防守单位,攻击前来进攻的敌人,防止他们闯入基地。

**场景**

塔防游戏的场景有些固定的模式,它由一个二维的单元格组成,每个格子的用途可能都不同:

专用于摆放防守单位的格子。 专用于敌人通过的格子。 既无法摆放防守单位,也不允许敌人通过的格子。 **摄像机**

摄像机始终由上至下俯视游戏场景,按住鼠标左键并移动可以移动摄像机的位置。

**胜负判定**

我方基地有10点生命值,敌人攻入基地一次减少一点生命值,当生命值为0,游戏失败。

敌人以波数的形式向我方基地进攻,每波由若干个敌人组成。在这个实例中,一关有10波,当成功击退敌人10波的进攻则游戏胜利。

**敌人**

敌人有两种:一种在陆地上行走;另一种可以飞行。每打倒一个敌人会奖励一些铜钱,用来购买新的防守单位。

**防守单位**

塔防游戏会有多种类型的防守单位。本游戏有两种类型的防守单位:一种是近战类型;另一种是远程。每造一个防守单位需要消耗相应数量的铜钱。

**UI界面**

游戏中的UI包括防守单位的按钮、敌人的进攻波数、基地的生命值和铜钱数量。

当防守单位攻击敌人时,在敌人的头上需要显式一个生命条表示剩余的生命值。

当游戏失败或胜利后显示一个按钮重新游戏。

地图编辑器

在正式开始制作游戏之前,我们有必要先完成一个塔防游戏的地图编辑器。Unity编辑器的自定义功能非常强大,几乎可以把Unity编辑器扩展成任何界面。在示例中,我们将完成一个“格子”编辑系统,帮助我们输入塔防游戏的地图信息。

“格子”数据

新建工程,在Hierarchy窗口中创建一个空对象,然后创建脚本TileObject.cs指定给这个空对象,这里将空对象命名为Grid Object,这个类主要用于保存场景中的“格子”数据,代码如下所示。

TileObject.cs

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293using UnityEngine;public class TileObject : MonoBehaviour { public static TileObject Instance = null; // tile 碰撞层 public LayerMask tileLayer; // tile 大小 public float tileSize = 1; // x 轴方向tile数量 public int xTileCount = 2; // z 轴方向tile数量 public int zTileCount = 2; // 格子的数值,0表示锁定,无法摆放任何物体。1表示敌人通道,2表示可摆放防守单位 public int[] data; // 当前数据 id [HideInInspector] public int dataID = 0; [HideInInspector] // 是否显示数据信息 public bool debug = false; void Awake() { Instance = this; } // 初始化地图数据 public void Reset() { data = new int[xTileCount * zTileCount]; } // 获得相应tile的数值 public int getDataFromPosition(float pox, float poz) { int index = (int)((pox - transform.position.x)/ tileSize) * zTileCount + (int)((poz - transform.position.z)/ tileSize); if (index < 0 || index >= data.Length) return 0; return data[index]; } // 设置相应tile的数值 public void setDataFromPosition( float pox, float poz, int number ) { int index = (int)((pox - transform.position.x) / tileSize) * zTileCount + (int)((poz - transform.position.z) / tileSize); if (index < 0 || index >= data.Length) return; data[index] = number; } // 在编辑模式显示帮助信息 void OnDrawGizmos() { if (!debug) return; if (data==null) { Debug.Log("Please reset data first"); return; } Vector3 pos = transform.position; for (int i = 0; i < xTileCount; i++) // 画Z方向轴辅助线 { Gizmos.color = new Color(0, 0, 1, 1); Gizmos.DrawLine(pos + new Vector3(tileSize * i, pos.y, 0), transform.TransformPoint(tileSize * i, pos.y, tileSize * zTileCount)); for (int k = 0; k < zTileCount; k++) // 高亮显示当前数值的格子 { if ( (i * zTileCount + k) < data.Length && data[i * zTileCount + k] == dataID) { Gizmos.color = new Color(1, 0, 0, 0.3f); Gizmos.DrawCube(new Vector3(pos.x + i * tileSize + tileSize * 0.5f, pos.y, pos.z + k * tileSize + tileSize * 0.5f), new Vector3(tileSize, 0.2f, tileSize)); } } } for (int k = 0; k < zTileCount; k++) // 画X方向轴辅助线 { Gizmos.color = new Color(0, 0, 1, 1); Gizmos.DrawLine(pos + new Vector3(0, pos.y, tileSize * k), this.transform.TransformPoint(tileSize * xTileCount, pos.y, tileSize * k)); } }} 在Inspector窗口添加自定义UI控件

Unity提供了API可以扩展Inspector窗口中的UI控件。

(1)以本示例的地图编辑器为例,为了扩展TileObject这个类的Inspector窗口,我们创建了脚本TileEditor.cs,继承自Editor。因为它是一个编辑器脚本,所以必须放到Editor文件夹中,代码如下:

TileEditor.cs

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273using UnityEngine;using UnityEditor;[CustomEditor(typeof(TileObject))]public class TileEditor : Editor { // 是否处于编辑模式 protected bool editMode = false; // 受编辑器影响的tile脚本 protected TileObject tileObject; void OnEnable() { // 获得tile脚本 tileObject = (TileObject)target; } // 更改场景中的操作 public void OnSceneGUI() { if (editMode) // 如果在编辑模式 { // 取消编辑器的选择功能 HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); // 在编辑器中显示数据(画出辅助线) tileObject.debug = true; // 获取Input事件 Event e = Event.current; // 如果是鼠标左键 if ( e.button == 0 && (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) && !e.alt) { // 获取由鼠标位置产生的射线 Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); // 计算碰撞 RaycastHit hitinfo; if (Physics.Raycast(ray, out hitinfo, 2000, tileObject.tileLayer)) { //float tx = hitinfo.point.x - tileObject.transform.position.x; //float tz = hitinfo.point.z - tileObject.transform.position.z; tileObject.setDataFromPosition(hitinfo.point.x, hitinfo.point.z, tileObject.dataID); } } } HandleUtility.Repaint(); } // 自定义Inspector窗口的UI public override void OnInspectorGUI() { GUILayout.Label("Tile Editor"); // 显示编辑器名称 editMode = EditorGUILayout.Toggle("Edit", editMode); // 是否启用编辑模式 tileObject.debug = EditorGUILayout.Toggle("Debug", tileObject.debug); // 是否显示帮助信息 //tileObject.dataID = EditorGUILayout.IntSlider("Data ID", tileObject.dataID, 0, 9); // 编辑id滑块 string[] editDataStr = { "Dead", "Road", "Guard" }; tileObject.dataID = GUILayout.Toolbar(tileObject.dataID, editDataStr); //Debug.Log(tileObject.dataID); EditorGUILayout.Separator(); // 分隔符 if (GUILayout.Button("Reset" )) // 重置按钮 { tileObject.Reset(); // 初始化 } DrawDefaultInspector(); }}

(2)

(3)

(4)

游戏场景创建游戏管理器

GameManager.cs

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250using UnityEngine;using UnityEngine.SceneManagement;using System.Collections;using System.Collections.Generic;using UnityEngine.UI; // 注意UI控件命名空间的引用using UnityEngine.Events; // 注意UI事件命名空间的引用using UnityEngine.EventSystems; // 注意UI事件命名空间的引用public class GameManager : MonoBehaviour { public static GameManager Instance; // 显示路点的debug开关 public bool m_debug = true; // 路点 public List m_PathNodes; // 敌人列表 public List m_EnemyList = new List(); // 地面的碰撞Layer public LayerMask m_groundlayer; // 波数 public int m_wave = 1; public int m_waveMax = 10; // 生命 public int m_life = 10; // 铜钱数量 public int m_point = 30; // UI文字控件 Text m_txt_wave; Text m_txt_life; Text m_txt_point; // UI重新游戏按钮控件 Button m_but_try; // 当前是否选中的创建防守单位的按钮 bool m_isSelectedButton =false; void Awake() { Instance = this; } // Use this for initialization void Start () { // 创建UnityAction,在OnButCreateDefenderDown函数中响应按钮按下事件 UnityAction downAction = new UnityAction(OnButCreateDefenderDown); // 创建UnityAction,在OnButCreateDefenderDown函数中响应按钮抬起事件 UnityAction upAction = new UnityAction(OnButCreateDefenderUp); // 创建按钮按下事件Entry EventTrigger.Entry down = new EventTrigger.Entry(); down.eventID = EventTriggerType.PointerDown; down.callback.AddListener(downAction); // 创建按钮抬起事件Entry EventTrigger.Entry up = new EventTrigger.Entry(); up.eventID = EventTriggerType.PointerUp; up.callback.AddListener(upAction); // 查找所有子物体,根据名称获取UI控件 foreach (Transform t in this.GetComponentsInChildren()) { if (t.name.CompareTo("wave") == 0) //找到文字控件"波数" { m_txt_wave = t.GetComponent(); SetWave(1); } else if (t.name.CompareTo("life") == 0) //找到文字控件"生命" { m_txt_life = t.GetComponent(); m_txt_life.text = string.Format("生命:{0}", m_life); } else if (t.name.CompareTo("point") == 0) //找到文字控件"铜钱" { m_txt_point = t.GetComponent(); m_txt_point.text = string.Format("铜钱:{0}", m_point); } else if (t.name.CompareTo("but_try") == 0) //找到按钮控件"重新游戏" { m_but_try = t.GetComponent(); // 添加按钮单击函数回调,重新游戏按钮 m_but_try.onClick.AddListener( delegate() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); }); // 默认隐藏重新游戏按钮 m_but_try.gameObject.SetActive(false); } else if (t.name.Contains("but_player")) //找到按钮控件"创建防守单位" { // 给创建防守单位按钮添加EventTrigger,并添加前面定义的按钮事件 EventTrigger trigger = t.gameObject.AddComponent(); trigger.triggers = new List(); trigger.triggers.Add(down); trigger.triggers.Add(up); } } BuildPath(); } // Update is called once per frame void Update () { // 如果选中创建士兵的按钮则取消摄像机操作 if (m_isSelectedButton) return; // 鼠标或触屏操作,注意不同平台的Input代码不同#if (UNITY_IOS || UNITY_ANDROID) && !UNITY_EDITOR bool press = Input.touches.Length > 0 ? true : false; // 手指是否触屏 float mx = 0; float my = 0; if (press) { if ( Input.GetTouch(0).phase == TouchPhase.Moved) // 获得手指移动距离 { mx = Input.GetTouch(0).deltaPosition.x * 0.01f; my = Input.GetTouch(0).deltaPosition.y * 0.01f; } }#else bool press = Input.GetMouseButton(0); // 获得鼠标移动距离 float mx = Input.GetAxis("Mouse X"); float my = Input.GetAxis("Mouse Y"); #endif // 移动摄像机 GameCamera.Inst.Control(press, mx, my); } // 更新文字控件"波数" public void SetWave(int wave) { m_wave= wave; m_txt_wave.text = string.Format("波数:{0}/{1}", m_wave, m_waveMax); } // 更新文字控件"生命" public void SetDamage(int damage) { m_life -= damage; if (m_life { m_liveEnemy--; });// 当敌人死掉时减少敌人数量 enemyIndex++; // 更新敌人数组下标 yield return new WaitForSeconds(wave.interval); // 生成敌人时间间隔 } // 创建完全部敌人,等待敌人全部被消灭 while(m_liveEnemy>0) { yield return 0; } enemyIndex = 0; // 重置敌人数组下标 waveIndex++; // 更新战斗波数 if (waveIndex< waves.Count) // 如果不是最后一波 { StartCoroutine(SpawnEnemies()); } else { // 通知胜利 } } // 在编辑器中显示一个图标 void OnDrawGizmos() { Gizmos.DrawIcon(transform.position, "spawner.tif"); }}

(3)创建一个空游戏体作为敌人生成器放置到场景中,为其指定EnemySpawner.cs脚本。在m_startNode中设置起始路点,在Waves中配置敌人的生成,这里配置了10波,如下图所示。

遍历敌人123456789101112131415161718192021222324void Start () { GameManager.Instance.m_EnemyList.Add(this); // ...}public void DestroyMe(){ GameManager.Instance.m_EnemyList.Remove(this); onDeath(this); // 发布死亡消息 Destroy(this.gameObject); // 注意在实际项目中一般不要直接调用Destroy}public void SetDamage(int damage){ m_life -= damage; if (m_life


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有