下方将持续更新简单的塔防游戏:
****************************************************************************************************************************************
2018.11.25
1> 地图的构建 (使用Cube平铺成一个地图)
2> 起点与终点 (敌人产生的位置和我方基地的位置)
3> 敌人移动路线的设置
4> 敌人自动移动 (标记点+Scripts)
一:效果展示
二:关键部分 -> 敌人自动移动 (标记点+Scripts)
标记点的设置:
在每一个弯道处设置一个EmptyObject来进行标记,通过在脚本中构建方向向量和使用Transform组件来直接进行敌人的移动
(具体见脚本内容)
三:Scripts
1> WayPoints.cs(将该脚本拖到WayPoint整体组件上):构建static标记点数组供另一个脚本调用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WayPoints : MonoBehaviour
{
public static Transform[] positions; //使用一个Transform类来存储所有的标记点
void Awake()
{
positions = new Transform[transform.childCount]; //定义数组大小,positions为数组首地址
for (int i = 0; i < positions.Length; i++)
{
positions[i] = transform.GetChild(i); //将标记点逐个赋值给标记点数组
}
}
}
2> Enemy.cs (将该脚本拖到敌人上):控制敌人的自动移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour {
private Transform[] positions; //新建一个Transform数组用来存储上一个Scripts中定义的标记点数组
private int index = 0;
public float Enemy_Speed = 20;
// Use this for initialization
void Start () {
positions = WayPoints.positions; //获取标记点数组(上一个标记点十足是静态变量)
}
// Update is called once per frame
void Update () {
Move();
}
void Move()
{
if (index > positions.Length - 1) return; //防止数组下标越界
transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * Enemy_Speed); //获取方向向量进行移动(Translate类自动进行)
if (Vector3.Distance(positions[index].position, transform.position) < 0.2f) //转向判断
{
index++;
}
}
}
3> ViewController.cs (将脚本拖到Camera上):Camera视角的变换 (使用Space.Global世界坐标系)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ViewController : MonoBehaviour {
public float wsad_Speed = 80;
public float mouse_Speed = 500;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float mouse = Input.GetAxis("Mouse ScrollWheel");
//transform.Translate(new Vector3(h, 0, v) * Time.deltaTime * wsad_Speed); //以local坐标系
transform.Translate(new Vector3(-v * wsad_Speed, mouse * mouse_Speed, h * wsad_Speed) * Time.deltaTime, Space.World);
}
}
4> 项目文件及布局
****************************************************************************************************************************************
2018.11.26(Morning)
1> Enemy.cs (改) -> 相较上面的Scripts简单改动
增加功能:使小球 (Enemy)能够循环出现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour {
private Transform[] positions; //新建一个Transform数组用来存储上一个Scripts中定义的标记点数组
private int index = 0;
public float Enemy_Speed = 20;
// Use this for initialization
void Start () {
positions = WayPoints.positions; //获取标记点数组(上一个标记点十足是静态变量)
}
// Update is called once per frame
void Update () {
Move();
}
void Move()
{
if (index > positions.Length - 1) //防止数组下标越界
{
index = 0;
transform.position = new Vector3(2.5f,2f,2f); //回到起点再次循环,因为标记的路径点中没有起始点,这里指定坐标跳转
}
transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * Enemy_Speed); //获取方向向量进行移动(Translate类自动进行)
if (Vector3.Distance(positions[index].position, transform.position) < 0.2f) //转向判断
{
index++;
}
}
}
2> 在蛮牛上偶遇一款Riko模型 (自带骨骼动画),
于是无情的将上面已完成部分作为背景,主体控制Riko进行个方向的移动漫游
传送门:---点我即达---
本文仍然继续更新~
****************************************************************************************************************************************
2018.11.26(Night)
上面已经可以产生一个敌人,并且敌人能够按照指定的路径移动,下一步应该考虑一下功能:
1> 设置三种类型的敌人
2> 敌人可以分三波产生,每一波的敌人的类型、速度和数量都不同
3> 每一波敌人产生之后休息一定时间间隔再产生下一波敌人
1> 设置三种类型的敌人
2> 敌人可以分三波产生,每一波的敌人的类型、速度和数量都不同
首先创建一个脚本来存储每一波的敌人信息,将每一波敌人的信息存进结构体数组,方便直接通过 . 来调用:
Wave.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//存储每一波敌人产生所需要的信息
[System.Serializable] //可序列化,声明这一条语句才可以将数组显示在Unity的Inspector栏中
public class Wave{
public GameObject EnemyPrefab; //需要产生的Enemy游戏物体
public float count; //这一波敌人的个数
public float rate; //这一波敌人的移动速度
}
接下来产生一波一波的敌人,需要创建一个EnemySpawner (敌人孵化器)脚本;
首先创建一个目录:EnemyManger,
然后再新建一个Enemy_Spawner.cs脚本 (脚本在图下方);
数组声明后就可以在Inspector栏中直接设置Wave.cs中的每一波敌人的各个属性;
Enemy_Spawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour {
public Wave[] waves; //定义一个数组来存储一共有多少波敌人,每一波敌人的属性都在Wave中
public Transform START; //定义开始位置
public float Wave_Rate = 3; //定义每一波的时间间隔为3s
void Start()
{
StartCoroutine(SpawnEnemy()); //携程 (Coroutine)的启动方法
}
//使用携程
//携程的简单介绍:全程协同程序,指一个协同程序在执行过程中,可以在任意位置使用yield语句,yoeld语句的返回值控制着核实回复协同程序向下执行
IEnumerator SpawnEnemy() //IEnumerator是C#的一个简单而强大的接口(numerator分子)
{
foreach (Wave wave in waves)
{
for(int i=0;i<wave.count;i++)
{
GameObject.Instantiate(wave.EnemyPrefab, START.position, Quaternion.identity); //Enemy实例化,第三个参数表示无旋转的四元数
//暂停
if(i!=wave.count-1) //这里采用一个条件语句来防止最后一个敌人出现后仍暂停上一个rate
yield return new WaitForSeconds(wave.rate);
}
//每出现一波敌人后暂停3s
yield return new WaitForSeconds(Wave_Rate);
}
}
}
****************************************************************************************************************************************
2018.11.27(Morning)
上面敌人已经可以在指定间隔后产生一波又一波,但是如果想要在前一波敌人到达目的地的时候再使得下一波敌人产生,
该怎么做呢?
1> 在前一波敌人到达终点的时候,产生下一波敌人
思路:
1> 因为敌人是在EnemySpawner中孵化出来的,这里就可以对敌人进行计数,当产生一个敌人,计数器++;
2> 当Enemy到达终点的时候,在Enemy.cs中间接调用计数器,销毁 (Destory) 该物体的同时,使计数器的值--;
3> 在EnemySpawner.cs中对Enemy数量进行判断,当数量一直>0(敌人没有完全达到终点)就直接暂停;
4> 这里的暂停 (yield return null),会使得后面的语句不执行,继续执行前面的语句,当满足条件的时候再执行暂停后的语句,
这里就是Unity中携程的一个功能体现;
Scripts:
1> EnemySpawner.cs(改)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour {
public static int CountAliveEnemy = 0; //存活敌人的数量
public Wave[] waves; //定义一个数组来存储一共有多少波敌人,每一波敌人的属性都在Wave中
public Transform START; //定义开始位置
public float Wave_Rate = 0.5F; //定义每一波的时间间隔为3s
void Start()
{
StartCoroutine(SpawnEnemy()); //携程 (Coroutine)的启动方法
}
//使用携程
//携程的简单介绍:全程协同程序,指一个协同程序在执行过程中,可以在任意位置使用yield语句,yoeld语句的返回值控制着核实回复协同程序向下执行
IEnumerator SpawnEnemy() //IEnumerator是C#的一个简单而强大的接口(numerator分子)
{
foreach (Wave wave in waves)
{
for(int i=0;i<wave.count;i++)
{
GameObject.Instantiate(wave.EnemyPrefab, START.position, Quaternion.identity); //第三个参数表示无旋转的四元数
//暂停
CountAliveEnemy++;
if (i!=wave.count-1) //这里采用一个条件语句来防止最后一个敌人出现后仍暂停上一个rate
yield return new WaitForSeconds(wave.rate);
}
//每出现一波敌人后暂停3s
while(CountAliveEnemy>0)
{
yield return null; //暂停0帧
}
yield return new WaitForSeconds(Wave_Rate);
}
}
}
2> Enemy.cs(改)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour {
private Transform[] positions; //新建一个Transform数组用来存储上一个Scripts中定义的标记点数组
private int index = 0;
public float Enemy_Speed = 20;
// Use this for initialization
void Start () {
positions = WayPoints.positions; //获取标记点数组(上一个标记点十足是静态变量)
}
// Update is called once per frame
void Update () {
Move();
}
void Move()
{
//if (index > positions.Length - 1) //防止数组下标越界
//{
// index = 0;
// transform.position = new Vector3(2.5f,2f,2f); //回到起点再次循环,因为标记的路径点中没有起始点,这里指定坐标跳转
//}
//if (index > positions.Length - 1) //防止数组下标越界(不循环,直接停止就好)
// return;
transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * Enemy_Speed); //获取方向向量进行移动(Translate类自动进行)
if (Vector3.Distance(positions[index].position, transform.position) < 1f) //转向判断
{
index++;
}
Debug.Log(index);
if(index > positions.Length-1)
{
ReachDestination();
}
}
void ReachDestination() //当物体Enemy到达终点,调用该函数
{
GameObject.Destroy(this.gameObject);
EnemySpawner.CountAliveEnemy--;
}
void OnDestory() //一旦该物体被炮塔销毁,调用该函数
{
EnemySpawner.CountAliveEnemy--;
}
}
****************************************************************************************************************************************
2018.11.27(Night)
上面敌人已经能够正常产生与移动了,接下来开始实现选择炮塔的UI界面制作
1> 选择炮塔的UI界面
2> 点击单选炮塔 (Turret),被选择的炮塔阴影显示
1> UI界面的设置首先需要可以放在界面上的炮塔UI图片
资源地址:https://download.csdn.net/download/qq_42292831/10810278
因为上传资源必须要添加需要分数,这个资源就设置为1分,本资源基于SiKi学院资源进行二次打包(添加了相应的三个炮塔和升级后炮塔的Prefabs),分数紧张的话可以去SiKi学院寻找下载原始资源。
操作步骤:
1) 将Scene窗口切换为2D视角
2) 在Hierarchy中创建Canvas (右键->UI),然后在Canvas下创建三个Toggle (开关)分别用来存储三个炮塔的选择开关
3) 对Toggle进行属性调整 (增加炮塔图标+设置大小)
* 注意这里有时候会遇到在2DScene视角显示不出来图标,但是在Game视图中可以显示的问题,需要进行一下操作:
(改为Default,确认子目录属性一并更改即可)
4) 添加点击时候的阴影 (Background中的Checkmark)
5) 使用ToggleGroup将三个Toggle包含起来 (ToggleGroup需要自行创建新物体并改名添加脚本):
注意在Scene界面大小要将三个Toggle包含,在目录中也要设置为父子关系;
6) 启动游戏时隐形是否显示 (勾选为显示,取消为默认不显示)
7) 如果上面一切正常到这一步的话一定会遇到一个问题,那就是游戏运行时点击图标没有阴影产生的效果:
这就需要在Canvas中额外添加一个物件:EventSystem (右键UI中可以找到)
8) 这个时候点击会产生多选的效果,但是这么实现单选呢?这就体现到ToggleGroup的作用了:
9) 正确操作会得到如下效果 (运行的时候点击会单选还会出现阴影)
10) 本功能到此已经结束了。
****************************************************************************************************************************************
2018.11.28 (Morning)
1> 炮塔信息的存储类 (TurretData.cs)
2> 炮塔管理器 (调用炮塔信息的存储类有利于监听类SelectedTurretData的赋值)与点击炮塔事件的监听
TurretData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class TurretData {
public GameObject turretPrefab;
public int cost;
public GameObject turretUpgradedPreefab;
public int cost_upgraded;
public TurretType type;
}
public enum TurretType
{
LaserTurret,
MissileTurret,
StdandardTurret
}
BuildManager.cs (对开关Toggle事件进行监听)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BuildManager : MonoBehaviour {
public TurretData LaserTurretData;
public TurretData MissileTurretData;
public TurretData StdandardTurretData;
//当前选择的炮塔信息
public TurretData SelectedTurretData;
public void OnLaserChanged(bool isOn) //监听
{
if(isOn)
{
SelectedTurretData = LaserTurretData;
}
}
public void OnMissileChanged(bool isOn)
{
if (isOn)
{
SelectedTurretData = MissileTurretData;
}
}
public void OnStdandardChanged(bool isOn)
{
if (isOn)
{
SelectedTurretData = StdandardTurretData;
}
}
}
<正常效果:游戏运行时每次点击不同的Turret,SelectedTurretData信息都会动态改变>