上一次的学习我们完成了敌人的AI编写,但是有一个问题就是敌人会跑到地图外,所以我们这次需要开始初始化地图了。
21、初始化地图工具的创建
我们新建一个游戏物体,命名为MapCreation,用来创建地图,并挂在一个MapCreation的脚本用来初始化地图。
//0.老家 1.墙 2.障碍 3.出生效果 4.河流 5.草 6.空气墙
public GameObject[] item;//初始地图所需物体的数组
private void Awake()//对地图进行实例化
{
//实例化老家
CreateItem(item[0], new Vector3(0, -8, 0), Quaternion.identity);
CreateItem(item[1], new Vector3(-1, -8, 0), Quaternion.identity);//老家旁边的保护墙
CreateItem(item[1], new Vector3(1, -8, 0), Quaternion.identity);//老家旁边的保护墙
CreateItem(item[1], new Vector3(-1, -7, 0), Quaternion.identity);//老家旁边的保护墙
CreateItem(item[1], new Vector3(0, -7, 0), Quaternion.identity);//老家旁边的保护墙
CreateItem(item[1], new Vector3(1, -7, 0), Quaternion.identity);//老家旁边的保护墙
}
private void CreateItem(GameObject obj,Vector3 pos,Quaternion qua)//创建物体到Map里
{
GameObject createObj = Instantiate(obj, pos, qua);//实例化游戏物体
createObj.transform.SetParent(gameObject.transform);//设置父物体
}
我们给MapCreation脚本挂载上他需要的预制体,然后在地图中直接创建老家,下面是效果:
22、编写产生随机位置的方法
接下来我们就需要创建一些其他的物体,但是因为这些物体位置信息每一局都不能一样,所以我们需要让这些物体产生随机位置。
public List<Vector3> itemPosList=new List<Vector3>();//用于存放已经生成的位置
private Vector3 CreatePosRandomly()//产生随机位置
{
//约束最外围的边界不产生物体,确保可以通行(x=-10,10,y=-8,8)
while(true)
{
Vector3 tempPos = new Vector3(Random.Range(-9,10),Random.Range(-7,8),0);//产生一个随即位置
for (int i = 0; i < itemPosList.Count; i++) //判断列表中是否存在该位置
{
if (tempPos == itemPosList[i])
continue;
}
return tempPos;
}
}
然后我们实例化一下外层的空气墙,在Awake函数里面实现:
//实例化边界的空气墙
for (int i = -11; i < 12; i++)
{
CreateItem(item[6], new Vector3(i, 9, 0), Quaternion.identity);//上面的围墙
CreateItem(item[6], new Vector3(i, -9, 0), Quaternion.identity);//下面的围墙
}
for(int i = -8; i < 9; i++)
{
CreateItem(item[6], new Vector3(-11, i, 0), Quaternion.identity);//左边的围墙
CreateItem(item[6], new Vector3(11, i, 0), Quaternion.identity);//右边的围墙
}
23、初始化地图其他的游戏物体
我们现在初始化一下游戏的其他物体,这里我们先初始化草、水、墙和障碍物
//实例化地图其他物体
for (int i = 0; i < 20; i++)//每个种类产生20个
{
CreateItem(item[1], CreatePosRandomly(), Quaternion.identity);//实例化墙
CreateItem(item[1], CreatePosRandomly(), Quaternion.identity);//实例化墙
CreateItem(item[1], CreatePosRandomly(), Quaternion.identity);//实例化墙
CreateItem(item[2], CreatePosRandomly(), Quaternion.identity);//实例化障碍
CreateItem(item[4], CreatePosRandomly(), Quaternion.identity);//实例化河流
CreateItem(item[5], CreatePosRandomly(), Quaternion.identity);//实例化草
}
然后我们添加一下玩家和敌人,这些都是Awake函数中完成
//实例化玩家
GameObject player = Instantiate(item[3], new Vector3(-2, -8, 0), Quaternion.identity);
itemPosList.Add(new Vector3(-2, -8, 0));//将玩家的位置添加到列表中
player.GetComponent<Born>().createPlayer = true;
//实例化敌人
CreateItem(item[3], new Vector3(-10, 8, 0), Quaternion.identity);
CreateItem(item[3], new Vector3(0, 8, 0), Quaternion.identity);
CreateItem(item[3], new Vector3(10, 8, 0), Quaternion.identity);
InvokeRepeating("CreateEnemy", 4f, 5f);//延迟调用,第一次4s后随机产生敌人,随后每隔5s产生敌人
其中调用到了随机产生敌人的函数CreateEnemy()
private void CreateEnemy()//产生敌人
{
int num = Random.Range(0, 3);
Vector3 enemyPos=new Vector3();
if (num == 0)
enemyPos = new Vector3(-10, 8, 0);
else if(num==1)
enemyPos = new Vector3(0, 8, 0);
else if (num == 2)
enemyPos = new Vector3(10, 8, 0);
CreateItem(item[3], enemyPos, Quaternion.identity);//在随机位置产生敌人
}
我们看一下游戏运行的效果:
24、敌人AI的优化
我们发现当敌人过多的时候,有可能会扎堆,为了解决这个问题,我们让两个敌人碰到一起的时候立即改变方向:
private void OnCollisionEnter2D(Collision2D collision)//碰撞检测
{
if (collision.gameObject.tag == "Enemy")//如果两个敌人碰到一起就让他们方向发生改变,不用挤到一起
DirChangeTime = 4;
}
25、玩家状态的管理
现在我们需要对玩家的状态进行管理,包括玩家的生命,玩家消灭的敌人等信息,我们新建一个空物体,命名为PlayerManage,挂载上同名的脚本PlayerManage
//属性
public int playerLife = 3;//玩家的生命值
public int playerScore = 0;//玩家的得分
public bool isDead;//玩家是否死亡
//引用
public GameObject Born;//重生的特效
//单例
private static PlayerManage instance;
public static PlayerManage Instance
{
get => instance;
set => instance = value;
}
private void Awake()
{
Instance = this;
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (isDead)
Reborn();//如果死亡,进入重生函数
}
private void Reborn()
{
if (playerLife == 0)
;
else
{
playerLife--;//生命值减1
GameObject palyer = Instantiate(Born, new Vector3(-2, -8, 0), Quaternion.identity);
palyer.GetComponent<Born>().createPlayer = true;//设置为重生玩家
isDead = false;
}
}
然后我们在PlayerAI里面坦克死亡时把IsDead的值改为true,在EnemyAI敌人死亡时把分数递增:
//PlayerAI
private void TankDie()//坦克的死亡方法
{
if (isDefended)//如果玩家无敌则不会死亡
return;
PlayerManage.Instance.isDead = true;//设置玩家状态为死亡
//产生爆炸特效
Instantiate(explosionPrefab, transform.position, transform.rotation);
//死亡
Destroy(gameObject);
}
//EnemyAI
private void TankDie()//坦克的死亡方法
{
PlayerManage.Instance.playerScore++;//消灭一个敌人加一分
//产生爆炸特效
Instantiate(explosionPrefab, transform.position, transform.rotation);
//死亡
Destroy(gameObject);
}
下面是效果:
26、UI的制作
游戏的大部分功能都制作完成了,现在我们来设计UI显示玩家的状态与得分,我们先设置游戏屏幕比例为16:10,这样就可以在旁边留白部分进行UI的显示,设置一下整个UI的背景色为灰色,然后拖入一个纯黑的背景作为游戏的背景,设置渲染优先级为-2,这样就不会遮挡游戏物体。
我们先新建两个图片,用来表示坦克的生命和得分,然后分别加文本框用来显示得分数和生命值:
我们在代码中对这两个值进行实时的修改,接着我们添加一下游戏结束的处理程序,当老家死亡或者生命值为0时会显示游戏结束的界面,我们在PlayerManage脚本的Update里面添加处理程序,把结束UI的界面传到脚本的公有变量上:
public GameObject gameOver;//游戏结束界面
void Update()
{
if (isDead)
Reborn();//如果死亡,进入重生函数
if (isDefeat)
{
gameOver.SetActive(true);//设置游戏结束界面
return;
}
//实时更新UI
scoreText.text = "Score:"+playerScore.ToString();
lifeText.text = "Life:" + playerLife.ToString();
}
下面是效果:
27、第一个场景的制作
游戏的场景基本制作完了,接下来我们要制作游戏刚打开时的场景,我们ctrl+N新建一个场景,命名为Begin保存到当前工程下。我们在新的场景下添加一个图片,设置图片源为资源文件里面的title图片,设置好适合的大小。
然后我们新建一个图片,用来设置选择的指针,我们把坦克的向右的图片作为他的渲染图片,设置好对应的位置和大小:
然后我们新建两个空物体,把指针的位置和大小信息给这两个空物体,然后将第二个空物体向下移动到第二个选择,这样我们就设置好了两个位置信息,到时候选择的时候直接访问这两个位置信息进行移动就可以了。我们新建一个脚本挂载到选择指针上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ChooseOptions : MonoBehaviour
{
//属性
private int choose = 1;//记录玩家的选择
//引用
public GameObject posOne;//选择1的位置
public GameObject posTwo;//选择2的位置
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.UpArrow))
{
choose = 1;
transform.position = posOne.transform.position;//将选择指针移动到选择1
}
else if(Input.GetKeyDown(KeyCode.DownArrow))
{
choose = 2;
transform.position = posTwo.transform.position;//将选择指针移动到选择1
}
if(choose==1&&Input.GetKeyDown(KeyCode.Space))//如果玩家选择了1并且按下了空格确认
{
SceneManager.LoadScene("Main");
}
}
}
为了能够空格选择游戏模式顺利加载到我们的游戏主场景,我们需要在Build Settings里面拖入我们的两个场景,第一个是Begin,第二个是Main:
然后下面是效果:
28、音效的添加
接下来我们简单添加一下游戏的音效功能,我们先给老家死亡添加音效,在HeartAI中添加音效的引用,然后在死亡时播放:
public AudioClip heartdieVoice;//老家死亡音效
public void HeartDie()//老家中枪,游戏结束
{
Instantiate(explosionPrefab, transform.position, transform.rotation);//产生爆炸特效
spriteRenderer.sprite = HeartBroken;//更换爆炸后的图片
PlayerManage.Instance.isDefeat = true;//游戏结束
AudioSource.PlayClipAtPoint(heartdieVoice,transform.position);//播放死亡音效
}
把资源里面的Die音效托给heartdieVoice就可以了,接着我们添加爆炸音效,我们直接给爆炸动画添加一个Audio Source组件,选择Play On Awake为勾选状态,这样一初始化爆炸特效就会播放音效。不过记得要给Audio Clip设置音效的来源。
然后我们添加一下子弹打到障碍的一个音效,我们给障碍预制体添加一个脚本:
//引用
public AudioClip hitVoice;//子弹打到障碍物的声音特效
public void PlayAudio()//播放音效
{
AudioSource.PlayClipAtPoint(hitVoice, transform.position);
}
然后在子弹碰到障碍时调用这个函数就可以了
//仅作事例
switch(collision.tag)//根据碰撞物体的标签来响应事件
{
case "Barrier":
collision.SendMessage("PlayAudio");//播放击打音效
Destroy(gameObject);//销毁子弹
}
接着我们添加游戏开始音效和子弹发射的音效,设置组件与声音来源,勾选在Awake函数里面直接调用。
最后就是坦克移动的音效,我们给PlayerAI脚本新建两个变量,一个是Audio的组件,一个是音效的资源,分别拖动赋值:
然后我们根据坦克是否处于移动状态来播放不同的音效。