飞机大战简单概述

张开发
2026/4/13 14:53:21 15 分钟阅读

分享文章

飞机大战简单概述
开始界面选中Main Camera将天空盒改为用颜色显示Solid Color我这里用的NGUI而且要显示选角面板所以创建Root(3D)NGUI的摄像机只渲染UI层所以Main Camera的渲染会自动取消UI如果没取消记得取消。拼面板之前一定要检查UI Root的分辨率是否合适。NGUI上的按钮需要添加碰撞器Colider和 按钮Button Script。写脚本BeginPanel关联按钮编写事件监听。控制 UI 渲染顺序Sorting Layer/Order让相同材质的 UI 连续渲染避免不同类型 UI 穿插打断合批从而降低 Draw Call。给背景图添加碰撞器可以阻挡事件避免误触。数据管理类XMLpublic class GameDataMgr { private static GameDataMgr instance new GameDataMgr(); public static GameDataMgr Instance instance; //音乐相关数据 public MusicData musicData; //排行榜数据 public RankData rankData; //角色数据 public RoleData roleData; //子弹数据 public BulletData bulletData; //开火点 数据 public FireData fireData; //当前选择的角色编号 public int nowSelHeroIndex 0; private GameDataMgr() { //获取本地硬盘中存储的音乐数据 musicData XmlDataMgr.Instance.LoadData(typeof(MusicData), MusicData) as MusicData; //一开始 就读取本地的 排行榜数据 rankData XmlDataMgr.Instance.LoadData(typeof(RankData), RankData) as RankData; //一开始就读取角色数据 roleData XmlDataMgr.Instance.LoadData(typeof(RoleData), RoleData) as RoleData; //一开始 就读取子弹数据 bulletData XmlDataMgr.Instance.LoadData(typeof(BulletData), BulletData) as BulletData; //一开始就读取开火点数据 fireData XmlDataMgr.Instance.LoadData(typeof(FireData), FireData) as FireData; } #region 音乐音效相关 //保存音乐数据相关数据的方法 public void SaveMusicData() { XmlDataMgr.Instance.SaveData(musicData, MusicData); } //开关背景音乐的方法 public void SetMusicIsOpen(bool isOpen) { //改数据 musicData.musicIsOpen isOpen; //真正改变背景音乐的开关 BKMusic.Instance.SetBKMusicIsOpen(isOpen); } //开关音效的方法 public void SetSoundIsOpen(bool isOpen) { //改数据 musicData.soundIsOpen isOpen; //真正改变音效的开关 } //设置背景音乐的音量 public void SetMusicValue(float value) { //改数据 musicData.musicValue value; //真正改变背景音乐的大小 BKMusic.Instance.SetBKMusicValue(value); } //设置音效的音量 public void SetSoundValue(float value) { //改数据 musicData.soundValue value; } #endregion #region 排行榜相关 /// summary /// 添加排行榜数据 /// /summary /// param namename玩家名/param /// param nametime通关时间/param public void AddRankData(string name, int time) { RankInfo rankInfo new RankInfo(); rankInfo.name name; rankInfo.time time; rankData.rankList.Add(rankInfo); //排序 rankData.rankList.Sort((a, b) { if (a.time b.time) return -1; return 1; }); //移除大于20条的内容 if (rankData.rankList.Count 20) rankData.rankList.RemoveAt(20); //rankData.rankList.RemoveRange(20, rankData.rankList.Count - 20); //保存数据 XmlDataMgr.Instance.SaveData(rankData, RankData); } #endregion #region 玩家数据相关 /// summary /// 提供给外部获取当前选择的 英雄数据 /// /summary /// returns/returns public RoleInfo GetNowSeleHeroInfo() { return roleData.roleList[nowSelHeroIndex]; } #endregion }NGUI排行榜面板滚动视图创建Sprite放到合适位置长条状CtrlD一个作为子对象前景。给父对象添加Colider和Progress Bar Script滚动条脚本关联前景背景设置从上到下。创建Scroll View调整大小和位置Movement设为Verticl垂直滑动关联滚动条。Scroll View下创建Sprite调整宽高改名为RankItem排行榜一条数据的组件创建三个Label作为子对象。给RankItem添加Colider和Drag Scroll View脚本。将RankItem做成预设体放进Resources/UI文件夹。排行榜脚本public class RankPanel : BasePanelRankPanel { public UIButton btnClose; public UIScrollView svList; //专门用于存储 下面的单条数据控件的 private ListRankItem itemList new ListRankItem(); public override void Init() { btnClose.onClick.Add(new EventDelegate(() { HideMe(); })); HideMe(); /*//测试代码 一开始 加几条排行榜数据进去 用于更新 否则游戏开始没数据 for (int i 0; i 7; i) { GameDataMgr.Instance.AddRankData(桉愚青权 i, Random.Range(40,4000)); }*/ } public override void ShowMe() { base.ShowMe(); //更新面板上显示的信息 //获取本地存储的排行榜数据 ListRankInfo list GameDataMgr.Instance.rankData.rankList; //根据数据 更新面板上 组合控件的信息 //组合控件数量只会多不会减少 因为玩家只会玩游戏增加数据不会删除数据。 for (int i 0; i list.Count; i) { //如果面板上已经存在组合控件 直接更新即可 if (itemList.Count i) { itemList[i].InitInfo(i 1, list[i].name, list[i].time); } //如果面板上的 组合控件不够多 就去实例化出来 else { //创建预设体 GameObject obj Instantiate(Resources.LoadGameObject(UI/RankItem)); //设置父对象 obj.transform.SetParent(svList.transform, false); //设置位置 obj.transform.localPosition new Vector3(0, 155 - i * 45, 0); //设置数据 //得到脚本 RankItem item obj.GetComponentRankItem(); //调用设置数据的方法 item.InitInfo(i 1, list[i].name, list[i].time); //记录 itemList.Add(item); } } } }选角面板在面板中创建空物体作为角色创建时的位置模型层级改成UI。如果发现模型显示不全检查UI摄像机的近裁剪面和远裁剪面面板脚本public class ChoosePanel : BasePanelChoosePanel { //各个按钮 public UIButton btnClose; public UIButton btnLeft; public UIButton btnRight; public UIButton btnStart; //模型父对象 public Transform heroPos; //下方属性相关对象 public ListGameObject hpObjs; public ListGameObject speedObjs; public ListGameObject volumeObjs; //当前显示的飞机模型对象 private GameObject airPlaneObj; public Camera uiCamera; public override void Init() { //选择角色后 点击开始 切换场景 btnStart.onClick.Add(new EventDelegate(() { SceneManager.LoadScene(GameScene); })); btnLeft.onClick.Add(new EventDelegate(() { //Debug.Log(【调试】左按钮被点击了当前索引前 GameDataMgr.Instance.nowSelHeroIndex); //左按钮 减索引 --GameDataMgr.Instance.nowSelHeroIndex; //如果小于最小的索引 直接等于最后一个索引 if(GameDataMgr.Instance.nowSelHeroIndex 0) GameDataMgr.Instance.nowSelHeroIndex GameDataMgr.Instance.roleData.roleList.Count - 1; //Debug.Log(【调试】当前索引后 GameDataMgr.Instance.nowSelHeroIndex); ChangeNowHero(); })); btnRight.onClick.Add(new EventDelegate(() { //右按钮 加索引 GameDataMgr.Instance.nowSelHeroIndex; //如果大于最大的索引 直接等于0 if (GameDataMgr.Instance.nowSelHeroIndex GameDataMgr.Instance.roleData.roleList.Count) GameDataMgr.Instance.nowSelHeroIndex 0; ChangeNowHero(); })); btnClose.onClick.Add(new EventDelegate(() { //隐藏自己 HideMe(); //显示开始界面 BeginPanel.Instance.ShowMe(); })); HideMe(); } public override void ShowMe() { base.ShowMe(); //每次显示的时候都从第一个开始选择 GameDataMgr.Instance.nowSelHeroIndex 0; ChangeNowHero(); } public override void HideMe() { base.HideMe(); //删除当前的模型 DestroyObj(); } //切换当前的选择 private void ChangeNowHero() { //得到当前选择的 玩家英雄数据 RoleInfo info GameDataMgr.Instance.GetNowSeleHeroInfo(); //更新模型 //先删除上一次的飞机模型 DestroyObj(); //再创建当前的飞机模型 airPlaneObj Instantiate(Resources.LoadGameObject(info.resName)); //设置父对象 airPlaneObj.transform.SetParent(heroPos, false); //设置角度和位置 缩放 airPlaneObj.transform.localPosition Vector3.zero; airPlaneObj.transform.localRotation Quaternion.identity; airPlaneObj.transform.localScale Vector3.one * info.scale; //修改层级 airPlaneObj.layer LayerMask.NameToLayer(UI); //更新属性 for (int i 0; i 10; i) { hpObjs[i].SetActive(i info.hp); speedObjs[i].SetActive(i info.speed); volumeObjs[i].SetActive(i info.volume); } } /// summary /// 用于删除上一次显示的模型对象 /// /summary private void DestroyObj() { if (airPlaneObj ! null) { Destroy(airPlaneObj); airPlaneObj null; } } private float time; //是否鼠标选中 模型 private bool isSel; void Update() { //让飞机上下浮动 time Time.deltaTime; heroPos.Translate(Vector3.up * Mathf.Sin(time) * 0.0001f, Space.World); //射线检测 让飞机 可以左右转动 if (Input.GetMouseButtonDown(0)) { //如果点击到了 UI层碰撞器 认为需要开始 拖动飞机了 if (Physics.Raycast(uiCamera.ScreenPointToRay(Input.mousePosition), 1000, 1 LayerMask.NameToLayer(UI))) { isSel true; } } //抬起取消选中 if(Input.GetMouseButtonUp(0)) isSel false; //旋转对象 if (Input.GetMouseButton(0) isSel) { heroPos.rotation * Quaternion.AngleAxis(Input.GetAxis(Mouse X) * -20, Vector3.up); } } }玩家模型背特效遮挡俯视调整模型距离发现玩家模型被特效遮挡。在不动特效的前提下可以使用两个摄像机渲染特效用一个摄像机渲染飞机再用一个摄像机渲染再将两个摄像机渲染的内容一起显示即可解决。Main Camera只渲染特效层新建层级Effect新建摄像机Camera渲染除了UI和Effect外的其他层将Skybox天空盒改成Depth only只渲染这一层调整摄像机层级UI显示在最前面UI摄像机改到最大层层级越大显示得越前面主玩家相关解决延迟//使用热键因为是渐变会有延迟 hValue Input.GetAxis(Horizontal); //无渐变不会有延迟 hValue Input.GetAxisRaw(Horizontal);移动相关//移动 this.transform.Translate(Vector3.forward * vValue * speed * Time.deltaTime); //横向移动因为改变了物体旋转所以需要使用世界坐标 this.transform.Translate(Vector3.right * hValue * speed * Time.deltaTime, Space.World);控制移动范围将玩家控制在屏幕内防止移动超出屏幕。//当前世界坐标转屏幕上的点 private Vector3 nowPos; //上一次玩家的位置 就是在位移前 玩家的 private Vector3 frontPos; void Update() { //在位移之前 记录 之前的位置 frontPos this.transform.position; //移动相关 //进行极限判断 nowPos Camera.main.WorldToScreenPoint(this.transform.position); //左右溢出判断 if (nowPos.x 0 || nowPos.x Screen.width) { this.transform.position new Vector3(frontPos.x, this.transform.position.y, this.transform.position.z); } //上下溢出判断 if (nowPos.y 0 || nowPos.y Screen.height) { this.transform.position new Vector3(this.transform.position.x,this.transform.position.y, frontPos.z); } }简单来说就是用两个变量一个存移动前的值一个存移动后的值进行溢出判断。子弹相关数据类/// summary /// 子弹数据集合 /// /summary public class BulletData { public ListBulletInfo bulletInfoList new ListBulletInfo(); } //子弹单条数据 public class BulletInfo { [XmlAttribute] public int id;//子弹数据的ID 方便我们配置的时候修改数据 [XmlAttribute] public int type;//子弹的移动规则1~5代表不同的五种移动规则 [XmlAttribute] public float forwardSpeed;//正朝向 移动速度 [XmlAttribute] public float rightSpeed;//横向移动速度 [XmlAttribute] public float roundSpeed;//旋转速度 [XmlAttribute] public string resName;//子弹特效资源路径 [XmlAttribute] public string deadEffRes;//子弹销毁时 创建的死亡特效 [XmlAttribute] public float lifeTime;//子弹出生到销毁的生命周期时间 }逻辑处理public class BulletObject : MonoBehaviour { //子弹使用的数据 private BulletInfo info; //用于曲线运动的计时变量 private float time; //初始化子弹数据的方法 public void InitInfo(BulletInfo info) { this.info info; //根据生命周期函数 决定自己什么时候 延迟移除 //Destroy(this.gameObject, info.lifeTime); //Debug.Log($子弹生命周期{info.lifeTime}销毁特效路径{info.deadEffRes}); // 新增日志 Invoke(DealyDestroy, info.lifeTime); } private void DealyDestroy() { //直接死亡 会播放特效 Dead(); } //销毁场景上的子弹 public void Dead() { //创建死亡特效 GameObject effObj Instantiate(Resources.LoadGameObject(info.deadEffRes)); //设置特效的位置 创建当前子弹的位置 effObj.transform.position this.transform.position; //1秒钟后延迟移除特效对象 Destroy(effObj, 1f); //销毁子弹对象 Destroy(this.gameObject); } //和对象碰撞时触发 private void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag(Player)) { //得到玩家脚本 PlayerObject obj other.GetComponentPlayerObject(); //玩家受伤减血 obj.Wound(); //销毁自己 Dead(); } } // Update is called once per frame void Update() { //所有移动的共同特点 都是朝自己的面朝向前进 this.transform.Translate(Vector3.forward * info.forwardSpeed * Time.deltaTime); //接着再来处理其他的移动逻辑 //1 代表 只朝自己面朝向移动 直线运动 //2 代表 曲线运动 //3 代表 右抛物线 //4 代表 左抛物线 //5 代表 跟踪导弹 switch (info.type) { case 2: time Time.deltaTime; //sin里面值变化的快慢 决定了 左右变化的频率 //乘以的速度 变化的大小 决定了左右唯一的多少 //曲线运动时 roundSpeed 主要用于控制变化的频率 this.transform.Translate(Vector3.right * Mathf.Sin(time * info.roundSpeed) * Time.deltaTime * info.rightSpeed); break; case 3: //右抛物线 无非就是改变旋转角度 this.transform.rotation * Quaternion.AngleAxis(info.roundSpeed * Time.deltaTime, Vector3.up); break; case 4: //左抛物线 无非就是改变旋转角度 this.transform.rotation * Quaternion.AngleAxis(-info.roundSpeed * Time.deltaTime, Vector3.up); break; case 5: //跟踪导弹 不停地计算玩家和我的向量 然后得到四元数 自己的角度不停地变化为这个目标四元数 this.transform.rotation Quaternion.Slerp(this.transform.rotation, Quaternion.LookRotation(PlayerObject.Instance.transform.position - this.transform.position), info.roundSpeed * Time.deltaTime); break; } } }Invoke延迟函数。物体被Destroy后会停止。开火点相关数据类public class FireData { public ListFireInfo fireInfoList new ListFireInfo(); } /// summary /// 单条开火点数据 /// /summary public class FireInfo { [XmlAttribute] public int id;//开火点ID 主要方便配置 [XmlAttribute] public int type;//开火点类型 是散弹还是按顺序发射 1顺序 2散弹 [XmlAttribute] public int num; //数量 该组子弹有多少颗 [XmlAttribute] public float cd;//每颗子弹的间隔时间 [XmlAttribute] public string ids;//关联的子弹ID 1~10 代表的 就是1~10ID的 子弹数据中去随机 [XmlAttribute] public float delay;//组间间隔时间 }逻辑处理/// summary /// 表示开火点的类型 /// /summary public enum E_Pos_Type { TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight, } public class FireObject : MonoBehaviour { public E_Pos_Type type; //当前开火点的数据信息 private FireInfo fireInfo; private int nowNum; private float nowCd; private float nowDelay; //当前组子弹使用的的子弹信息 private BulletInfo nowBulletInfo; //散弹时 每颗子弹的间隔角度 private float changeAngle; //表示屏幕上的点 private Vector3 screenPos; //初始发射子弹的方向 主要用于作为散弹的初始方向 用于计算 private Vector3 initDir; //这个是用于发射散弹时 记录上一次的方向的 private Vector3 nowDir; // Update is called once per frame void Update() { //更新位置 UpdatePos(); //每一次都检测是否需要重置 开火点的数据 ResetFireInfo(); //发射子弹 UpdateFire(); } //根据点的类型 来更新它的位置 private void UpdatePos() { //这里设置为主玩家位置转屏幕坐标后的 z位置一样 目的是 让点和玩家所在的 横截面是一致的 screenPos.z 350; switch (type) { case E_Pos_Type.TopLeft: screenPos.x 0; screenPos.y Screen.height; initDir Vector3.right; break; case E_Pos_Type.Top: screenPos.x Screen.width / 2; screenPos.y Screen.height; initDir Vector3.right; break; case E_Pos_Type.TopRight: screenPos.x Screen.width; screenPos.y Screen.height; initDir Vector3.left; break; case E_Pos_Type.Left: screenPos.x 0; screenPos.y Screen.height / 2; initDir Vector3.right; break; case E_Pos_Type.Right: screenPos.x Screen.width; screenPos.y Screen.height / 2; initDir Vector3.left; break; case E_Pos_Type.BottomLeft: screenPos.x 0; screenPos.y 0; initDir Vector3.right; break; case E_Pos_Type.Bottom: screenPos.x Screen.width / 2; screenPos.y 0; initDir Vector3.right; break; case E_Pos_Type.BottomRight: screenPos.x Screen.width; screenPos.y 0; initDir Vector3.left; break; } //再把屏幕坐标 转换为 世界坐标 那么得到的就是 想要的坐标 this.transform.position Camera.main.ScreenToWorldPoint(screenPos); } //重置当前要发射的炮台数据 private void ResetFireInfo() { //自己定规则 只有当cd和数量都为0时 才认为需要重新获取我们的 发射点数据 if (nowCd ! 0 nowNum ! 0) return; //组间休息时间判断 if (fireInfo ! null) { nowDelay - Time.deltaTime; //还在组间休息 if (nowDelay 0) return; } //从数据中随机取出来一条 按照规则 发射子弹 ListFireInfo list GameDataMgr.Instance.fireData.fireInfoList; fireInfo list[Random.Range(0, list.Count)]; //不能直接改变数据当中的内容 我们应该拿变量 临时存储下来 这样就不会影响数据本身 nowNum fireInfo.num; nowCd fireInfo.cd; nowDelay fireInfo.delay; //通过开火点数据取出 当前要使用的子弹数据信息 //得到开始id和结束id 用于随机取子弹信息 string[] strs fireInfo.ids.Split(,); int beginID int.Parse(strs[0]); int endID int.Parse(strs[1]); int randomBulletID Random.Range(beginID, endID 1); nowBulletInfo GameDataMgr.Instance.bulletData.bulletInfoList[randomBulletID - 1]; //如果是散弹 那么计算每颗子弹的间隔角度 if (fireInfo.type 2) { switch (type) { case E_Pos_Type.TopLeft: case E_Pos_Type.TopRight: case E_Pos_Type.BottomLeft: case E_Pos_Type.BottomRight: changeAngle 90f / (nowNum 1); break; case E_Pos_Type.Top: case E_Pos_Type.Left: case E_Pos_Type.Right: case E_Pos_Type.Bottom: changeAngle 180f / (nowNum 1); break; } } } //检测开火 private void UpdateFire() { //当前状态不需要发射子弹 if (nowNum 0 nowCd 0) return; //cd更新 nowCd - Time.deltaTime; if (nowCd 0) return; GameObject bullet; BulletObject bulletObj; switch (fireInfo.type) { //一个一个地发射子弹 case 1: //动态创建子弹对象 bullet Instantiate(Resources.LoadGameObject(nowBulletInfo.resName)); //动态挂载 子弹脚本 bulletObj bullet.AddComponentBulletObject(); //把当前的子弹数据传入子弹脚本 进行初始化 bulletObj.InitInfo(nowBulletInfo); //设置子弹的位置和朝向 bullet.transform.position this.transform.position; bullet.transform.rotation Quaternion.LookRotation(PlayerObject.Instance.transform.position - this.transform.position); //表示已经发射一颗子弹 --nowNum; //重置CD nowCd nowNum 0 ? 0 : fireInfo.cd; break; //发射散弹 case 2: //无cd 一瞬间 发射完所有子弹 if (nowCd 0) { for (int i 0; i nowNum; i) { //动态创建子弹对象 bullet Instantiate(Resources.LoadGameObject(nowBulletInfo.resName)); //动态挂载 子弹脚本 bulletObj bullet.AddComponentBulletObject(); //把当前的子弹数据传入子弹脚本 进行初始化 bulletObj.InitInfo(nowBulletInfo); //设置子弹的位置和朝向 bullet.transform.position this.transform.position; //每次都会旋转一个角度 得到一个新的方向 nowDir Quaternion.AngleAxis(changeAngle * i, Vector3.up) * initDir; bullet.transform.rotation Quaternion.LookRotation(nowDir); } //因为是瞬间创建完所有子弹 所以重置数据 nowCd nowNum 0; } else { //动态创建子弹对象 bullet Instantiate(Resources.LoadGameObject(nowBulletInfo.resName)); //动态挂载 子弹脚本 bulletObj bullet.AddComponentBulletObject(); //把当前的子弹数据传入子弹脚本 进行初始化 bulletObj.InitInfo(nowBulletInfo); //设置子弹的位置和朝向 bullet.transform.position this.transform.position; //每次都会旋转一个角度 得到一个新的方向 nowDir Quaternion.AngleAxis(changeAngle * (fireInfo.num - nowNum), Vector3.up) * initDir; bullet.transform.rotation Quaternion.LookRotation(nowDir); //表示已经发射一颗子弹 --nowNum; //重置CD nowCd nowNum 0 ? 0 : fireInfo.cd; } break; } } }重点得到玩家转屏幕坐标后 横截面的z轴值print(Camera.main.WorldToScreenPoint(PlayerObject.Instance.transform.position));屏幕本身物理 / 显示层面确实没有 z 轴Unity 里WorldToScreenPoint返回的 z 轴本质是 “借Vector3的 z位存储3D 世界的深度信息” ——它不是屏幕的属性而是 “屏幕坐标和 3D 世界坐标之间的桥梁”。因为NGUI的设置问题鼠标会在中心点且被隐藏。可以在游戏场景里UI Root下的Camera的脚本里修改设置以解决问题。把这个选项取消勾选即可。点击事件关于鼠标点击子弹进行子弹销毁的逻辑//射线检测 用于销毁子弹 if (Input.GetMouseButtonDown(0)) { RaycastHit hitInfo; //这里只检测子弹层 if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hitInfo, 1000f, 1 LayerMask.NameToLayer(Bullet))) { BulletObject bulletObj hitInfo.transform.GetComponentBulletObject(); //直接让被点中的子弹销毁 bulletObj.Dead(); } }Physics.Raycast 这个方法有两个任务1. 返回 bool 值有没有击中物体true/false2. 告诉你击中了谁坐标是多少物体的 Transform 是什么但 C# 方法只能返回一个值return所以用 out 把碰撞的详细数据从方法内部「打包带出来」存到 hitInfo 变量里。没有out只能知道击没击中物体但是拿不到hitInfo.transform→ 也就无法获取子弹组件、无法销毁子弹。关于out和refout输出参数核心含义方法给你塞数据 → 把内部的值带到外部• 不用提前给变量赋值• 方法内部必须给这个变量赋值• 用途方法需要返回多个结果时用比如射线的 bool 碰撞信息ref 引用参数核心含义给方法传数据 → 方法修改后外部变量同步变• 必须提前给变量赋值• 方法内部可以改也可以不改• 用途修改外部变量的值

更多文章