太空避障:主要是实现飞机躲避子弹
面板基类、音乐类、排行榜类、json等等都和上一篇Unity坦克迷宫Demo总结一样,太空避障主要是对四元数的练习和使用
1.选择飞机面板
(1)通过左右按钮对显示的模型进行切换
(2)通过点击鼠标左键可以实现对飞机的拖动 观察飞机的整体
知识点
(1)四元数 * 四元数:会得到一个新的四元数,大多数用于旋转量相乘,相当于旋转(这个旋转的坐标是物体自身的坐标系)
(2)四元数 * 向量:会得到一个新的向量,对一个向量进行一定旋转,相当于旋转向量
public class SelectPanel : BasePanel<SelectPanel>
{
public Button btnRight;
public Button btnLeft;
public Button btnStart;
public Button btnClose;
//标识当前已经创建的飞机 当切换时要删除该飞机
private GameObject nowAirObj;
//飞机的父物体位置
public Transform airPos;
//飞机的属性列表 通过隐藏和显示GameObject实现
public GameObject[] hpList;
public GameObject[] speedList;
public GameObject[] voluemeList;
public override void Init()
{
//在GameDataManager中保存了当前选中的飞机id 默认从0开始 点右按钮++ 左按钮-- 还有对边界的判断
btnRight.onClick.AddListener(() =>
{
++GameDataManager.Instance.nowSelectAir;
if(GameDataManager.Instance.nowSelectAir > GameDataManager.Instance.airData.Count - 1)
GameDataManager.Instance.nowSelectAir = 0;
ChangAir();
});
btnLeft.onClick.AddListener(() =>
{
--GameDataManager.Instance.nowSelectAir;
if (GameDataManager.Instance.nowSelectAir < 0)
GameDataManager.Instance.nowSelectAir = GameDataManager.Instance.airData.Count - 1;
ChangAir();
});
btnStart.onClick.AddListener(() =>
{
SceneManager.LoadScene("GameScene");
});
btnClose.onClick.AddListener(() =>
{
BeginPanel.Instance.ShowPanel();
HidePanel();
});
HidePanel();
}
//重写了隐藏和显示方法 这样就可以同时实现一些逻辑
public override void ShowPanel()
{
base.ShowPanel();
GameDataManager.Instance.nowSelectAir = 0;
ChangAir();
}
public override void HidePanel()
{
base.HidePanel();
DestroyObj();
}
public void ChangAir()
{
//删除上个飞机 创建新飞机 并记录
DestroyObj();
AirData airData = GameDataManager.Instance.airData[GameDataManager.Instance.nowSelectAir];
nowAirObj = Instantiate(Resources.Load<GameObject>(airData.res));
nowAirObj.transform.SetParent(airPos.transform, false);
for(int i = 0; i < hpList.Length;i++)
{
hpList[i].SetActive(i < airData.hp);
speedList[i].SetActive(i < airData.speed);
voluemeList[i].SetActive(i < airData.volume);
}
}
public void DestroyObj()
{
if(nowAirObj != null)
{
Destroy(nowAirObj);
}
}
private float time;
//点中可以进行拖动
public bool isSelect;
private void Update()
{
//让飞机上下缓缓飞(展示作用)通过sin函数 乘内测的数代表飞的频率 乘外侧的数代表飞的位移
//基于世界坐标 飞机的展示面会倾斜 基于自己坐标是错的
time += Time.deltaTime;
this.airPos.Translate(Vector3.up * Mathf.Sin(time) * 0.001f,Space.World);
//鼠标左键实现拖动 通过射线检测 将飞机的旋转角度以y旋转 度数为Mouse X * 10度
if(Input.GetMouseButtonDown(0))
{
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), 1000, 1 << LayerMask.NameToLayer("Air")))
{
isSelect = true;
}
}
if(Input.GetMouseButtonUp(0))
{
isSelect = false;
}
if(isSelect && Input.GetMouseButton(0))
{
airPos.rotation *= Quaternion.AngleAxis(Input.GetAxis("Mouse X") * 10,Vector3.up);
}
}
}
2.玩家类(飞机超出屏幕问题未解决)
(1)当飞机左右移动时,需要有一定的倾斜,停止时恢复
(2)玩家类需要受伤 死亡 移动方法
知识点
(1)射线检测时销毁碰撞体(子弹)
public class PlayerObj : MonoBehaviour
{
//写成一个单例类
private static PlayerObj instance;
public static PlayerObj Instance => instance;
//maxHp代表最大血量 nowHp代表变量
public int nowHp;
public int maxHp;
public int moveSpeed;
public int rotaSpeed;
//是否死亡
public bool isDead;
//目标四元数旋转角度
Quaternion targetQ;
private float hValue;
private float vValue;
//世界坐标的点 没有去屏幕外的点
private Vector3 frontPos = new Vector3(-41,0,23);
private Vector3 frontPos2 = new Vector3(41, 0, -23);
//世界坐标转换为屏幕坐标的点
private Vector3 nowPos;
private void Awake()
{
instance = this;
}
void Update()
{
if (isDead)
return;
//GetAxisRaw 不是渐变 -1 1两个数
//ad
hValue = Input.GetAxisRaw("Horizontal");
//ws
vValue = Input.GetAxisRaw("Vertical");
//为0时说明静止
if (hValue == 0)
targetQ = Quaternion.identity;
else
//不为0时 按左键值会小于0 右键值会大于0 决定一下旋转的方向
//可以得到一个目标的四元数
targetQ = hValue < 0 ? Quaternion.AngleAxis(20, Vector3.forward) : Quaternion.AngleAxis(-20, Vector3.forward);
//趋近于目标四元数
this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetQ,Time.deltaTime * rotaSpeed);
//正常移动
this.transform.Translate(Vector3.forward * vValue * Time.deltaTime * moveSpeed);
//左右移动不能使用自身坐标系 越动越会往下掉 要使用世界
this.transform.Translate(Vector3.right * hValue * Time.deltaTime * moveSpeed,Space.World);
//当前坐标的屏幕坐标点
this.nowPos = Camera.main.WorldToScreenPoint(this.transform.position);
//******************临时判断屏幕边缘的逻辑 当分辨率变化时会出错 暂未解决******************
if (nowPos.x <= 0)
{
//只管理x 不管理yz
this.transform.position = new Vector3(frontPos.x, this.transform.position.y, this.transform.position.z);
}
if (nowPos.y <= 0)
{
this.transform.position = new Vector3(this.transform.position.x, this.transform.position.y, frontPos2.z);
}
if(nowPos.x >= Screen.width)
{
this.transform.position = new Vector3(frontPos2.x, this.transform.position.y, this.transform.position.z);
}
if (nowPos.y >= Screen.height)
{
this.transform.position = new Vector3(this.transform.position.x, this.transform.position.y, frontPos.z);
}
//获取碰到的对象的信息
RaycastHit hit;
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 1000, 1 << LayerMask.NameToLayer("Bullet")))
{
//通过hit可以得到他身上所有的信息
BulletObj bullet = hit.transform.GetComponent<BulletObj>();
bullet.Dead();
}
}
public void Wound()
{
this.nowHp -= 1;
Debug.Log("当前血量" + nowHp);
GamePanel.Instance.ChangHp(nowHp);
if(nowHp <= 0)
{
nowHp = 0;
Dead();
}
}
public void Dead()
{
isDead = true;
EndPanel.Instance.ShowPanel();
}
}
3.子弹类
(1)子弹的初始化数据 移动方式 生命周期 销毁以及碰撞检测
public class BulletObj : MonoBehaviour
{
private BulletData data;
private float time;
/// <summary>
/// 一个初始化方法 创建子弹时调用 传入目标数据
/// </summary>
/// <param name="id"></param>
public void Init(int id)
{
data = GameDataManager.Instance.bulletDatas[id - 1];
//生命周期 到一定时间自动销毁子弹
Invoke("DelayDestroy", data.destroyTime);
}
//startPos代表起始位置 下面做四元数匀速运动用的
private void Awake()
{
startPos = this.transform;
}
//销毁子弹 创建特效
public void Dead()
{
GameObject effObj = Instantiate(Resources.Load<GameObject>(data.effRes));
effObj.transform.position = this.transform.position;
Destroy(effObj,0.5f);
Destroy(this.gameObject);
}
//碰撞检测
public void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Player"))
{
if(!PlayerObj.Instance.isDead)
{
PlayerObj obj = other.GetComponent<PlayerObj>();
obj.Wound();
Dead();
}
else
{
this.Dead();
}
}
}
//不确定子弹会不会提前被移除掉 所以使用延迟函数 当子弹已经被移除的时候 就不会执行了
public void DelayDestroy()
{
GameObject effObj = Instantiate(Resources.Load<GameObject>(data.effRes));
effObj.transform.position = this.transform.position;
Destroy(effObj, 0.5f);
Destroy(this.gameObject);
}
//时间标识 做匀速运动用
private float uniform;
private Transform startPos;
// Update is called once per frame
void Update()
{
//共同的特征 面朝向移动
this.transform.Translate(Vector3.forward * data.forwardSpeed * Time.deltaTime);
//1 直线运动
//2 曲线运动
//3 左抛物线
//4 右抛物线
//5 跟踪
switch (data.type)
{
case 2:
//sin里面的值 决定 左右变化的频率
//sin后面的值 决定 左右位移的多少
time += Time.deltaTime;
this.transform.Translate(Vector3.right * Mathf.Sin(time * data.rightSpeed) * data.rotaSpeed * Time.deltaTime);
break;
case 3:
this.transform.rotation *= Quaternion.AngleAxis(-data.rotaSpeed * Time.deltaTime,Vector3.up);
break;
case 4:
this.transform.rotation *= Quaternion.AngleAxis(data.rotaSpeed * Time.deltaTime, Vector3.up);
break;
case 5:
//先快后慢 出现问题 "子弹无线接近目标旋转位置过远" 那要看一下是不是子弹的位置没有归0
//这里使用匀速
uniform += Time.deltaTime;
Quaternion q = Quaternion.LookRotation(PlayerObj.Instance.transform.position - this.transform.position);
this.transform.rotation = Quaternion.Slerp(this.startPos.rotation,q, uniform * data.rotaSpeed);
break;
}
}
}
4.炮口点
(1)炮口的位置为上方三点 中间左右两点 下方三点
(2)炮口的初始角度(为了方便开火点四元数的计算)
(3)上下左右四个点的旋转角度是180度,四个角旋转角度都是90度,这样子弹才能合理地发射在屏幕内
public enum Type_PointType
{
topLeft,
topRight,
topCenter,
Left,
Right,
bottomLeft,
bottomRight,
bottomCenter
}
public class PointObj : MonoBehaviour
{
public Type_PointType type = new Type_PointType();
private Vector3 pos;
//炮口的初始方向
private Vector3 initPos;
//炮口去拿到配置表中的数据 方便使用
private int num;
private float offset;
private float delay;
//全部的子弹数据
List<BulletData> bullets;
//单个开火数据
public FireData fire;
//单个子弹数据
public BulletData bullet;
// Update is called once per frame
void Update()
{
//初始化各个炮口的位置
UpdatePos();
//每次发射子弹的随机数 以及旋转角度的设置
Reset();
//发射子弹
Fire();
}
public void UpdatePos()
{
//我们需要找到在z这一层的切面 因为其他子弹也需要在这一层
//先根据玩家的世界坐标找到屏幕坐标的z是多少
//然后设置世界坐标的z给pos
//最后用屏幕转为世界坐标 让子弹出现的位置都在这一层即可
pos.z = 40;
switch(type)
{
case Type_PointType.topLeft:
pos.x = 0;
pos.y = Screen.height;
initPos = Vector3.right;
break;
case Type_PointType.topRight:
pos.x = Screen.width;
pos.y = Screen.height;
initPos = Vector3.left;
break;
case Type_PointType.topCenter:
pos.x = Screen.width/2;
pos.y = Screen.height;
initPos = Vector3.right;
break;
case Type_PointType.Left:
pos.x = 0;
pos.y = Screen.height/2;
initPos = Vector3.right;
break;
case Type_PointType.Right:
pos.x = Screen.width;
pos.y = Screen.height/2;
initPos = Vector3.left;
break;
case Type_PointType.bottomLeft:
pos.x = 0;
pos.y = 0;
initPos = Vector3.right;
break;
case Type_PointType.bottomRight:
pos.x = Screen.width;
pos.y = 0;
initPos = Vector3.left;
break;
case Type_PointType.bottomCenter:
pos.x = Screen.width/2;
pos.y = 0;
initPos = Vector3.right;
break;
}
this.transform.position = Camera.main.ScreenToWorldPoint(pos);
}
//旋转的角度
private float angle;
/// <summary>
///
/// 随机数 将子弹的四种移动方式和开火的两种方式随机排列组合 想实现的功能看个人
/// bullet 四种类型1.直线 2.曲线 3.左抛物线 4.右抛物线 5.跟踪
/// fire 两种类型1.连发 2.散弹
/// </summary>
public void Reset()
{
//终止条件的判断 如果子弹间隔时间和数量没有发完 不能从新随机
if (offset != 0 && num != 0)
return;
if(fire != null)
{
//delay代表每组子弹的间隔时间
delay -= Time.deltaTime;
if(delay > 0)
{
return;
}
}
//拿到全部的开火数据 随机选择开火方式
List<FireData> fires = GameDataManager.Instance.fireData;
fire = fires[Random.Range(0, fires.Count)];
//本地拿到初始值
this.num = fire.num;
this.offset = fire.offset;
this.delay = fire.delay;
//拿到全部的子弹数据 随机选择子弹移动方式
bullets = GameDataManager.Instance.bulletDatas;
bullet = bullets[Random.Range(0, bullets.Count)];
//1.左上角和右下角发射右抛物线
//2.左下角和右上角发射左抛物线
//3.type = 3 是左抛物线 type = 4 是右抛物线
if(fire.type == 2)
{
//2类型是散弹 需要随机角度
switch (type)
{
case Type_PointType.topLeft:
case Type_PointType.bottomLeft:
case Type_PointType.topRight:
case Type_PointType.bottomRight:
angle = 90 / num;
break;
case Type_PointType.topCenter:
case Type_PointType.Left:
case Type_PointType.Right:
case Type_PointType.bottomCenter:
angle = 180 / num;
break;
}
}
}
private Vector3 newRot;
/// <summary>
/// 开火 个人决定开火口能发射怎样的子弹
/// 核心点:根据不同的开火方式 去创建子弹 以及子弹特效 子弹的位置 子弹的旋转角度
/// </summary>
public void Fire()
{
if (type == Type_PointType.topLeft || type == Type_PointType.bottomRight)
{
bullet.type = 4;
}
if (type == Type_PointType.topRight || type == Type_PointType.bottomLeft)
{
bullet.type = 3;
}
//间隔时间和数量归0 不会进入循环
if (offset == 0 && num == 0)
return;
//每个子弹的判断放到最前面
offset -= Time.deltaTime;
//当间隔时间大于0 还在等待期
if (offset > 0)
return;
GameObject go;
switch (fire.type)
{
case 1:
go = Instantiate(Resources.Load<GameObject>(bullet.bulletRes));
go.GetComponent<BulletObj>().Init(bullet.id);
go.transform.position = this.transform.position;
go.transform.rotation = Quaternion.LookRotation(PlayerObj.Instance.transform.position - this.transform.position);
//子弹减少
--num;
//当子弹发射完成后 间隔时间归0 就不会再次进入循环 没发射完就重置间隔时间
offset = num == 0 ? 0 : fire.offset;
break;
case 2:
//散弹不能发射曲线子弹
if (bullet.type != 2)
{
for (int i = 0; i < num; i++)
{
go = Instantiate(Resources.Load<GameObject>(bullet.bulletRes));
go.GetComponent<BulletObj>().Init(bullet.id);
go.transform.position = this.transform.position;
//四元数乘向量 得到新的向量
//每次旋转i * 20
newRot = Quaternion.AngleAxis(i * angle, Vector3.up) * initPos;
//新的方向给四元数 最后赋值给角度即可
go.transform.rotation = Quaternion.LookRotation(newRot);
}
offset = num = 0;
}
else
{
Reset();
}
break;
}
}
}
5.Main调用
(1)最后通过Main放置在游戏场景,当进入场景时,根据选择的id创建飞机,初始化变量,更新界面的ui,其实放在GamePanel中也是一样的,都可以