Unity实例开发-AlienShooter(二)

通往下一关:

Unity实例开发-AlienShooter(二)

脚本及其实现方法:

自动删除脚本AutoDestroy:

控制每次子弹发射后自动销毁。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AutoDestroy : MonoBehaviour {

	// Use this for initialization
	//子弹创建后自动销毁
	void Start () {
		Destroy (gameObject, 0.1f);	
	}
}

传送门Door:

一个碰撞方法,控制与玩家的碰撞。

通关方法:首先限制玩家的移动,然后加载幕布,重置玩家的位置,重置地图,最后恢复玩家的移动,销毁传送门。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Door : MonoBehaviour {
	// Use this for initialization
	void OnCollisionEnter(Collision col)
	{
		//碰撞方法:如果是玩家与其发生了碰撞 执行通关方法
		if (col.transform.tag == "Player")
			StartCoroutine (Operator (col.transform.GetComponent<MyPlayer> ()));
	}
	//通关方法
	private IEnumerator Operator(MyPlayer player)
	{
		player.enabled = false;									//限制玩家移动
		yield return new WaitForSeconds (1f);					
		Vector3 pos = player.transform.position;
		UIManager.Instance.Init ();								//黑幕
		++GlobeValue.Level;										//关卡递增
		pos.x = new IntegerVector2 (0, 0).X;					//重置玩家位置
		pos.z = new IntegerVector2 (0, 0).Z;
		player.transform.position = pos;						
		FindObjectOfType<MyMap> ().InitMap ();					//重置地图
		player.enabled = true;									//恢复玩家移动
		Destroy (gameObject);									//销毁传送门	
	}
}

  GlobeValue:

保存游戏需要的全局变量。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GlobeValue : MonoBehaviour {
	//全局变量
	public static int Level = 0;   	//关卡
	public static int Score = 0;	//得分
}

A*算法 AStar脚本:

A*寻路算法:
    1. 从起点A开始, 把它作为待处理的方格存入一个"开启列表", 开启列表就是一个等待检查方格的列表. 
    2. 寻找起点A周围可以到达的方格, 将它们放入"开启列表", 并设置它们的"父方格"为A. 
    3. "开启列表"中删除起点 A, 并将起点 A 加入"关闭列表", "关闭列表"中存放的都是不需要再次检查的方格
    图中浅绿色描边的方块表示已经加入 "开启列表" 等待检查. 淡蓝色描边的起点 A 表示已经放入 "关闭列表" , 
    它不需要再执行检查.  "开启列表" 中找出相对最靠谱的方块, 什么是最靠谱? 它们通过公式 F=G+H 来计算.
    F = G + H 
        G 表示从起点 A 移动到网格上指定方格的移动耗费 (可沿斜方向移动). 
        H 表示从指定的方格移动到终点 B 的预计耗费 (H 有很多计算方法, 这里我们设定只可以上下左右移动).
    4. 把它从 "开启列表" 中删除, 并放到 "关闭列表" . 
    5. 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑) 的方格. 如果这些方格还不在 
    "开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G, H  F 值各是多少, 并设置它们的 "父方格"  C. 
    6. 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更
    低一些, 如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G  (H 值不需要
        重新计算, 因为对于每个方块, H 值是不变的). 如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择,
    因为它需要更远的路, 这时我们什么也不做.
    除了起始方块, 每一个曾经或者现在还在 "开启列表" 里的方块, 它都有一个 "父方块", 通过 "父方块" 可以索引到最初
     "起始方块", 这就是路径.

首先有初始化地图的方法,制作一个二维数组,保存是否有障碍等信息。

然后有寻路方法的实现,最后通过一个函数调用寻路算法就可以找到路径。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyAStar : MonoBehaviour {
	
	public MyPoint[,] map; 											//地图
	//初始化地图
	public void InitMap(bool[,] place)
	{
		map = new MyPoint[MyMap.LENGTH, MyMap.LENGTH];
		for (int i = 0; i < MyMap.LENGTH; ++i)
			for (int j = 0; j < MyMap.LENGTH; ++j) {
				map[i, j] = new MyPoint (i, j);
				if (place [i, j]) 									//有障碍
					map[i, j].isWall = true;
			}
	}
	/*
	A*寻路算法:
	1. 从起点A开始, 把它作为待处理的方格存入一个"开启列表", 开启列表就是一个等待检查方格的列表. 
	2. 寻找起点A周围可以到达的方格, 将它们放入"开启列表", 并设置它们的"父方格"为A. 
	3. 从"开启列表"中删除起点 A, 并将起点 A 加入"关闭列表", "关闭列表"中存放的都是不需要再次检查的方格
	图中浅绿色描边的方块表示已经加入 "开启列表" 等待检查. 淡蓝色描边的起点 A 表示已经放入 "关闭列表" , 
	它不需要再执行检查. 从 "开启列表" 中找出相对最靠谱的方块, 什么是最靠谱? 它们通过公式 F=G+H 来计算.
	F = G + H 
		G 表示从起点 A 移动到网格上指定方格的移动耗费 (可沿斜方向移动). 
		H 表示从指定的方格移动到终点 B 的预计耗费 (H 有很多计算方法, 这里我们设定只可以上下左右移动).
	4. 把它从 "开启列表" 中删除, 并放到 "关闭列表" 中. 
	5. 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑) 的方格. 如果这些方格还不在 
	"开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G, H 和 F 值各是多少, 并设置它们的 "父方格" 为 C. 
	6. 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更
	低一些, 如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要
		重新计算, 因为对于每个方块, H 值是不变的). 如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择,
	因为它需要更远的路, 这时我们什么也不做.
	除了起始方块, 每一个曾经或者现在还在 "开启列表" 里的方块, 它都有一个 "父方块", 通过 "父方块" 可以索引到最初
	的 "起始方块", 这就是路径.
	*/
	private void FindPath(MyPoint start, MyPoint end)
	{
		List<MyPoint> openList = new List<MyPoint> ();  						//开始列表
		List<MyPoint> closeList = new List<MyPoint> ();  						//结束列表

		openList.Add (start);													//起点装入开始列表
		while (openList.Count > 0) {
			MyPoint point = FindMinFPoint(openList);   							//寻找F最小的点
			openList.Remove(point);		
			closeList.Add(point);

			List<MyPoint> surrendPoints = GetSurrendPoints(point, closeList); 	//获得其周围点
			foreach(MyPoint p in surrendPoints)
			{
				if (openList.Contains (p)) {       								//起始列表中包含p
					float newG = CalG (p, point);   							//计算G的值
					if (newG < p.G)
						p.UpdatePoint (point, newG); 							//更新点p的G值
				} 
				else {
					p.parent = point;
					CalF (p, end); 												//计算F的值
					openList.Add (p);
				}
			}
				
			if (openList.Contains (end)) {   									//找到了路径。
				break;
			}
				
		}
	}
	//寻找开始列表中F值最小点,并返回
	private MyPoint FindMinFPoint(List<MyPoint> list)
	{
		float f = float.MaxValue;
		MyPoint point = null;
		foreach (MyPoint p in list) {
			if (f > p.F) {
				f = p.F;
				point = p;
			}
		}
		return point;
	}
	//判断点是否合法
	private bool isValid(int x, int y, List<MyPoint> closeList)
	{
		if (x >= 0 && x < MyMap.LENGTH && y >= 0 && y < MyMap.LENGTH &&
		    !map [x, y].isWall && !closeList.Contains (map [x, y]))
			return true;
		return false;
	}
	//获取点p周围4个点,返回包含点的list
	private List<MyPoint> GetSurrendPoints(MyPoint p, List<MyPoint> closeList)
	{
		List<MyPoint> list = new List<MyPoint> ();
		if(isValid(p.x-1, p.y, closeList))
			list.Add (map [p.x-1, p.y]);
		if(isValid(p.x+1, p.y, closeList))
			list.Add (map [p.x+1, p.y]);
		if(isValid(p.x, p.y-1, closeList))
			list.Add (map [p.x, p.y-1]);
		if(isValid(p.x, p.y+1, closeList))
			list.Add (map [p.x, p.y+1]);
		return list;
	}
	//计算点p通过parnet得到的新的G值
	private float CalG(MyPoint p, MyPoint parent)
	{
		float dis = Vector2.Distance (new Vector2 (p.x, p.y), new Vector2 (parent.x, parent.y));
		return parent.G + dis;
	}
	//计算点p的F值
	private void CalF(MyPoint p, MyPoint end)
	{
		p.H = Mathf.Abs (p.x - end.x) + Mathf.Abs (p.y - end.y);
		p.G = CalG (p, p.parent);
		p.F = p.G + p.H;
	}
	//给出起点与终点,返回要走的下一个位置的点
	public MyPoint Path(IntegerVector2 s, IntegerVector2 e)
	{
		MyPoint start =  map[s.x, s.z]; 
		MyPoint end = map [e.x, e.z];
		FindPath (start, end);

		MyPoint target = end;
		while (target.parent != start) 
			target = target.parent;
		return target;
	}
}

音乐控制脚本AudioController   :   

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyAudioController : MonoBehaviour {
	public static MyAudioController Instance = null;  											//单例模式:整个项目实例化一次
	private Dictionary<string, int> AudioDictionary = new Dictionary<string, int> ();			//字典,用于保存音效
	private const int MaxAudioCount = 10;														//最大音效数目
	private const string ResourcePath = "Audio/";												//资源路径
	private const string StreamingAssetsPath = "";					
	private AudioSource bgmAudioSource = null;													//bgm音乐组件
	private AudioSource lastAudioSource = null;													//上次音效音乐组件

	void Awake()
	{
		Instance = this;
	}
	enum eResType
	{
		AB = 0,
		CLIP = 1
	};
	/// <summary>
	/// 下载音效
	/// </summary>
	/// <returns>The audio clip.</returns>
	/// <param name="audioName">Audio name.</param>
	/// <param name="type">Type.</param>
	private AudioClip GetAudioClip(string audioName, eResType type = eResType.CLIP)
	{
		AudioClip audioClip = null;

		switch (type) {
		case eResType.AB:
			break;
		case eResType.CLIP:
			audioClip = GetResAudioClip (audioName);
			break;
		default:
			break;
		}

		return audioClip;
	}
	//获取音乐
	private AudioClip GetResAudioClip(string audioName)
	{
		return Resources.Load (ResourcePath + audioName) as AudioClip;
	}

	/// <summary>
	/// 音效播放
	/// </summary>
	/// <param name="audioName">Audio name.</param>
	/// <param name="volume">Volume.</param>
	/// <param name="name">Name.</param>
	public void SoundPlay(string audioName, float volume = 1f, string name = null)
	{
		if (audioName == null)
			return;
		if (!AudioDictionary.ContainsKey (audioName))
			AudioDictionary.Add (audioName, 1);
		if (AudioDictionary [audioName] <= MaxAudioCount) {
			AudioClip clip = GetAudioClip (audioName);
			if (clip != null)
				StartCoroutine (PlayClip(clip, audioName, volume));
		}
	}

	/// <summary>
	/// 播放音效
	/// </summary>
	/// <returns>The clip.</returns>
	/// <param name="clip">Clip.</param>
	/// <param name="audioName">Audio name.</param>
	/// <param name="volume">Volume.</param>
	/// <param name="name">Name.</param>
	private IEnumerator PlayClip(AudioClip clip, string audioName, float volume = 1.0f, string name = null)
	{
		++AudioDictionary [audioName];

		GameObject obj = new GameObject (name == null ? "SoundClip" : name);
		obj.transform.parent = transform;
		AudioSource source = obj.AddComponent<AudioSource> ();
		source.volume = volume;													//音量
		source.clip = clip;														
		source.pitch = 1f;														//音调
		source.Play ();
		lastAudioSource = source;

		yield return new WaitForSeconds (clip.length * Time.timeScale);

		--AudioDictionary [audioName];
		if (AudioDictionary [audioName] <= 0)
			AudioDictionary.Remove (audioName);
		Destroy (obj);
	}
	/// <summary>
	/// 暂停
	/// </summary>
	public void SoundPause()
	{
		if (lastAudioSource)
			lastAudioSource.Pause ();
	}
	/// <summary>
	/// 所有音效暂停
	/// </summary>
	public void AllSoundPause()
	{
		AudioSource[] allSource = FindObjectsOfType <AudioSource>();
		if (allSource != null)
			for (int i = 0; i < allSource.Length; ++i)
				if(allSource [i] != bgmAudioSource)   							//不应该是bgm
					allSource [i].Pause ();
	}
	/// <summary>
	/// 停止特定的音效
	/// </summary>
	/// <param name="audioName">Audio name.</param>
	public void SoundStop(string audioName)
	{
		GameObject obj = transform.Find (audioName).gameObject; 				//寻找子对象
		if (obj)
			Destroy (obj);
	}
	/// <summary>
	/// 播放背景音乐
	/// </summary>
	/// <param name="audioName">Audio name.</param>
	/// <param name="volume">Volume.</param>
	/// <param name="name">Name.</param>
	public void BGMPlay(string audioName, float volume = 1f, string name = null)
	{
		if (audioName == null)
			return;
		BGMStop ();   																	//停止之前bgm的播放

		AudioClip clip = GetAudioClip (audioName);
		if (clip != null) {
			GameObject obj = new GameObject (name == null ? "LoopAudio" : name);
			obj.transform.parent = transform;
			AudioSource source = obj.AddComponent<AudioSource> ();
			source.pitch = 1f;
			source.clip = clip;
			source.volume = volume;
			source.loop = true;  														//循环播放
			source.Play ();
			bgmAudioSource = source;
		}
	}
	/// <summary>
	/// 停止背景音乐
	/// </summary>
	public void BGMStop()
	{
		if (bgmAudioSource && bgmAudioSource.gameObject) {
			Destroy (bgmAudioSource.gameObject);
			bgmAudioSource = null;
		}
	}
	/// <summary>
	/// 设置背景音乐音量
	/// </summary>
	/// <param name="volume">Volume.</param>
	public void BGMSetVolume(float volume)
	{
		if(bgmAudioSource)
			bgmAudioSource.volume = volume;
	}
	/// <summary>
	/// 暂停背景音乐
	/// </summary>
	public void BGMPause()
	{
		if (bgmAudioSource) 
			bgmAudioSource.Pause ();
	}
	/// <summary>
	/// 重新播放
	/// </summary>
	public void BGMReplay()
	{
		if (bgmAudioSource)
			bgmAudioSource.Play ();
	}
}

定义一个Dictionary<string, int>   来保存音效的名字与音效的数目。

获取音乐:从指定的路径中把音乐加载出来,返回的是AudioClip  

音效播放:首先判断一下音效名字是否为空,如果不为空,查看一下AudioDictionary有没有这个音效,如果没有就添加一个进去。如果这个音效的数目没有超过最大音效,就获得这个音效的音频剪辑,如果获得有效的音频剪辑,就调用播放音频剪辑的协程方法。

音频剪辑的协程方法:首先递增AudioDictionary中对应音效的数目,然后新建一个游戏对象,然后成为当前游戏对象的子对象,然后在此游戏对象上新建一个AudioSource组件, 然后设置组件的音量、音频剪辑、音调,然后调用Play方法播放。保存一下该组件。然后播放完毕后剪去对应音效的数目,如果音效的数目,最后销毁这个游戏对象。

所有音效暂停:寻找所有的AudioSource组件,只要不是BGM组件,就调用暂停方法。

背景音乐播放:首先获取音频剪辑,如果不为空的话,然后新建一个游戏对象,然后成为当前游戏对象的子对象,然后在此游戏对象上新建一个AudioSource组件, 然后设置组件的音量、音频剪辑、音调,然后调用Play方法播放。保存一下该组件。

停止背景音乐:如果背景音乐组件与其游戏对象存在,销毁游戏对象,音乐附件赋空。

子弹类:

子弹碰撞函数,如果碰到了标签为敌人的对象,则调用相应的方法。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyBullet : MonoBehaviour {

	//内置碰撞函数
	void OnCollisionEnter(Collision col)
	{
		//如果碰撞到了敌人为标签的对象
		if (col.transform.tag == "Enemy") {
			col.transform.GetComponent<MyEnemy> ().Damage ();
		}
		Destroy (gameObject);  									//销毁自身
	}

}

相机控制类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyCameraCtrl : MonoBehaviour {
	private GameObject Target = null; 									//跟随的目标物体
	public float smoothTime = 0.01f; 									//平滑时间,默认为0.01s
	public Vector3 TargetOffset = Vector3.zero;							//目标偏移量
	private Vector3 Velocicy = Vector3.zero;							//平滑初速度
	private Transform maskTrans = null;									//遮挡视线的物体

	// Use this for initialization
	void Start () {
		Target = GameObject.FindWithTag ("Player");
	}
	
	// Update is called once per frame
	void Update () {
		
	}

	void LateUpdate()
	{
		if (Target == null)
			return;
		Vector3 targetPos = Target.transform.position + TargetOffset;
		transform.position = Vector3.SmoothDamp (transform.position, targetPos, ref Velocicy, smoothTime); //平滑阻尼
		//射线
		Ray ray = new Ray (transform.position, transform.forward);
		RaycastHit hit; 																		//射线投射碰撞信息
		if (Physics.Raycast (ray, out hit, 6.0f)) { 											//射线检测,最远6f
			if (hit.transform.tag == "Mask") {
				if(maskTrans)
					maskTrans.GetComponentInChildren<MeshRenderer> ().enabled = true;  			//之前的物体恢复渲染
				maskTrans = hit.transform;
				maskTrans.GetComponentInChildren<MeshRenderer> ().enabled = false;				//不渲染这个物体
			} 
		}
	}

}

相机更新方法:首先计算目标位置,即目标位置加相机偏移量。然后调用平滑函数进行移动。

当我们的角色移动到障碍物后面时,我们希望仍然能够看到小人。我们可以当相机射线碰到障碍物时,停止渲染障碍物。我们把障碍物的标签设为Mask,表示为遮挡对象。定义一个射线,如果射线碰到了游戏对象,并且这个对象为遮挡对象,如果之前有过遮挡视线的物体,那么这个物体恢复渲染。并且保存一下这个物体。然后不渲染这个物体。

敌人类:

敌人有三种动画,我们可以通过不同的状态来播放不同的动画并作出相应的操作。当处于警戒范围或者攻击范围时怪物会追逐或者攻击玩家。

追逐玩家:首先判断玩家是否存在,如果存在的话,获取自身位置与玩家位置的二维数组的坐标,然后通过AStar脚本获得下一步要走的位置,然后判断一下在三维世界中的目标位置有没有设置过或者有没有到达这个目标位置,如果没有或者到达的目标位置,就计算一个新的三维世界目标位置,最后调用函数向目标位置移动即可。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyEnemy : MonoBehaviour {
	private Transform player;							//玩家的位置
	private int index;									//敌人的标记
	private float life;									//生命值
	public float warnRange; 							//警戒范围
	public float attackRange;							//攻击范围
	public float attackDate;							//攻击周期
	private float time = 0;								//记录上次攻击后经过的时间
	private State state = State.Idle;					//敌人的状态
	private Animator anim;								//动画状态机
	private Vector3 movePos = Vector3.zero;				//下一个要移动的位置

	private enum State
	{
		Idle,											//待在原地
		Chase, 											//追逐敌人
		Attack											//攻击
	}
		
	// Use this for initialization
	void Start () {
		anim = GetComponentInChildren<Animator> ();
	}
	
	// Update is called once per frame
	void Update () {
		switch (state) {
		case State.Idle:
			if (DistanceToPlayer < warnRange) {
				state = State.Chase;
				anim.SetBool ("Move", true);
			}
			break;
		case State.Chase:
			if (DistanceToPlayer > warnRange) {
				state = State.Idle;
				anim.SetBool ("Move", false);
			}
				
			if (DistanceToPlayer < attackRange) {
				state = State.Attack;
				anim.SetBool ("Move", false);
			}
			Chase ();
			transform.LookAt (player); 						//看向玩家
			break;
		case State.Attack:
			if (DistanceToPlayer > attackRange) {
				state = State.Chase;
				anim.SetBool ("Move", true);
			}
			if (time >= attackDate) {
				time = 0;
				Attack ();
			}
			time += Time.deltaTime;
			break;
		}
	}

	public void Init(int index)
	{
		life = 100 * (Random.Range (1, 5) + GlobeValue.Level);    				//设置生命值
		player = GameObject.FindGameObjectWithTag ("Player").transform; 		//获取玩家的位置
		this.index = index; 
	}
	//获取玩家与敌人的位置
	public float DistanceToPlayer
	{
		get {
			return Vector3.Distance(transform.position, player.position);
		}
	}
	//受伤
	public void Damage()
	{
		life -= 100;
		MyAudioController.Instance.SoundPlay ("ZomBear Hurt");   				//播放受伤音效
		if (life <= 0) {
			GameObject.FindObjectOfType<MyEnemySpawn>().Remove(index);
			Dead ();
		}
	}
	//死亡
	public void Dead()
	{
		UIManager.Instance.UpdateScoreText ();
		anim.SetTrigger("Dead");
		MyAudioController.Instance.SoundPlay ("ZomBear Death");
		gameObject.tag = "Untagged";
		Destroy (this, 0.01f);
		Destroy (gameObject, 3.0f);
	}
	//攻击
	private void Attack()
	{
		if(player.GetComponent<MyPlayer>())
			player.GetComponent<MyPlayer> ().Damage ();
	}
	//追逐玩家
	private void Chase()
	{
		int x = Mathf.RoundToInt((transform.position.x + 10) / 2.5f);
		int z = Mathf.RoundToInt((transform.position.z + 10) / 2.5f);

		IntegerVector2 start = new IntegerVector2 (x, z);                   //获取起点
		if (player.GetComponent<MyPlayer> () == null)
			return;
		IntegerVector2 end = player.GetComponent<MyPlayer> ().Position;		//获取终点
		MyPoint target = FindObjectOfType<MyAStar> ().Path (start, end);	//获取下一步要走的位置
		//如果没有设置过目标位置或者已经到达目标位置,并且找到了路径和target 获取目标位置 
		if ((movePos == Vector3.zero || Vector3.Distance (movePos, transform.position) < 0.01f) && target != null) {
			IntegerVector2 iv = new IntegerVector2(target.x, target.y);
			movePos = new Vector3 (iv.X, transform.position.y, iv.Z);
		}
			
		transform.position =  Vector3.MoveTowards (transform.position, movePos, Time.deltaTime);     //向目标位置移动
	}
}

敌人生成类:

用一个游戏对象数组表示多个敌人预制体。用一个字典保存剩余敌人。生成敌人的时候首先清空剩余的敌人,然后随即一个敌人数目的数字。然后产生一个随机的位置,判断一下这个位置有没有生成过敌人或者障碍物,没有的话,就生成并放到字典中。当敌人死亡,将其从字典中删除。每个敌人都有一个标记,可以通过标记快速找到敌人。

当攻击敌人的时候我们需要获取距离玩家最近的敌人。我们可以遍历所有敌人,通过与玩家的距离计算方法来寻找距离玩家最近的敌人。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyEnemySpawn : MonoBehaviour {
	public GameObject[] enemysPre;    													//敌人预制体
	private Dictionary<int, GameObject> enemyDict = new Dictionary<int, GameObject>();  //保存剩余敌人

	// Use this for initialization
	void Start () {
		
	}

	// Update is called once per frame
	void Update () {
		
	}
	//产生敌人
	public void Spawn(bool [,]place)
	{
		Clear (); 																			//首先清空剩余敌人
		int num = Random.Range (3, 7);  													//随机敌人数量
		for (int i = 0; i < num; ++i) {
			int count = 10;
			while (count-- > 0) {
				//产生随机的位置
				IntegerVector2 pos = new IntegerVector2 (Random.Range (1, MyMap.LENGTH - 1),
					Random.Range (1, MyMap.LENGTH - 1));
				if (!place [pos.x, pos.z]) {
					place [pos.x, pos.z] = true;
					GameObject obj = Instantiate (enemysPre [Random.Range (0, enemysPre.Length)], 
						                 new Vector3 (pos.X, 0, pos.Z), Quaternion.identity);
					obj.GetComponent<MyEnemy> ().Init (pos.Index);
					enemyDict.Add (pos.Index, obj);
					break;
				}
			}
		}
	}
	//清空所有敌人
	private void Clear()
	{
		foreach (GameObject e in enemyDict.Values)
			Destroy (e);
		enemyDict.Clear ();
	}
	//清空标记为index的敌人
	public void Remove(int index)
	{
		GameObject e = null;
		enemyDict.TryGetValue (index, out e);
		enemyDict.Remove (index);
		//敌人数目为0, 触发胜利
		if (enemyDict.Count == 0)
			FindObjectOfType<MyMap> ().Victory ();
	}
	//获取距离玩家最近的敌人
	public Transform Target
	{
		get{
			float dis = float.MaxValue;
			Transform target = null;
			foreach (GameObject obj in enemyDict.Values)
				if (dis > obj.GetComponent<MyEnemy> ().DistanceToPlayer) {
					dis = obj.GetComponent<MyEnemy> ().DistanceToPlayer;
					target = obj.transform;
				}
			return target;
		}
	}

}

地图类:

首先是一个结构体,表示自己定义的二维坐标。

生成地图:首先清空之前生成的障碍物。生成地图跟生成怪物的算法基本一样。不同的是之后要做一次检测,检测有没有封闭的空间。这里只是简单的检测,有的情况并没有办法解决,比如矩形的封闭空间。检测方法:直接检测一个空地的上下左右四个位置,如果都有障碍物说明是一个封闭空间。就把上面的一个障碍物销毁就可以了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IntegerVector2 											//工具类,用于记录位置坐标
{
	public int x;						
	public int z;
	public float X {get{ return x * 2.5f - 10;}}						//获取场景中X坐标
	public float Z {get{ return z * 2.5f - 10;}}						//获取场景中Z坐标
	public IntegerVector2(int x, int z)
	{
		this.x = x;
		this.z = z;
	}
	public int Index{get { return x * MyMap.LENGTH + z;}} 				//获取标记
}

public class MyMap : MonoBehaviour {
	public GameObject[] models;  										//模型预制体
	public const int LENGTH = 9;  										//地图长宽
	private bool[,] place;       										//占位,当前位置是否有物体,x=i * 2.5f - 10
	public bool[,] Place {get{ return place;}} 							//对外开放占位符
	private Dictionary<int, GameObject> modelDict = new Dictionary<int, GameObject>(); //字典集,将物体与位置关联起来
	private MyEnemySpawn enemySpawn;  									//获取敌人孵化器
	public Door door; 													//下一关的传送门模型
	public Light mainLight; 											//直射光

	// Use this for initialization
	void Start () {
		MyAudioController.Instance.BGMPlay ("Background Music", 0.5f);
		enemySpawn = FindObjectOfType<MyEnemySpawn> (); 
		InitMap ();
	}
	
	// Update is called once per frame
	void Update () {
		if (Input.GetKeyDown (KeyCode.Escape)) 
			Application.Quit (); 										//退出游戏
	}
	//地图的生成
	public void InitMap()
	{
		place = new bool[LENGTH, LENGTH];
		foreach(GameObject obj in modelDict.Values)
			Destroy(obj);
		modelDict.Clear ();

		int num = Random.Range (10, 20);
		for(int i = 0; i < num; ++i)
		{
			int count = 10;
			while(count-- > 0)
			{
				IntegerVector2 pos = new IntegerVector2 (
					                     Random.Range (1, LENGTH - 1),
					                     Random.Range (1, LENGTH - 1));
				if(!place[pos.x, pos.z])
				{
					place [pos.x, pos.z] = true;
					GameObject obj = Instantiate (
						                 models [Random.Range (0, models.Length)],
						                 new Vector3 (pos.X, 0.0f, pos.Z),
						                 Quaternion.identity);
					modelDict.Add (pos.Index, obj);
					break;
				}
			}
		}
		Check ();
		FindObjectOfType<MyAStar> ().InitMap (place);
		enemySpawn.Spawn (place);
		//产生随机颜色的直射光
		mainLight.color = new Color (Random.Range (0.2f, 0.8f), Random.Range (0.2f, 0.8f), Random.Range (0.2f, 0.8f));
	}
	//将没有出口的房间打破
	private void Check()
	{
		for (int i = 1; i < LENGTH-1; ++i)
			for (int j = 1; j < LENGTH-1; ++j)
				if (!place [i, j])
					CheckRoom (new IntegerVector2(i, j));
	}
	//检查是不是上下左右都是墙,如果是,选择上面的墙销毁
	private void CheckRoom(IntegerVector2 pos)
	{
		if (place [pos.x - 1, pos.z] &&
		   place [pos.x, pos.z-1] &&
		   place [pos.x + 1, pos.z] &&
		   place [pos.x, pos.z+1]) 
		{
			++pos.x;	
			place [pos.x, pos.z] = false;
			GameObject obj = null;
			modelDict.TryGetValue (pos.Index, out obj);
			Destroy (obj);
			modelDict.Remove (pos.Index);
		}
	}
	//胜利
	public void Victory()
	{
		//出现传送门
		Instantiate (door);
	}
}

玩家类:

移动:首先判断是否为攻击状态,不是的话改变人物转向为移动的方向。然后设定速度为移动向量乘速度即可,然后设定动画。

射击处理:设置攻击状态为true,然后获取最近的目标位置。如果目标位置存在的话,调整玩家朝向为敌人的方向。然后实例化子弹,给子弹施加力,攻击间隔为1s,攻击完毕后设置攻击状态为false。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyPlayer : MonoBehaviour {
	public float moveSpeed; 												//移动速度
	private Rigidbody rb;													//刚体组件
	private Animator anim;	
	private bool isAttack = false;											//是否正在攻击
	public GameObject bullet;          										//子弹
	public Transform shotOrigin;    										//枪口位置
	public float shotForce;													//子弹的受力
	public float maxLife;  													//最大生命值
	private float life; 													//生命值
	public GameObject shotLight; 											//枪口火焰
	// Use this for initialization
	void Start () {
		rb = GetComponent<Rigidbody> ();
		life = maxLife;
		anim = GetComponentInChildren<Animator> ();
		UIManager.Instance.UpdateHPSlider (life, maxLife);
	}
	
	// Update is called once per frame
	void Update () {
		//按下开火键
		if (ETCInput.GetButtonDown ("FireButton"))
			StartCoroutine (Shot ());
	}
	//移动操作
	public void MoveStart(Vector2 direction)
	{
		//改变人物朝向
		if (!isAttack)
			transform.rotation = Quaternion.LookRotation (new Vector3(direction.x, 0, direction.y));
		Vector3 dir = new Vector3 (direction.x, 0, direction.y).normalized;
		rb.velocity = dir * moveSpeed;
		//设置人物动画
		anim.SetBool ("Move", true);
	}
	//结束移动
	public void MoveEnd()
	{
		rb.velocity = Vector3.zero;
		anim.SetBool ("Move", false);
	}
	//射击处理
	private IEnumerator Shot()
	{
		isAttack = true;
		Transform target = FindObjectOfType<MyEnemySpawn>().Target;   												//获取最近目标位置
		if (target) {
			GameObject obj = new GameObject ();
			obj.transform.position = new Vector3 (target.position.x, transform.position.y, target.position.z);
			transform.LookAt (obj.transform);    //调整人物朝向
			Destroy (obj);
			GameObject go = Instantiate (bullet, shotOrigin.position, Quaternion.identity);							//实例化子弹
			Instantiate (shotLight, shotOrigin.position, Quaternion.identity); 										
			MyAudioController.Instance.SoundPlay ("Player GunShot");   
			go.GetComponent<Rigidbody> ().AddForce (shotForce * (target.position - shotOrigin.position).normalized); //给子弹施加力
			yield return new WaitForSeconds (1.0f);
		}
			
		isAttack = false;
	}
	//获取人物在数组中的位置
	public IntegerVector2 Position
	{
		get {
			int x = Mathf.RoundToInt((transform.position.x + 10) / 2.5f);
			int z = Mathf.RoundToInt((transform.position.z + 10) / 2.5f);
			return new IntegerVector2 (x, z);
		}
	}
	//受伤
	public void Damage()
	{
		life -= 100;
		MyAudioController.Instance.SoundPlay ("Player Hurt");
		UIManager.Instance.UpdateHPSlider (life, maxLife);  										//更新血条
		if (life <= 0)
			Dead ();
	}
	//死亡
	private void Dead()
	{
		anim.SetTrigger ("Dead");
		MyAudioController.Instance.SoundPlay ("Player Death");
		UIManager.Instance.ShowEndPanel ();      													//绘制结束画面
		Destroy (this);
	}
		
}

AStar算法用到的地图结点:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//AStar 算法地图结点
public class MyPoint{
	public int x;
	public int y;
	public float F;
	public float G;
	public float H;
	public bool isWall; 													//是否是墙壁
	public MyPoint parent; 													//父结点

	public MyPoint(int x, int y)
	{
		this.x = x;
		this.y = y;
		this.isWall = false;
		this.parent = null; 
		this.F = this.G = this.H = 0.0f;
	}
	//更新F,G和parent的值
	public void UpdatePoint(MyPoint parent, float g)
	{
		this.parent = parent;
		G = g;
		F = G + H;
	}
}

开始场景加载:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class StartScene : MonoBehaviour {
	public void LoadScene()
	{
		//加载场景
		SceneManager.LoadScene ("02");
		//重置全局变量
		GlobeValue.Score = 0;
		GlobeValue.Level = 0;
	}
}

UI管理:

幕布加载:curtain.alpha表示幕布的透明度,curtain.blocksRaycasts是否遮挡射线的,如果true就不许用户进行操作,因为我们在幕布加载的时候是不允许用户操作的。实现思路就是随时间改变幕布的透明度,2s后加载结束。恢复原来的状态。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class UIManager : MonoBehaviour {
	public static UIManager Instance = null;        //对象实例
	public CanvasGroup curtain;						//幕布
	private bool isLoad = false;					//是否加载幕布
	private float time = 0f;						//记录记载幕布的时间
	public Slider HPSlider;							//血条
	public CanvasGroup endPanel;  					//结束画面
	public Text scoreText;							//分数文本
	public Text scoreTextED;						//结束分数文本

	void Awake()
	{
		Instance = this;
	}

	// Use this for initialization
	void Start () {
		Init ();
	}

	public void Init()
	{
		StartCoroutine ("ShowPanel");
	}

	// Update is called once per frame
	void Update () {
		if (isLoad) {
			time += Time.deltaTime;
			curtain.alpha = (2 - time) / 2.0f;			//改变幕布透明度
			if (time > 2) {
				time = 0;
				isLoad = false;
			}
		}
	}
	//幕布加载
	private IEnumerator ShowPanel()
	{
		curtain.alpha = 1;
		curtain.blocksRaycasts = true;
		isLoad = true;
		yield return new WaitForSeconds (2f);
		curtain.alpha = 0;
		curtain.blocksRaycasts = false;
	}
	//更新血条
	public void UpdateHPSlider(float currentHp, float maxHp)
	{
		HPSlider.value = currentHp / maxHp;
	}
	//绘制结束画面
	public void ShowEndPanel()
	{
		endPanel.alpha = 1;
		endPanel.blocksRaycasts = true;
		scoreTextED.text = "Score:" + GlobeValue.Score;
	}
	//back按钮:加载开始场景
	public void OnBackButtonClick()
	{
		SceneManager.LoadScene ("00");
	}
	//更新分数文本
	public void UpdateScoreText()
	{
		GlobeValue.Score += GlobeValue.Level + 1;
		scoreText.text = "Score:" + GlobeValue.Score;
	}
}