用QFramework重构飞机大战(Siki Andy的)(下02)(06-0? 游戏界面及之后的所有面板)
用QFramework重构飞机大战(Siki Andy的)(下02)(06-0? 游戏界面及之后的所有面板)
GitHub
//
官网的
全民飞机大战(第一季)-----框架设计篇(Unity 2017.3)
全民飞机大战(第二季)-----游戏逻辑篇(Unity 2017.3)
全民飞机大战(第三季)-----完善功能篇(Unity 2017.3)
全民飞机大战(第四季)-----新手引导篇
//
B站各放几集
全民飞机大战(第一季)-----框架设计篇(Unity 2017.3)
全民飞机大战(第二季)-----游戏逻辑篇(Unity 2017.3)
全民飞机大战(第三季)-----完善功能篇(Unity 2017.3)
全民飞机大战(第四季)-----新手引导篇
bug QF处理单例类中的使用this.Getxxx的
Single.GetModel不行,会一直跳空,使用System
using QFramework; using QFramework.AirCombat; using System.Collections; using System.Collections.Generic; using UnityEngine; public class AniMgr : NormalSingleton { public void PlaneDestroyAni(Vector3 pos) { var go = PoolMgr.Single.Spawn(ResourcesPath.EFFECT_FRAME_ANI); var view = go.GetOrAddComponent(); view.Init(); view.SetScale(Vector3.one*0.5f); view.SetPos(pos); } public void BulletDestroyAni(Vector3 pos) { var go = PoolMgr.Single.Spawn(ResourcesPath.EFFECT_FRAME_ANI); var view = go.GetOrAddComponent(); view.Init(); view.SetScale(Vector3.one*0.1f); view.SetPos(pos); } } public interface IAniSystem : ISystem { } public class AniSystem : AbstractSystem, IAniSystem { protected override void OnInit() { this.RegisterEvent(OnPlaneDestroyAni); this.RegisterEvent(OnBulletDestroyAni); } public void OnPlaneDestroyAni(PlaneDestroyAniEvent e) { Vector3 pos = e.pos; var go = this.GetSystem().Spawn(ResourcesPath.EFFECT_FRAME_ANI); var view = go.GetOrAddComponent(); view.Init(); view.SetScale(Vector3.one * 0.5f); view.SetPos(pos); } public void OnBulletDestroyAni(BulletDestroyAniEvent e) { Vector3 pos = e.pos; var go = this.GetSystem().Spawn(ResourcesPath.EFFECT_FRAME_ANI); var view = go.GetOrAddComponent(); view.Init(); view.SetScale(Vector3.one * 0.1f); view.SetPos(pos); } }
modify MsgEvent时间的注册发送`在这里插入代码片
-------------------------------------------------------------
modify typeof中的参数,怎么作为一个方法的参数来传入
目前做不到注释中的做法
public static void RepeatConstException(this Type type) { //var type = typeof(className);//不知道className怎么做参数 var hs = new HashSet(); var fis = type.GetFields(); foreach (var fi in fis) { var value = fi.GetRawConstantValue(); if (value is int) { if (!hs.Add((int)value)) { Debug.LogError($"{type.Name}中有重复项,重复值为:{value}"); } } else { Debug.LogError($"属性:{fi.Name}.类型错误,此类所有常量必须是int类型"); } } }
效果
using System.Collections; using System.Collections.Generic; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; /// 检测MsgEvent是否有重复项 public class MsgEventTest : ITest { public IEnumerator Execute() { (typeof(MsgEvent)).RepeatConstException(); yield return null; } }
bug QF this.GetSystem为空
注释的那句是不行的,所以在接口中要写抽象方法
ISceneSystem sys = this.GetSystem(); //SceneSystem sys = this.GetSystem(); sys.AddSceneLoaded(SceneName.Game, callBack =>
----------------------------------------
Audio
先替换掉AudioMgr
可以省略掉缓存字典、列表,不用自己做
官方案例
三种喇叭
Music
Sound
Voice
using UnityEngine; using UnityEngine.UI; namespace QFramework.Example { public class AudioExample : MonoBehaviour { private void Awake() { var btnPlayHome = transform.Find("BtnPlayHome").GetComponent(); var btnPlayGame = transform.Find("BtnPlayGame").GetComponent(); var btnPlaySound = transform.Find("BtnPlaySoundClick").GetComponent(); var btnPlayVoiceA = transform.Find("BtnPlayVoice").GetComponent(); var btnSoundOn = transform.Find("BtnSoundOn").GetComponent(); var btnSoundOff = transform.Find("BtnSoundOff").GetComponent(); var btnMusicOn = transform.Find("BtnMusicOn").GetComponent(); var btnMusicOff = transform.Find("BtnMusicOff").GetComponent(); var btnVoiceOn = transform.Find("BtnVoiceOn").GetComponent(); var btnVoiceOff = transform.Find("BtnVoiceOff").GetComponent(); var btnStopAllSound = transform.Find("BtnStopAllSounds").GetComponent(); var musicVolumeSlider = transform.Find("MusicVolume").GetComponent(); var voiceVolumeSlider = transform.Find("VoiceVolume").GetComponent(); var soundVolumeSlider = transform.Find("SoundVolume").GetComponent(); btnPlayHome.onClick.AddListener(() => { AudioKit.PlayMusic("resources://home_bg"); }); btnPlayGame.onClick.AddListener(() => { AudioKit.PlayMusic("resources://game_bg"); }); btnPlaySound.onClick.AddListener(() => { AudioKit.PlaySound("resources://button_clicked"); }); btnPlayVoiceA.onClick.AddListener(() => { AudioKit.PlayVoice("resources://hero_hurt"); }); btnSoundOn.onClick.AddListener(() => { AudioKit.Settings.IsSoundOn.Value = true; }); btnSoundOff.onClick.AddListener(() => { AudioKit.Settings.IsSoundOn.Value = false; }); btnMusicOn.onClick.AddListener(() => { AudioKit.Settings.IsMusicOn.Value = true; }); btnMusicOff.onClick.AddListener(() => { AudioKit.Settings.IsMusicOn.Value = false; }); btnVoiceOn.onClick.AddListener(() => { AudioKit.Settings.IsVoiceOn.Value = true; }); btnVoiceOff.onClick.AddListener(() => { AudioKit.Settings.IsVoiceOn.Value = false; }); btnStopAllSound.onClick.AddListener(() => { AudioKit.StopAllSound(); }); AudioKit.Settings.MusicVolume.RegisterWithInitValue(v => musicVolumeSlider.value = v); AudioKit.Settings.VoiceVolume.RegisterWithInitValue(v => voiceVolumeSlider.value = v); AudioKit.Settings.SoundVolume.RegisterWithInitValue(v => soundVolumeSlider.value = v); musicVolumeSlider.onValueChanged.AddListener(v => { AudioKit.Settings.MusicVolume.Value = v; }); voiceVolumeSlider.onValueChanged.AddListener(v => { AudioKit.Settings.VoiceVolume.Value = v; }); soundVolumeSlider.onValueChanged.AddListener(v => { AudioKit.Settings.SoundVolume.Value = v; }); } } }
播放Music
public void PlayBGM() { AudioKit.PlayMusic( $"resources://Audio/BGM/{BGAudio.Game_BGM.Enum2String()}" ); }
播放Sound
角色出场语音的切换
public void PlayPlayerVoice(string name, bool loop = false) { _curPlayer = name; AudioKit.PlayVoice($"resources://Audio/Player/{name}"); // 这个会保持一个喇叭 //AudioKit.PlaySound($"resources://Audio/Player/{name}"); // 这个不会保持一个喇叭 }
bug QF报错
找不到clip会报错,但是这里存在 GameAudio.Null,表示不用播放。
所以做一个return
public void PlaySound(string name, bool loop = false) { if(name==GameAudio.Null) { return; } AudioKit.PlaySound($"{_path}{name}", loop); }
GameAudio枚举改成类
bug Attribute与Summary冲突
可以看到Summary不显示
图2,1放在2前面,可以看到3有显示
。。。
这种需要把summary放在最前面
bug GameLayer生成的节点暴露在根节点下
正常在如下位置。
原来的,把GameLayerMgr跪在GameRoot下
现在把GameLayerMgr挂在Mgr下,GameLayer的三个枚举还是挂在原 来的GameRoot下
用GameObject.Find而不是transform.FindTop
watch 改的顺序
LoadCreaterData
。。。
LifeCycleMgr
LifeCycleAddConfig
LifeCycleConfig
。。。
LoadMgr
ILoader
ResourcesLoader
ABLoader
…
CoroutineMgr
LifeCycleAddConfig
UILayerMgr
UIManager
watch 引导
GuideMgrBase
watch 敌人生成
IEnemyCreator
LevelData
EnemyLevelData
EnemyCreateMgr
GameProcessMgr.Init()
GameProcessMgr.UpdateFunc()
GameProcessMgr中的_start,会监听一个开始游戏的Event,该event来自于StateMdoel
所以生成敌人,需要改变StateMdoel中GameState的值
Message消息
MessageMgr,SubMsgMgr
MessageSystem,IMessageSystem
ActionMgr
modify KeysUtil => KeysUtil:IKeysUtil
改成
IKeysUtil:IUtil
KeysUtil:IKeysUtil,ICanGetModel
modify DataMgr(存储) => StorageUitl
IDataMemory改成IStorage ,可以看到飞机大战的接口设计得更泛用,所以用飞机大战的接口设计
01 只用PlayerPrefs
对应的PlayerPrefsMemory:IDataMemory改名为PlayerPrefsStorage : IStorage(完全照抄PlayerPrefsMemory,就不展示了)
。。。
对应的JsonMemory:IDataMemory,是空的,就不加了 => 不是空的,因为用了拓展DataMgr的方式来操作
public interface IStorage : IUtility { //老版的定义 //void SaveInt(string key, int value); //int LoadInt(string key, int defaultValue = 0); //void SaveString(string key, string value); //string LoadString(string key, string defaultValue = ""); //void SaveFloat(string key, float value); //float LoadFloat(string key, float defaultValue = 0.0f); T Get(string key); void Set(string key, T value); object Getobject(string key); void Setobject(string key, object value); void Clear(string key); void ClearAll(); bool ContainsKey(string key); }
02 接口方法多样,PlayerPrefs+Json
后面发现PlayerPrefs+Json两种方式、StorageUtil。这三者对IStorage 中的方法是杂着来的。
比如这四个其实是PlayerPrefs+Json共用的,适合放在StorageUtil(后面改成StorageSystem)中
T Get(string key); void Set(string key, T value); object Getobject(string key); void Setobject(string key, object value);
而这三个,PlayerPrefs+Json实现的方法是不同的,所以不能都:IStorage,所以要将IStorage 拆掉。
void Clear(string key); void ClearAll(); bool ContainsKey(string key);
完成类似这种效果
public interface IContainsKey { bool ContainsKey(string key); } public interface IClear { void Clear(string key); } public interface IClearAll { void ClearAll(); } public interface ISet { void Set(string key, T value); } public class PlayerPrefsStorage : IClear,IClearAll,IContainsKey { #region 实现 public void Clear(string key) { PlayerPrefs.DeleteKey(key); } public void ClearAll() { PlayerPrefs.DeleteAll(); } public bool ContainsKey(string key) { bool has = PlayerPrefs.HasKey(key); return has; } #endregion }
03 Json处理
原本采用this的拓展方法,this了PlayerPrefsMemory中的字典。两个子集怎么能交叉呢?所以索性把相关操作提高到StorageSystem。JsonMemory(实际是DataUtil(拓展的方式操作Json),因为JsonMemory里面啥都没有)改成接口
public interface ISetJsonData { void SetJsonData(string key, JsonData value); }
04 StorageSystem
不行,因为Util(System不能在Model中使用,不用Systm就没有Init重写),所以又改回来
05 StorageUtil
public class StorageUitl : IStorageUtil { #region 字属 private static readonly Dictionary _defaultValues = new Dictionary { {typeof(int), default(int)}, {typeof(string), ""}, {typeof(float), default(float)} }; //这两个原来是readonly,但我需要在OnInit中体现出初始赋值的过程(有可能别的初始读取方式),所以改了static // 但是因为Util(System不能在Model中使用,不用Systm就没有Init重写),所以又改回来 private readonly Dictionary _dataGetter = new Dictionary { {typeof(int), key => PlayerPrefs.GetInt(key, (int) _defaultValues[typeof(int)])}, {typeof(string), key => PlayerPrefs.GetString(key, (string) _defaultValues[typeof(string)])}, {typeof(float), key => PlayerPrefs.GetFloat(key, (float) _defaultValues[typeof(float)])} }; private readonly Dictionary _dataSetter = new Dictionary { {typeof(int), (key, value) => PlayerPrefs.SetInt(key, (int) value)}, {typeof(string), (key, value) => PlayerPrefs.SetString(key, (string) value)}, {typeof(float), (key, value) => PlayerPrefs.SetFloat(key, (float) value)} }; private string _className = "PlayerPrefsMemory"; PlayerPrefsStorage _pp=new PlayerPrefsStorage(); //JsonStorage _json; //采用接口的方式 #endregion #region 实现 public T Get(string key) //0level { var type = typeof(T); var td = TypeDescriptor.GetConverter(type); if (_dataGetter.ContainsKey(type)) { //根据字符串找类型 return (T)td.ConvertTo(_dataGetter[type](key), type);//0 } Debug.LogError(_className + "中无此类型数据,类型名:" + typeof(T).Name); return default(T); } public object Getobject(string key) { if (ContainsKey(key)) { foreach (var pair in _dataGetter) { var value = pair.Value(key); if (!value.Equals(_defaultValues[pair.Key])) { return value; } } } else { //Debug.Log(_className + "内不包含对于键值(所以改数据会转Json):" + key); } return null; } public void Set(string key, T value) { var type = typeof(T); if (_dataSetter.ContainsKey(type)) _dataSetter[type](key, value); //0level,0 else Debug.LogError(_className + "中无此类型数据,数据为 key:" + key + " value:" + value); } public void Setobject(string key, object value) { var success = false; foreach (var pair in _dataSetter) { if (value.GetType() == pair.Key) { pair.Value(key, value); success = true; } } if (!success) { Debug.LogError(_className + "未找到当前值的类型,赋值失败,value:" + value); } } public void Clear(string key) { _pp.Clear(key); // _json.Clear(key); } public void ClearAll() { _pp.ClearAll(); //_json.ClearAll(); } public bool ContainsKey(string key) { return _pp.ContainsKey(key);//|| _json.ContainsKey(key); } #endregion #region 实现 ISetJsonData public void SetJsonData(string key, JsonData data) { #region 说明 /** { "planes": [ { "planeId": 0, "level": 0, "attackTime":1, "attack": { "name":"攻击","value":5,"cost":200,"costUnit":"star","grouth":10,"maxVaue": 500}, "fireRate": { "name":"攻速","value":80,"cost":200,"costUnit":"star","grouth":1,"maxVaue": 100}, "life": { "name":"生命","value":100,"cost":200,"costUnit":"star","grouth":50,"maxVaue": 1000}, "upgrades": { "name":"升级","coefficient": 2,"max":4,"0": 100,"1": 200,"2": 300,"3": 400,"costUnit":"diamond"} }, ...... ], "planeSpeed": 1.2 } **/ #endregion //key=0level,0attackTime(0就是planeId) IJsonWrapper jsonWrapper = data; switch (data.GetJsonType()) { case JsonType.None: Debug.Log("当前jsondata数据为空"); break; case JsonType.Object: SetObjectData(key, data); break; case JsonType.String: Set(key, jsonWrapper.GetString()); break; case JsonType.Int: Set(key, jsonWrapper.GetInt()); //0level ,0 break; case JsonType.Long: Set(key, (int)jsonWrapper.GetLong()); break; case JsonType.Double: Set(key, (float)jsonWrapper.GetDouble()); break; default: throw new ArgumentOutOfRangeException(); } } private void SetObjectData(string oldkey, JsonData data) { foreach (var key in data.Keys) { var newKey = oldkey + key; if (!ContainsKey(newKey)) { SetJsonData(newKey, data[key]); } } } #endregion }
modify ConfigMgr => ConfigSystem
ConfigMgr有初始化操作
modify ReadMgr => ReadUtil
IReader,JsonReader,ReaderConfig都放在一个文件上
ReadMgr没有初始化操作,所以改改成ReadUtil
public interface IReaderUtil : QFramework.IUtility { public IReader GetReader(string path); } public class ReaderUtil : IReaderUtil { private readonly Dictionary_readerDic = new Dictionary(); /// 通过路径获取一个填充好数据Configd(json)的IReader public IReader GetReader(string path) { IReader reader = null; if (_readerDic.ContainsKey(path)) { reader = _readerDic[path]; } else { reader = ReaderConfig.GetReader(path); LoadMgr.Single.LoadConfig(path, reader.SetData); if (reader != null) { _readerDic[path] = reader; } else { Debug.LogError("ReaderMgr未获取到对应reader,路径:" + path); } } return reader; } }
modify LoadMgr => LoadSystem
有初始化,所以用LoadSystem
modify ItemFactory改成ItemFactoryUtil
modify GameUtil改成GameUtil:IGameUtil
GameUtil太长就不贴了。改这玩意,一是有什么方法在接口中看得清楚;而是是在启动中能看得清楚(都Register在一起了)
。。。
主要就是去掉方法的static
public interface IGameUtil : QFramework.IUtility { public Vector2 GetCameraSize(); public Vector2 GetCameraMin(); public Vector2 GetCameraMax(); public Camera GetCamera(); public SubMsgMgr GetSubMsgMgr(Transform trans); public void ShowWarnning(); public int GetInt(object value); public List InitEnemyCreator(EnemyType type , Transform parent , AllEnemyData allEnemyData , EnemyTrajectoryDataMgr trajectoryData , LevelData levelData); public GameProcessNormalEvent GetNormalEvent(Action spawnAction , Func spawnedNum , int spawnTotalNum); public GameProcessTriggerEvent GetTriggerEvent(float prg , Action action , bool needPauseProcess , Func isEnd); }
miodify 消息MessageMgr => MessageSystem
IMessageSystem
MessageSystem:IMessageSystem
SubMsgMgr : MonoBehaviour,IMessageSystem
MessageMgr : NormalSingleton, IMessageSystem
ActionMgr
同统一改成
IMessageSystem
MessageSystem:IMessageSystem
ActionMgr
modify 消息机制底层委托的结构
private Dictionary mTypeEvents public class EasyEvent : IEasyEvent { private Action mOnEvent = () => { }; 。。。 public class EasyEvent : IEasyEvent { private Action mOnEvent = e => { };
而飞机大战用了HashSet
/// 维护了一个HashSet public class ActionMgr { #region 字属构造 /// 委托链 private HashSet _actionHs; private Action _action; public ActionMgr() { _actionHs = new HashSet(); _action = null; } #endregion public void Add(Action action) { if (_actionHs.Add(action)) { _action += action; } } public void Remove(Action action) { if (_actionHs.Remove(action)) { _action -= action; } } public void Execute(T t) { _action.DoIfNotNull(t); } public bool Contains(Action action) { return _actionHs.Contains(action); } }
modify 移动
因为觉得和KeyCode 有关,所以扔到InputSystem(原InputMgr)去了
public void AddListener(KeyCode code, KeyState state, Action callback) public void RemoveListener(KeyCode code, KeyState state, Action callback)
详细看下一条
modify InputMgr => InputSystem
有初始化
watch 先贴一下组织的
using System.Collections.Generic; using System; using UnityEngine; using QFramework; #region 接口 public interface IInputModule { void AddListener(KeyCode code); void AddMouseListener(int code); void RemoveListener(KeyCode code); void RemoveMouseListener(int code); } /// 按键与按键状态的组合key public interface IInputUtil : QFramework.IUtility { public string GetKey(KeyCode code, KeyState state); public string GetKey(int code, KeyState state); } #endregion #region InputSystem public interface IInputSystem : QFramework.ISystem, IInputModule, IInputUtil { void AddListener(KeyCode keyCode, KeyState KeyState, Action callback); void RemoveListener(KeyCode keyCode, KeyState KeyState, Action callback); } public class InputSystem : QFramework.AbstractSystem,IInputSystem, IUpdate,ICanGetSystem { private readonly InputModule _module= new InputModule(); private readonly bool _updating = false; protected override void OnInit() { _module.AddSendEvent(SendKey); _module.AddSendEvent(SendMouse); } #region pub IInputUtil public string GetKey(KeyCode code, KeyState state) { return code + state.ToString(); } public string GetKey(int code, KeyState state) { return code + state.ToString(); } #endregion #region pub IInputModule public void AddListener(KeyCode code) { _module.AddListener(code); AddUpdate(); } public void AddMouseListener(int code) { _module.AddMouseListener(code); AddUpdate(); } public void RemoveListener(KeyCode code) { _module.RemoveListener(code); RemoveUpdate(); } public void RemoveMouseListener(int code) { _module.RemoveMouseListener(code); RemoveUpdate(); } #endregion #region pub InputSystem public void AddListener(KeyCode keyCode, KeyState keyState, Action callback) { var key = GetKey(keyCode, keyState); this.GetSystem().AddListener(key, callback); } public void RemoveListener(KeyCode keyCode, KeyState keyState, Action callback) { var key = GetKey(keyCode, keyState); this.GetSystem().RemoveListener(key, callback); } #endregion #region pub IUpdate public int Timing { get; set; } public int Time { get; } public void UpdateFunc() { _module.Execute(); } #endregion #region pri private void SendKey(KeyCode code, KeyState state) { this.GetSystem().DispatchMsg(GetKey(code, state), state); } private void SendMouse(int code, KeyState state) { this.GetSystem().DispatchMsg(GetKey(code, state), state); } private void AddUpdate() { if (!_updating) LifeCycleMgr.Single.Add(LifeName.UPDATE, this); } private void RemoveUpdate() { if (_module.ListenerCount == 0) LifeCycleMgr.Single.Remove(LifeName.UPDATE, this); } #endregion } #endregion #region InputModule public class InputModule : IInputModule { #region 字属构造 private readonly Dictionary _keyCodeDic; private readonly Dictionary _mouseDic; private Action _keyEvent; private Action _mouseEvent; public InputModule() { _keyCodeDic = new Dictionary(); _mouseDic = new Dictionary(); } public int ListenerCount { get { if (_keyCodeDic == null || _mouseDic == null) return 0; return _keyCodeDic.Count + _mouseDic.Count; } } #endregion #region 实现 public void AddListener(KeyCode code) { if (_keyCodeDic.ContainsKey(code)) _keyCodeDic[code] += 1; else _keyCodeDic.Add(code, 1); } public void AddMouseListener(int code) { if (_mouseDic.ContainsKey(code)) _mouseDic[code] += 1; else _mouseDic.Add(code, 1); } public void RemoveListener(KeyCode code) { if (_keyCodeDic.ContainsKey(code)) { _keyCodeDic[code] -= 1; if (_keyCodeDic[code] Debug.LogError("当前移除指令并没有被监听,Keycode:" + code); } } public void RemoveMouseListener(int code) { if (_mouseDic.ContainsKey(code)) { _mouseDic[code] -= 1; if (_mouseDic[code] Debug.LogError("当前移除指令并没有被监听,Keycode:" + code); } } #endregion #region 辅助 public void AddSendEvent(Action _keyEvent = keyEvent; } public void AddSendEvent(Action _mouseEvent = keyEvent; } public void Execute() { if (_keyEvent == null || _mouseEvent == null) { Debug.LogError("输入监听模块发送消息事件不能为空"); return; } foreach (var pair in _keyCodeDic) { if (Input.GetKeyDown(pair.Key)) _keyEvent(pair.Key, KeyState.DOWN); if (Input.GetKey(pair.Key)) _keyEvent(pair.Key, KeyState.PREE); if (Input.GetKeyUp(pair.Key)) _keyEvent(pair.Key, KeyState.UP); } foreach (var pair in _mouseDic) { if (Input.GetMouseButtonDown(pair.Key)) _mouseEvent(pair.Key, KeyState.DOWN); if (Input.GetMouseButton(pair.Key)) _mouseEvent(pair.Key, KeyState.PREE); if (Input.GetMouseButtonUp(pair.Key)) _mouseEvent(pair.Key, KeyState.UP); } } #endregion } public enum KeyState { DOWN, /// CoroutineSystem(还有Delay版的)类似于这样改,不展示全部
#region CoroutineSystem public interface ICoroutineSystem :QFramework.ISystem { int ExecuteOnce(IEnumerator routine); void Delay(float time, Action callBack); void Restart(int id); void StartExecute(int id); void Pause(int id); void Continue(int id); void Stop(int id); } /// 维护了两个同类型字典,一个跑一次,一个存起来 /// Dictionary <int, CoroutineController > /// public class CoroutineSystem : QFramework.AbstractSystem,ICoroutineSystem {modify LifeCycleMgr => LifeCycleSystem
bug QFramework.AbstractSystem与MonoBehaviour
不能同时:QFramework.AbstractSystem与MonoBehaviour,选择:QFramework.AbstractSystem,然后添加一个MonoBehaviour
watch LifeCycleSystem
using QFramework; using QFramework.AirCombat; using System; using System.Collections.Generic; using UnityEngine; #region LifeCycleSystem public interface ILifeCycleSystem : QFramework.ISystem { void Add(LifeName name, object o); void Remove(LifeName name, object o); void RemoveAll(object o); } /// 有Json数据和PlayerPrefers数据的初始化 public class LifeCycleSystem : QFramework.AbstractSystem, ILifeCycleSystem { private LifeCycleSystemMono _mono; protected override void OnInit() { var cfg = new LifeCycleAddConfig(); cfg.Init(); Add2LifeCycleConfig(cfg); LifeCycleConfig.Do(LifeName.INIT); // Transform t = Camera.main.transform.FindTopOrNewPath(GameObjectPath.System_LifeCycleSystem); _mono= t.GetOrAddComponent(); _mono.DoUpdate(Update); } #region 生命 void Update() { if (this.GetModel().E_GameState == GameState.PAUSE ) { return; } LifeCycleConfig.Do(LifeName.UPDATE); } #endregion #region pub ILifeCycleSystem public void Add(LifeName name, object o) { LifeCycleConfig.Add(name, o); } public void Remove(LifeName name, object o) { LifeCycleConfig.Remove(name, o); } public void RemoveAll(object o) { LifeCycleConfig.RemoveAll( o); } #endregion #region pri private void Add2LifeCycleConfig(LifeCycleAddConfig cfg) { if (true)//尝试私有化 LifeCycleConfig.LifeCycleDic的写法 { foreach (object o in cfg.LifeCycleArrayLst) { //TODD :LifeCycleMgr,不确定这样改会不会报错 LifeCycleConfig.Add(o); } }else { foreach (var o in cfg.LifeCycleArrayLst) { foreach (var cycle in LifeCycleConfig.LifeCycleFuncDic) { // if (cycle.Value.Add(o)) { break; } } } } } #endregion #region 重写 public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } #endregion } #endregion #region ILifeCycle public interface ILifeCycle { bool NeedAdd(object obj); void Remove(object obj); void Execute(Action execute); } /// 维护一个List<object> public class LifeCycle : ILifeCycle { private readonly List _objLst = new List(); public bool NeedAdd(object o) { if (o is T) { if (_objLst.Contains(o)) { return false; } else { _objLst.Add(o); return true; } } return false; } public void Remove(object o) { _objLst.Remove(o); } public void Execute(Action execute) { for (int i = 0; imodify GuideMgr => GuideSystem
stars FindTopOrNewPath
Camera.main基本都有
DoUpdate是UniRx的
跑通一次,就不多试了
Transform t = Camera.main.transform.FindTopOrNewPath(GameObjectPath.System_LifeCycleSystem); _mono= t.GetOrAddComponent(); _mono.DoUpdate(Update);public static MonoBehaviour DoUpdate(this MonoBehaviour mono,Action action) { Observable .EveryUpdate() .Subscribe(_ => action()) .AddTo(mono) .DisposeWhenGameObjectDestroyed(mono); return mono; }/// A/B/C => A B(父节点A) C(父节点B) 。返回了C public static Transform FindTopOrNewPath(this Transform t,string path) { string topName = path.TrimName(TrimNameType.SlashFirst);//A/B/C => A Transform top = t.FindTop(topName); if (top.IsNullObject())// return (UnityEngine.Object)obj == null; { top = new GameObject(topName).transform; } string[] names=path.Split('/');//A/B/C => A B C Transform[] ts = new Transform[names.Length]; ts[0]=top; for (int i = 1; imodify UIManager => UISystem
主要看接口中属性的使用(Canvas)
public interface IUISystem : QFramework.ISystem { Transform Canvas { get; set; } IView Show(string tarPath); void Back(); void Hide(string name); Transform GetViwePrefab(string path); Transform GetCurrentViewPrefab(); DialogView ShowDialog(string content , Action trueAction = null , Action falseAcion = null); } public class UISystem : AbstractSystem, IUISystem { #region 字属 ...... Transform IUISystem.Canvas { get { return _canvas; } set { _canvas = value; } } Transform _canvas { get; set; } ......star SimpleSingleton
/// 为空就会New() public class SimpleSingleton where T : new() { protected static T _single; public static T Single { get { if (_single == null) { var t = new T(); _single = t; } return _single; } } }GuideDataMgr => GuideStorageUtil
因为是存储
public interface IGetBool { bool GetBool(T key); } /// Key值,Value值,两个Equals public interface ISetIntKEV { void SetInt(int value); } #region GuideStorageUtil public interface IGuideStorageUtil : QFramework.IUtility,IGetBool,ISetIntKEV { } public class GuideStorageUtil : IGuideStorageUtil { public void SaveData(int key, bool value = true) { PlayerPrefs.SetInt(key.ToString(), Convert.ToInt32(value)); } public bool GetBool(T key) { int result = PlayerPrefs.GetInt(key.ToString(), Convert.ToInt32(false)); return Convert.ToBoolean(result) ; } public void SetInt( int kv) { PlayerPrefs.SetInt(kv.ToString(), Convert.ToInt32(kv)); } } #endregionmodify PathMgr
一开始想把Path做成Pool。
因为IPath中的方法都有了,没必要再用PathMgr来套上一层吧。
。。。
但Path的具体实现才是Spawn的对象。
比如一个直线阵列的Path被回收后,第二次不能Spawn为W阵列的Path。
IPath是接口。PathBase是抽象类。都不能被实例。
。。。
考虑改名,就一个IPath,好意思带Mgr后缀。
改名
Unit,部门,单元,不怎么合适
One,直观但不雅
Ctrl,占用了:QFramework.IController
noun,概念,不准确
IPath是一个移动阵列,根据接口有类型,方向,成员初始位置
Wave,一波敌人
class SumPath { IPath iPath; PathBase pathBase; StraightPath straightPath; WPath wPath; StayOnTopPath pathOnTopPath; EllipsePath ellipsePath; } /// /// 路径接口,提供具体的路径的计算方法 /// public interface IPath { void Init(Transform trans,ITrajectoryData trajectory); Vector3 GetInitPos(int id); Vector2 GetDir(); bool FollowCamera(); } public abstract class PathBase : IPath { protected PathState _state; protected ITrajectoryCalc _trajectoryCalc; protected Transform _trans; public virtual void Init(Transform trans, ITrajectoryData trajectoryData) { _trans = trans; } public abstract Vector3 GetInitPos(int id); public abstract Vector2 GetDir(); public abstract bool FollowCamera(); } #region Path : PathBase (具体实现)modify TrajectoryData改名
TrajectoryData相关,改成PathData,与IPath,PathBase对应统一
TrajectoryData相关,
有ITrajectoryData,实例类:ITrajectoryData
有EnemyTrajectoryDataMgr
watch 类的静态方法 静态类的静态方法
----------------------------------------------
LoadCreatorData => ILoadCreatorDataSystem
modify PlaneEnemyCreator、MissileEnemyCreator隔离MonoBehaviour
为了管理OnDestroy(原本LifeName就有Init,Upadte)。
如果加上Destroy,就能隔离需要OnDestroy的类
modify GameUtil的一些方法转Command
有一些是Camera的
有一些是初始Plane数据的,觉得不搭,而且以后会混杂,所以转成Command
InitEnemyCreatorLstCommand
public class InitEnemyCreatorLstCommand : AbstractCommand { EnemyType enemyType; AllEnemyData allEnemyData; PathDataMgr pathDataMgr; LevelData levelData; public InitEnemyCreatorLstCommand(EnemyType enemyType, AllEnemyData allEnemyData, PathDataMgr pathDataMgr, LevelData levelData) { this.enemyType = enemyType; this.allEnemyData = allEnemyData; this.pathDataMgr = pathDataMgr; this.levelData = levelData; } protected override List OnExecute() { List list = new List(); foreach (PlaneCreatorData data in levelData.PlaneCreaterDatas) //这里可以到LoadCreaterData这看 { if (data.Type == enemyType) //ever error;data都是normal { list.Add(SpawnCreator(data, allEnemyData, pathDataMgr)); } //Debug.Log($"data.EnemyType == enemyType=>{data.Type}=={enemyType}"); } if (list.IsNotNull() && list.Count > 0) { return list; } else { throw new System.Exception($"Creater初始化失败:{enemyType}"); } } private IEnemyCreator SpawnCreator( PlaneCreatorData data , AllEnemyData allEnemyData , PathDataMgr trajectoryData) { var creater = new PlaneEnemyCreator(); creater.Init(data, allEnemyData, trajectoryData); return creater; } }modify :MonoBehaviour的后缀又是Mgr,改成Component
避免混乱
modify PoolSystem SpawnPlaneSystem
bug 敌人初始位置不准
位置初始看带队的第一架飞机,对PathMgr 的初始化。
public class PlaneEnemyView : PlaneView,IUpdate ,ICanGetSystem { private PathMgr _pathMgr;所以不会将PathMgr 放在PlaneEnemyView ,所以创建它的 PlaneEnemyCreator
放在这里。
/// 在PlaneEnemyCreator中有 private PlaneEnemyView InitPlaneEnemyView(int posIdxInRange, IPathData pathData) { var plane = this.GetSystem().Spawn(ResourcesPath.PREFAB_PLANE); // 需要 plane if (posIdxInRange == 0) { _pathMgr = new PathMgr(); _pathMgr.Init(plane.transform, _enemyData, pathData); } // 需要_pathMgr var view = plane.GetOrAddComponent(); view.Init(posIdxInRange, _enemyType, _enemyData, _sprite, pathData,_pathMgr); return view; }原来的放在这里。这里挺迷的,带队的将自身传进去,后面的算位移偏差就行了。
没必要多new几个PathMgr
public class PlaneEnemyCreator : ...... { private void InitComponent(EnemyData data,EnemyType type, Sprite sprite, ITrajectoryData trajectoryData) { ...... //路径初始化 _path = new PathMgr(); _path.Init(transform,data,trajectoryData);bug 队伍生成间隔时间段太快
原版的是
01 撞机直接死
02 每队伍第一架飞机接触屏幕底部后,就会生成另外一队
bug 玩家子弹的结束位置不对
判断条件的问题
之前是_sR.bounds.max.y,所以那样
if (_dir == Vector2.up) return _sR.bounds.min.y Camera _camera; Vector2 _dir; SpriteRenderer _sR; public InCameraBorderCommand(Transform t, Vector2 dir) { _camera = t.MainOrOtherCamera(); _sR = t.gameObject.GetComponent if (_dir == Vector2.up) return _sR.bounds.min.y _destroyCase.Injure(bullet.GetAttack())) ;_destroyCase.DoIfNotNull(_destroyCase.Dead);
位置是PlaneCollideMsgComponent
。。。
意思是_destroyCase!=null,就执行后面括号里面的Action。主要是第一句 _destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ;的写法,要加() => ,表示这是一个Func,而不只是object
。。。
对比第二句,第二句应该是C#的语法默认设置,省事 () =>方法()
/// o不为空,就执行cb public static object DoIfNotNull(this object o,Action cb) { if (o != null) { cb(); } return o; }出处
public void ColliderMsg(Transform other) { var bullet = other.GetComponentInChildren(); if (other.tag == Tags.BULLET && bullet != null && _selfBullet != null && bullet.ToTags.Contains(_selfBullet.From) //BulletComponent ) { _destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ; } else if (_selfBullet != null && _selfBullet.ContainsTo(other.tag)) { _destroyCase.DoIfNotNull(_destroyCase.Dead); }star Character Injure
/**************************************************** 文件:Test_ExtendCharacter.cs 作者:lenovo 邮箱: 日期:2024/1/10 17:19:6 功能: *****************************************************/ using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; namespace Demo00_00 { public class Test_ExtendCharacter : MonoBehaviour { #region 属性 [SerializeField] float _curLife; [SerializeField] float _change; [SerializeField] Slider _slider; #endregion /// 首次载入 void Awake() { _slider=GetComponentInChildren(); _slider.onValueChanged.AddListener(Injure); } void Injure(float change) { _change = change; _curLife.Injure(_change, () => Debug.Log("受伤"), () => Debug.Log("死亡")); } } }public static int Injure(ref this int cur, int change, Action injureAction, Action deadAction) { if (cur cur = 0; return cur; } var after = cur - change; if (after after = 0; deadAction(); } else { injureAction(); } cur = after; return cur; } GameObject[] gos = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects(); foreach (GameObject go in gos) { if (go.name == tarName) { return go.transform; } } return null; } public static Transform FindTop(this Transform t, string tarName) { GameObject[] gos = GameObject.FindObjectsOfType if (go.name == tarName) { return go.transform; } } return null; } //EnemyData data = os[0] as EnemyData;//没用 //EnemyData data = (EnemyData)os[0] ; //没用 if (_enemyData.IsNull()) { // Debug.LogError("异常:EnemyData值:" + os[0].ToString()); } else { //_enemyData = data; //_lifeComponent = gameObject.GetOrAddComponent public int id; public double attackTime; public int attack; public double fireRate; public int life; public double speed; // public TrajectoryType trajectoryType; /// string str = ""; str += "\t" + id; str += "\t" + attack; str += "\t" + life; str += "\t" + trajectoryID; str += "\t" + starNum; str += "\t" + score; str += "\t" + itemCount; str += "\t" + attackTime; str += "\t" + fireRate; str += "\t" + speed; str += "\t" + trajectoryType.ToString(); str += "\t"; foreach (var item in bulletType) { str += item.ToString() + ","; } str += "\t"; foreach (var item in itemRange) { str += item.ToString() + ","; } return str; } } if (name == GameAudio.Null) { return; } AudioKit.PlaySound($"{_path}{name}"); } return; } GameObject go = gameObject; go.GetOrAddComponent private IEffectView _effectView; private void CollideEvent() { AudioMgr.Single.PlayOnce(GetGameAudio().ToString()); _effectView.Stop(ItemLogic); } protected virtual void ItemLogic() { Destroy(gameObject); } private IEffectView _effectView; protected override void InitComponent() { gameObject.GetOrAddComponent _cur = null; foreach (IEnemyCreater tmp in list) { if (_cur == null || _cur.GetSpawnRatio() tmp.GetSpawnRatio()) { if (!tmp.IsSpawning()) { _cur = tmp; } } } return _cur; } foreach (IEnemyCreator tmp in list) { bool changeCreator = false;//是否切换当前的Creator //空或有小的prg都换,写这么长是为了方便打点 if (curCreator != null && curCreator!=tmp)//不能自己和自己做对比 { changeCreator = curCreator.GetSpawningPrg() tmp.GetSpawningPrg(); } else if (curCreator == null) { changeCreator = true; } else { changeCreator = false; } if (changeCreator) //换了 { if (!tmp.IsSpawning())//正在 { curCreator = tmp; return curCreator; } } } return curCreator;//没换 }bug unity脚本不显示图标
有的:Mono脚本不能放在一起,出问题
watch EllipseTrajectory
椭圆,玩家子弹升级后的样式
bug 一队生成两个后马上生成第二队,也是两个
OutTop的原因,顶部不算进超范围
出现原因是子弹也需要这个组件, 所以需要把它改成可选项
public class AutoDespawnOtherCollideCameraBorderCommand : AbstractCommand { /// 回收到pool的key,这里用path string _poolKey; /// 因为一般以图片消失视觉最近人 SpriteRenderer _sr; /// 需要despawn的物体 Transform _despawnTrans; /// 测试用到,不用传参 Dir _dir; public AutoDespawnOtherCollideCameraBorderCommand(string path, SpriteRenderer sr, Transform despawnTrans) { _poolKey = path; _sr = sr; _despawnTrans = despawnTrans; } protected override void OnExecute() { if (JudgeBeyondBorder()) { //this.GetSystem().DespawnWhileKeyIsName(_despawnTrans.gameObject, _poolKey) ; this.GetSystem().Despawn(_despawnTrans.gameObject, _poolKey); } } #region pri private bool JudgeBeyondBorder() { if (_sr.IsNull()) { return true; } //判断底边界限 //因为都是运动的,有偏差 // if (OutTop()) { _dir = Dir.TOP; return true; } //这条没有 if (OutBottom()) { _dir = Dir.BOTTOM; return true; } if (OutLeft()) { _dir = Dir.LEFT; return true; } if (OutRight()) { _dir = Dir.RIGHT; return true; } return false; } bool OutLeft() { float x1 = _sr.BoundsMinX(); float x2 = this.GetUtility().GetCameraMinPoint().x; if (x1 x2) { //Debug.Log(x1 + "," + x2); return true; } return false; } bool OutTop() { float y1 = _sr.BoundsMinY(); float y2 = this.GetUtility().GetCameraMaxPoint().y; if (y1 > y2) { //Debug.Log(y1+","+y2); return true; } return false; } bool OutBottom() { float y1 = _sr.BoundsMaxY(); float y2 = this.GetUtility().GetCameraMinPoint().y; if (y1bug 两个Normal敌人Creator生成的飞机位置靠中间
PathMgr的初始化问题,它是每个敌人都会new一个,不是队伍层级的,所以我注释还有"leaderPlane"等的遗留注释
主要是这一块.竖屏,所以可以用creatorPos的x值,y需要跟随相机变化,这样就设置了飞机的初始位置
enemyTrans.SetPosX(creatorPos.x); //x需要creator传过来,不动的的,所以不能取y enemyTrans.SetPosY( GetY(enemyTrans) ); //y需要跟随相机的移动(竖屏),加上一点点偏移/// 管理一架飞机,路径的初始位置,方向等 public class PathMgr :ICanGetUtility ,ICanSendQuery { #region 字属构造 private IPath _path; /// /// 根据飞机的初始位置,生成相应的pathMgr /// 所以需要全部生成完再移动,这样避免第一种情况 /// public PathMgr(Transform enemyTrans, EnemyData enemyData, IPathData pathData,Vector3 creatorPos) { //以下是顶部左右两个creator的straight轨迹的飞机 enemyTrans.SetPosX(creatorPos.x); //x需要creator传过来,不动的的,所以不能取y enemyTrans.SetPosY( GetY(enemyTrans) ); //y需要跟随相机的移动(竖屏),加上一点点偏移 _path = PathFactory.GetPath(enemyData.trajectoryType) ; _path.Init(enemyTrans, pathData);//这里trans穿进去了 } #endregion /// leaderPlane的出场位置 /// 需要Sprite // public void InitComponentEnemy(Transform enemyTrans, EnemyData enemyData, IPathData _pathData) //{ // } float GetY(Transform enemyTrans) { float posY = this.GetUtility().GetCameraMaxPoint().y; float height = enemyTrans.GetComponent().BoundsHeight() / 2.0f; return posY+height ; }watch 敌人生成的流程
01 有两个事件(就是多少进度,回调相应方法), 大约是NormalEvent(Normal敌人的), TriggerEvent(Normal外的敌人的,火箭,Boss,精英怪)
// Normal的
02 GameProcessSystem会Update这两种Event的列表
03 在NormalEvent中, 进入条件是在场飞机数,少于5架(被击毁或者超出范围),就去触发Event中相应的Creator
04 Creator(Config中有两个Normal的),会北CreatorNMgr比较彼此的生成队伍进度(就是A生成一队,下一次B生成一队)
05 队伍正在生成就继续生成,不允许换下一队.
bug 小兵死完,只有警报声,警报界面没出现,Boss没出现
调试准备
这个看BossCreatorMgr.UpdateFunc(可以自定义帧数的Update)
调试时先注册一个调试事件(用户QF的Command)控制_start,
public void Init() { _start = false; this.RegisterEvent(_ => { _start = true; }); this.GetSystem().Add(LifeName.UPDATE, this); } public void FrameUpdate() { if (!_start) return; if (true) //if (this.GetSystem().ActiveCount(ResourcesPath.PREFAB_PLANE) == 0) { this.GetUtility().ShowWarnning(); SpawnAQueueBoss(); // this.GetSystem().Delay(Const.WAIT_BOSS_TIME, SpawnAQueueBoss); _start = false; } }bug Boss 打两下就死了
因为Boss 打两下就会去超过相机右边边界,所以就死了
边界检测自动销毁的问题,Boss所有方向的边界都不能取触发自动销毁
Boss传入的 _excludeDirs = Dir.TOP, Dir.BOTTOM, Dir.LEFT, Dir.RIGHT
public class AutoDespawnOtherCollideCameraBorderCommand : AbstractCommand { /// 回收到pool的key,这里用path string _poolKey; /// 因为一般以图片消失视觉最近人 SpriteRenderer _sr; /// 需要despawn的物体 Transform _despawnTrans; /// 有的物体的方向不需要进行自动销毁. 一般敌人忽略顶部,Bosss略全部 Dir[] _excludeDirs; Dir _dir; public AutoDespawnOtherCollideCameraBorderCommand(string path, SpriteRenderer sr, Transform despawnTrans,Dir[] excludeDirs=null) { _poolKey = path; _sr = sr; _despawnTrans = despawnTrans; _excludeDirs = excludeDirs; } protected override void OnExecute() { if (JudgeBeyondBorder()) { //this.GetSystem().DespawnWhileKeyIsName(_despawnTrans.gameObject, _poolKey) ; this.GetSystem().Despawn(_despawnTrans.gameObject, _poolKey); } } #region pri private bool JudgeBeyondBorder() { if (_sr.IsNull()) { return true; } //判断底边界限 //因为都是运动的,有偏差 if (_excludeDirs != null) { if (!_excludeDirs.Contains(Dir.TOP)) if (OutTop()) { _dir = Dir.TOP; return true; } //这条没有 if (!_excludeDirs.Contains(Dir.BOTTOM)) if (OutBottom()) { _dir = Dir.BOTTOM; return true; } if (!_excludeDirs.Contains(Dir.LEFT)) if (OutLeft()) { _dir = Dir.LEFT; return true; } if (!_excludeDirs.Contains(Dir.RIGHT)) if (OutRight()) { _dir = Dir.RIGHT; return true; } } else //一碰就死 { if (OutTop()) { _dir = Dir.TOP; return true; } //这条没有 if (OutBottom()) { _dir = Dir.BOTTOM; return true; } if (OutLeft()) { _dir = Dir.LEFT; return true; } if (OutRight()) { _dir = Dir.RIGHT; return true; } } return false; } bool OutLeft() { float x1 = _sr.BoundsMinX(); float x2 = this.GetUtility().GetCameraMinPoint().x; if (x1 x2) { //Debug.Log(x1 + "," + x2); return true; } return false; } bool OutTop() { float y1 = _sr.BoundsMinY(); float y2 = this.GetUtility().GetCameraMaxPoint().y; if (y1 > y2) { //Debug.Log(y1+","+y2); return true; } return false; } bool OutBottom() { float y1 = _sr.BoundsMaxY(); float y2 = this.GetUtility().GetCameraMinPoint().y; if (y1bug Boss没出现
触发了超过相机范围顶部的组件
上面说了,增加对边界检测的一些方向的排除
bug 警报界面没出现
初始化问题,y,scale都不正常
bug 子弹的几种bug
散乱弹
原因
子弹我写了两个脚本,BulletEnemyCtrl,BulletPlayerCtrl中的CollideMsgFromBulletComponent有的加错了,
导致一个Ctrl物体有两个CollideMsgFromBulletComponent,(此时还没写子弹不能打子弹)
触发了这个
/// 挂在Bullet的Collide public class CollideMsgFromBulletCommand : AbstractCommand { private IDespawnCase _selfDestroyCase; private IBullet _selfBullet; Transform _other; public CollideMsgFromBulletCommand(IDespawnCase destroyCase, IBullet selfBullet, Transform other) { _selfDestroyCase = destroyCase; _selfBullet = selfBullet; _other = other; } protected override void OnExecute() { IBullet otherBullet = _other.GetComponentInChildren(); //分开节点方便看,这个不是子弹是飞机上的子弹信息节点 if (_selfBullet == null)//自身不能被撞击 { return; } if (_selfBullet.Owner == otherBullet.Owner) // 01自己的子弹不能打自己 02初始位置时就触发了自己 { return; } if (_other.gameObject.CompareTag(Tags.BULLET)) //子弹不能打子弹 { return; } //只要是飞机类(),都会受子弹攻击 if ( otherBullet != null //敌人或者玩家等可以碰撞的 && otherBullet.ContainsShootBulletOwner(_selfBullet.Owner)) //伤了 { _selfDestroyCase.DoIfNotNull(() => { _selfDestroyCase.Injure(-otherBullet.GetAttack()); //扣血,所以减 }); } else if ( _selfBullet.ContainsDeadDestroyTag(_other.tag)) //死了 { _selfDestroyCase.DoIfNotNull(() => { _selfDestroyCase.Dead(); }); } } }bug 图
有的地方没敌人,可以看出子弹是不饱满的,有的地方时缺的
不动弹
IUpdate中Frame的问题
Frame不设置时默认为0,也就是每一个逻辑帧都会跑一次
设置为30时,30个逻辑帧才会跑一次
也就是如果我把子弹的Frame设置为30,同样需要把速度设置为 *30才会达到默认0的效果(实际只有速度跟得上,视觉都是散乱的)
所以Frame(原名Time),实际完整意思是 FramesPerCount(多少逻辑帧才会进行一次, 每一次逻辑会跑多少个逻辑帧)
//
解决方法就是不设置Frame,默认为0,每个逻辑帧都跑一次
bug代码
{ LifeName.UPDATE,() => { ILifeCycle life =_lifeCycleDic[LifeName.UPDATE]; life.Execute((IUpdate update) => { if (update.Framingbug 图
测试Frame=30的效果
天外弹
bug图
bug代码
bug 子弹对象池的父节点下有10个子弹是不能用到的
bug 所在
BulletPool_后面是 总数量 Active的数量
可以看到总数量为10,但实际有20个物体,
调用分析
对象池预加载的初始数量设置10(配置文件写的),我特意命名为Load
这个特意命名的方法只会在Pool初始化时调用,怀疑绷里调用两次
01 SceneConfigSystem调用了一次,就是场景记载好后的回调
02 总架构又调用一次
03 实际还有SceneConfig也调用了(但没调用SceneConfig)
导致第一次生成的物体的引用被断掉了.
所以注释掉02,03的调用
//
出现这问题的原因是战斗中的物体生成,因该是战斗时才调用,所以想弄成可调用的Init,而不是QF中实现的OnInit(注册在架构中)
效果
modify 子弹朝向
watch 朝向
做子弹朝向时(旋转角度与四元数,欧拉角的设置),有个旋转的效果挺好玩的
public void FrameUpdate() { Vector3 e = transform.rotation.eulerAngles; transform.Rotate( new Vector3(e.x, e.y, (_dir.y/_dir.x).Atan().Radian2Degree())); _moveOther.Move(_dir); }watch 最终效果反推
最左边的是20(初始弧度是-80) ,中间的是0,最右边的是-20(初始弧度是80),由此推出了下一条的效果
modify 反推的代码
使用如下,
效果是在"watch 朝向"
#region IUpdate public int Framing { get; set; } public int Frame { get; } public void FrameUpdate() { transform.FaceTo(_dir,Dir.UP); ...... } #endregion#region Face public static Transform FaceTo(this Transform t, Vector2 dir, Dir eDir) { Vector3 e = t.rotation.eulerAngles; float radian = (dir.y / dir.x).Atan();//求弧度 float degree = (radian).Radian2Degree();// 求角度 -80 (90) 80 float degreeOffset = 0; //朝向带来的偏移 switch ( eDir ) { case Dir.UP : degreeOffset = 90; break; // -80 (90) 80 => 10 0 -10 case Dir.DOWN : degreeOffset = -90; break; // -80 (-90) -100 => 0 -10 case Dir.LEFT : degreeOffset = 180; break; // 170 (180) 190 => 10 0 -10 case Dir.RIGHT : degreeOffset = 0; break; // -10 (0) 10 => 10 0 -10 default: break; } if (degreewatch 希腊字母
public static partial class ExtendGreekAlphabet { // https://baike.baidu.com/item/%CF%89/7451083?fr=ge_ala public static void ExampleGreekAlphabet() { //Debug.Log(EGreekAlphabet.α); //Debug.Log(EGreekAlphabet.β); //Debug.Log(EGreekAlphabet.Δ); //Debug.Log(EGreekAlphabet.Π); string str=""; for (int i = 0; i 测试过VS+Unity可以打印 public enum EGreekAlphabet { α, Α, alpha, 阿尔法, β, Β, beta, 贝塔, Γ, γ, gamma, 伽马, Δ, δ, delta,德尔塔 , Ε, ε, epsilon, 伊普西龙, Ζ, ζ, zeta,捷塔 , Η, η, eta, 艾塔 , Θ, θ, theta,西塔, Ι, ι, iota,伊奥塔, Κ, κ, kappa,卡帕, Λ, λ, lambda, 兰姆达, Μ, μ, mu,缪, Ν, ν, nu, 纽, Ξ, ξ, xi,克西, Ο, ο, omicron,欧米克戎, Π, π, pi,派, Ρ, ρ, rho,柔, Σ, σ, sigma, 西格玛, Τ, τ, tau,陶, Υ, υ, upsilon,宇普西龙, Φ, φ, phi,发爱, Χ, χ, chi, 开, Ψ, ψ, psi, 普西, Ω, ω, omega ,欧米伽 , COUNT } }bug 所有敌人的子弹没生成
生成位置在相机范围外,属于超出范围会被回收掉
watch 在边界外
注释掉敌人子弹的超界销毁,观察坐标变化
如下图看到y值有问题
bug 枪口位置的加减
不用看, 之前错了是因为muzzle进行了位置重置, 子节点会随着父节点旋转
bug 敌人发射得太早了,敌人一生成就发射
可以设置开始发射的条件 ( 我设置的是飞机完全相机视图 ).
2图的 object[] args 是原本设定的参数形式
watch 计算位子偏移量
/// /// 因为理解错误,敌人发生过Y值过高的bug ///
这里椭圆中心为Vector3,与muzzle的作用在外面计算 /// private Vector3[] GetPointOffsetArr(int colCnt, float boundsSizeX ,Dir muzzleDir) { if (_pointArr != null && _pointArr.Length == colCnt ) //跟之前的列数一样 .这一段决定了是初始时的坐标,所以外面要加上muzzle的实时坐标,来更新位置 { return _pointArr; } if (colCnt == 0) { throw new System.Exception("数值不能为0异常"); } _pointArr = new Vector3[colCnt]; if (colCnt == 1) { _pointArr[0] = Vector3.zero; return _pointArr; } // float xRadius = boundsSizeX / 2.0f; float yRadius = 0.3f; Ellipse ellipse = new Ellipse(xRadius, yRadius, Vector2.zero);; // float xHalf = boundsSizeX / 4; //自定义初始一行子弹的初始宽度 float xWidth = xHalf * 2; //初始一行子弹的宽度 float offset = xWidth / (colCnt-1);//2个,间隔及时全部;3个,间隔就是一半;4个,间隔就是1/3;5个,间隔就是1/4 float xMin = -xHalf -offset; // 初始一行子弹第一个的x值 ;-offset是方便后面循环,相当于第0或-1个子弹,反正就是第1的前面 float x = xMin + ellipse.Top.x; //从左到右的第一个x float y; // if (muzzleDir == Dir.DOWN) { for (int i = 0; iwatch 根据偏移量计算实际位置
/// 多少列子弹射击方向.向霰弹枪一样,发射一圈又一圈 public Vector3[] GetPointArr(int columCnt, Vector3 muzzlePos, float boundsSizeX, Dir muzzleDir) { Vector3[] posArr = GetPointOffsetArr(columCnt, boundsSizeX,muzzleDir); //更新位置 Vector3[] tempArr = new Vector3[columCnt]; for (int i = 0; iwatch 自定义的椭圆类
/// /// 椭圆 (x/a).Pow2()+(y/b).Pow2()=1 ,(k,h)=(0,0)时 /// 椭圆 ((x-k)/a).Pow2()+((y-h)/b).Pow2()=1 /// 椭圆(Ellipse)是平面内到定点F1、F2的距离之和等于常数(大于|F1F2|)的动点P的轨迹, ///
F1、F2称为椭圆的两个焦点。 ///
其数学表达式为:|PF1|+|PF2|=2a(2a>|F1F2|)。 /// public class Ellipse : ICircumference, ISquare { /** * 标准方程 * 参数方程 * 弦长公式 * 二级结论 * 焦点c, 长轴a,短轴b,半短轴d,半长轴e, 偏心率f, 离心率g, 整体椭圆系数h, 偏心系数i, 椭圆比例系数j, 椭球系数k, 焦距系数l, 圆心距离系数m, 偏心轴距离系数n, 椭球的半短轴系数o, 椭球的半长轴系数p, 偏心轴系数q, 椭球的比例系数r, 椭球的离心率系数s, 椭球的偏心率系数t, 椭球的整体椭圆系数u, 椭球的焦距系数v, 椭球的离心轴距离系数w, 椭球的圆心距离系数x, 椭球的偏心轴距离系数y, 椭球的偏心轴距离系数z。 **/ #region 本质参数 public Vector2 Pos { get; } public Vector2 Center { get { return Pos; } } /// 焦点1 小的 public Vector2 Focus1 { get { if (XHalfAxis > YHalfAxis) //焦点x轴 { return new Vector2(Center.x - (float)c, Center.y); } else if (XHalfAxis = YHalfAxis) { return YHalfAxis; } else if (XHalfAxis = YHalfAxis) { return XHalfAxis; } else if (XHalfAxis v1 = a.Pow2(); ///
v2 = 1 - ((y-h) / b).Pow2(); ///
v3 = (v1 * v2).Sqrt(); /// public double[] GetXArr(double y) { double[] xArr= new double[2]; double v1 = a.Pow2(); double v2 = 1 - ((y-h) / b).Pow2(); double v3 = (v1 * v2).Sqrt(); // xArr[0] = k - v3; xArr[1] = k + v3; return xArr; } /// /// 根据x求两个y ,先小后大 ///
v1 = b.Pow2(); ///
v2 = 1 - (( x - k) / a).Pow2(); ///
v3 = (v1*v2).Sqrt(); /// public double[] GetYArr(double x) { double[] yArr = new double[2]; double v1 = b.Pow2(); double v2 = 1 - (( x - k) / a).Pow2(); double v3 = (v1*v2).Sqrt(); //double sqrt = h+/- (b.Pow2-(x-k/a).Pow2).sqrt yArr[0] = h - v3 ; yArr[1] = h + v3 ; return yArr; } public float[] GetYArr(float x) { return GetYArr((double)x).ToFloatArray(); } public float GetYTop(float x) { return GetYArr((double)x).ToFloatArray()[1]; } public float GetYBottom(float x) { return GetYArr((double)x).ToFloatArray()[0]; } public float[] GetXArr(float y) { return GetXArr((double)y).ToFloatArray(); } public float GetXLeft(float y) { return GetXArr((double)y).ToFloatArray()[0]; } public float GetXRight(float y) { return GetXArr((double)y).ToFloatArray()[1]; } float ICircumference.Circumference() { throw new System.NotImplementedException(); } float ISquare.Square() { throw new System.NotImplementedException(); } #endregion }bug 第一轮正常,第二轮及其以后子弹过快发射
对椭圆行径的子弹位置的方法理解错误
01 最底层的方法是构造一个中心为Vector3.zero的椭圆,代入x求出y,此时的(x,y)是子弹对枪口位置的偏移量,不是实际位置,
实际位置也不该揉乱在这里
02 上一层的方法就是对枪口位置进行加减
//
如下图,以向上飞的Player为例
01 黑箭头为飞机, 飞机与椭圆的交点为 椭圆顶部的端点, 也是枪口的位置
02 矩形为子弹实例的位置的x值的取值范围(代码中取图片宽度的一半,即boundsSizeX/2.0f)
03 一列子弹,就是中间一列;
2列子弹,就在两端, 2列有1个间隔;
3列子弹,就在两端和中间, 3列有2个间隔;
同理可得,4列子弹, 就有3个间隔
以上可以得到所有x值,代入椭圆类,可以得到顶部的y值
04 x 和 y,组成了子弹偏移量
05 再上一层方法就是,加上枪口位置,相当于对椭圆进行了移动
//
watch 玩家子弹的生成位置
Top端点是枪口的位置,椭圆的x直径是飞机图片的宽度,y直径是自定义的0.6f,
设置枪口位置为muzzlePos,所以中心是 (muzzlePos.x, muzzlePos.y - 0.6/2.0f)
//
这就是这里的由来
原来的类名是EllipseTrajectory, 我新建了一个类Ellipse
Vector3 center = new Vector3(_muzzleTrans.position.x, _muzzleTrans.position.y-0.3f); Ellipse ellipse = new Ellipse(boundsSizeX/2.0f, 0.3f,center );private Vector3[] GetPointArr(int count, float boundsSizeX,BulletType bulletType) { if (_pointArr != null && _pointArr.Length == count) //分别是1列,2列,3列的设置 { return _pointArr; } else //注释一3列为标准 { _pointArr = new Vector3[count]; Vector3 center = new Vector3(_muzzleTrans.position.x, _muzzleTrans.position.y-0.3f); Ellipse ellipse = new Ellipse(boundsSizeX/2.0f, 0.3f,center ); // float offset = boundsSizeX / 4;//自定义 float minX = -offset; // 初始一行子弹第一个的x值 float validX = offset * 2; //初始一行子弹的宽度 // //int piece = count + 1; //间隔总会少一个,所以 float offsetX = validX / count; // Vector3 top = _muzzleTrans.position; float x = minX + top.x; //从左到右的第一个x float y = 0f; for (int i = 0; i效果
可以看到玩家和敌人射出子弹的 时机 和 路径 都正确了
bug muzzle被初始化
在生成位置以外修改了Transfrom,建议能放一起放一起,避免到处找
之前子弹位置出错,尝试修改加的
bug EnemyLevelData索引越界
打败Boss后CurLevel++,从0变成2
而EnemyLevelData只有2组数据,越界了
就是++调用了两次
一个MonoBehaviour的GameEvent监听了MsgEvent.EVENT_ONCE_START
自定义的GameProgress也监听了
watch& Boss的射击
拆成了几个组件
ShootCtrl一把武器,以下所有武器组件的管理者
ShootCtrls如果有多把武器
//
//武器组件
BulletSound音效
BulletLoad每轮子弹加载时间, 计时加载时间,计数剩余子弹
BulletShoot每颗子弹射击间隔,计时射击时间
BulletModel子弹相关数据,比如子弹加速度方向
BulletPointsCalcEllipse子弹初始位置,发现大部分是Ellipse椭圆,所以标识出来
射击调慢时
射击调快时
可以看到两种子弹,也就是两把枪共用一个枪口的原因
初步怀疑是两把枪
modify 尝试恢复成两把枪(设置Sprite)
1 是一轮子弹的装载量(用完需要加载,需要LoadTime来架子啊下一轮)
2 是射击速度(每隔射击时间,就射出一颗子弹)
3 是多枪口时(比如Boss, 枪口初始位置一般都是时飞机头,子弹位置有单独的BulletPathCalc组件来计算)
4 时预制体默认设置的单枪口(多枪口时不用它,可能Muzzles的局部位置用到它.其实比较麻烦)
//
这时命名为枪口Muzzle已经不合适了,比如Muzzxles节点下的第一个Muzzle,有3个射击位置,每个射击位置有2个射击方向
游戏"群星"看起来是命名为"武器接口",有大中小(SML)型武器
//
modify 后面把一把枪也整合到多把枪的代码中,这样统一点
1是单枪口
2是多枪口
watch 精英怪的W路径(找Boss撞击时看到的)
02 发现PathState
PathMgr.GetDir()
PathBase中PathState字段最接近
其中WPath:PathBase具体实现了
…
所以在IPath接口写了GetPathState()
PathBase写了virtual,返回NULL
WPath写了override
而EnterPath2Path返回NULL
public enum PathState { NULL,//自己加的 ENTER, FORWARD_MOVING, BACK_MOVING }02 WPath会使用到PathState
02 打点发现WPath是精英怪的
watch Boss的撞击(采用自定义类出现的彩蛋)
01 watch
可以观察到时直来直往的
01 整理Path相关
IPath
EnterPath:IPath, 管理IEnterPath的实现类,这个类的命名可能不是很准确(我用的名字是EnetrPath2Path)
IEnterPath,上到下,左右互到,主要是fromPos, toPos
IPathData
IPathCalc
//再次修改
IPta, 原来的IPath的部分方法,因为PathMgr也用到了
IPathBase, 原来的IPath
EnterPathMgr, 原来的EnterPath2Path
其它不变
//举例Boss描述
Boss的PathMgr,管理着EllipsePath
EllipsePath的抽象父类PathBase声明了PathState, IPathData, IPathCalc
EllipsePath引用了EnterPathMgr
EnterPath管理着各种EnterPath
01 watch Path与Trajectory名字的取用,暂时用Path
/// 无时间信息的路径 public const string Path = "Path"; /// 轨迹,路径(Path)和轨迹(Trajectory)的区别就在于,轨迹还包含了时间信息 public const string Trajectory = "Trajectory";02 显示Boss的PathMgr用到的PathName
方法一 在实现类中直接返回, 比较累, 每个实现类都要重写
方法二 各种EnterPath原来用的是接口, 所以用抽象类来写, 接口的还是接口, 减少修改
public interface IPathName { string PathName(); } public abstract class PathNameBase : IPathName { public virtual string PathName() { return this.GetType().Name; } }02 Boss的路径的变化
右边Inspector观察PathName
发现Boss显示EnterPath的Up2DownEnterPath, 到达指定位置后转EllipsePath
Ration那个是出现在屏幕中百分之几的高度
XRadius时椭圆x半轴长, yRadius是y半轴长(说是半径又不适合,半径跟圆心相关)
可以看到Precision就是在椭圆边上打多少个点
//
自己看了,索引0和19时同一个点(-1,5.3)//5.3=椭圆中心位置高度4.
03 Boss移动时抖一下的索引
03 Boss撞击的索引
撞击时索引在13,14(15)
13到14时撞过去,14到15是返回来
04 在返回来看那个椭圆边上的点的位置数组, 整体的移动
上图
红色部分的左右分别是椭圆上半圈和椭圆下半圈的y
蓝色是索引从-1到1(椭圆上半圈),再从1到-1((椭圆下半圈)的x
为什么不是y=0,因为是在屏幕的上半部分,yRatio=0.8,屏幕百分之80的高度
解释路径的算法
_data.Center.y如果采用自定义的Ellipse.Cenetr.y,中心y为5.3,Boss就会一直转圈圈
如果用 _data.Center.y( 6*0.8=4.8 )就会撞击
if ((yArr[0] - _data.Center.y).Abs()在索引i=5,最接近中心y轴,所以取小y=4.8(椭圆中心=5.3, 大y=5.8, yRadius=0.5看配置文件).
取上一个索引,i=4,y=5.8, dir=4.8-5.8=-1左右的向量y轴移动
//
在索引i=14,最接近中心y轴,没有设置就是默认为0
取上一个索引i=13,y=4.8, dir=0-4.8=-4.8左右的向量y轴移动
(可以尝试posArr[14] = new Vector3(0,-10)😉, dir=-10-4.8=-14.8左右的向量y轴移动看看效果如下
/// 图形边长的点坐标 private Vector3[] InitSidePointPosArr(EllipsePathData ellipse) { #region 数据 /** "ELLIPSE": [ { "YRatioInScreen": 0.8, "XRadius": 1, "YRadius": 0.5, "Precision": 20 } */ #endregion int precision = (ellipse.Precision).MultipleMore( 4);//一圈的精确度 20 float xLeft = _ellipse.Left.x; //x轴上的坐标分成多少份,举例,顶点为4个,x轴上坐标要被分成2份 float xTmp = xLeft;//这个循环中变量 float[] yArr; Vector3[] posArr = new Vector3[precision]; int halfPrecision = precision / 2; //上下两份,理解为半圈的精确度 10 float xOffset = (float)_ellipse.XAxis / halfPrecision; // 2/10=0.2 int symIdx = 0;//对称的索引,精确度20个点, 0对19, 1对18, 2对17 //posArr[14] = new Vector3(0,-10);//如果想尝试 for (int i = 0; i _data.Center.y)//上面的 //{ // posArr[i] = new Vector3(xTmp, yArr[0]); // posArr[symIdx] = new Vector3(xTmp, yArr[1]); //对称的 //} } xTmp += xOffset; } return posArr; }bug Boss出现后一动不动
PathMgr管理两种Path
以Boss举例,开始时U盘DownEnterPath,冲过指定位置(这是个变量和Camera相关), 就是常规的Path,Boss打点一下好像是EllipsePath
…
问题就在变量上, 所以 _toY改成方法或属性mToY .
需要改的还有Left2RightEnterPath, RIght2LeftEnterPath
public class Up2DownEnterPath : IEnterPath, ICanGetUtility { private Transform _trans; private float _halfHeight; private Vector3 _fromPos; private float _fromY; private float mToY { get { return this.GetUtility().CameraMinPoint().y + _halfHeight; } } ......watch Factory
发现用枚举来new不同对象时用Factory(静态类)命名的很多
EnetrPathFactory
PathFactory
modify PathMgr合IPath=>PathBase=>实现类有很多相似之处
#region IPath ,PathBase /// 这名字原本是IPathBase的,有部分方法PathMgr完全一样,所以再拆分组合 public interface IPath { Vector3 GetFromPos(int id); Vector2 GetDir(); PathState GetPathState(); bool FollowCamera(); } /// /// 路径接口,提供具体的路径的计算方法 /// public interface IPathBase :IPath { void Init(Vector3 startPos, SpriteRenderer sr, IPathData pathData); void Init(Transform t, IPathData pathData); }…
bug&star 情况不统一导致的枪口位置
非Boss敌人生成会进行旋转向下,枪口位置动态变化
但是Boss生成不用旋转, 图片直接就是向下的原图,那么预制体初始枪口的位置(位置稍微靠上)就有问题了
需要动态设置和回收时重置
//
localPosition是属性,直接操作不了
Y反转如下,XZ和position类似
public static Transform ReverseLocalPosY(this Transform t) { Vector3 v = t.localPosition; v.ReverseY(); t.localPosition = v; return t; } public static Vector3 ReverseY(ref this Vector3 v) { float y = v.y; v.y = -y; return v; }modify 拆分CollideMsgComponent
原来这两个脚本上合在一个脚本的
…//默认英文标点符号,方便敲代码
目的是为了Trigger2DComponent 的复用,以及节点清晰
Trigger2DComponent
/**************************************************** 文件:Trigger2DComponent.cs 作者:lenovo 邮箱: 日期:2024/4/6 19:37:56 功能: *****************************************************/ using QFramework; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using Random = UnityEngine.Random; /// 提供一个Trigger,处理外部的事件 [RequireComponent(typeof(Collider2D)) ] public class Trigger2DComponent : MonoBehaviour { public List EnterLst = new List(); //public List> ExitLst = new List>(); //public List> StayLst = new List>(); public Trigger2DComponent InitComponent(Rigidbody2D rigidbody2D) { rigidbody2D.gravityScale = 0; return this; } private void OnTriggerEnter2D(Collider2D otherCollider) { //foreach (var item in EnterLst)//这种报错 //{ // item(otherCollider); //} for (int i = 0; iCollideMsgComponent
/**************************************************** 文件:CollideMsgComponent.cs 作者:lenovo 邮箱: 日期:2024/6/10 14:33:6 功能: *****************************************************/ using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using Random = UnityEngine.Random; public class CollideMsgComponent : MonoBehaviour { Trigger2DComponent _trigger2DComponent; public CollideMsgComponent InitComponent(Trigger2DComponent trigger2DComponent) { _trigger2DComponent = trigger2DComponent; trigger2DComponent.EnterLst.Add(A); return this; } private void A(Collider2D otherCollider) { //Debug.Log(CommonClass.Log_ClassFunction() + $"\n:{otherCollider.gameObject.name}碰撞{gameObject.name}"); Transform self = _trigger2DComponent.transform; Transform other = otherCollider.transform; // List selfMsg = self.GetComponentsInChildren().ToList(); List otherMsg = otherCollider.GetComponentsInChildren().ToList(); //需要判空 selfMsg.ForEach(colliderMsg => colliderMsg.CollideMsg(other)); otherMsg.ForEach(colliderMsg => colliderMsg.CollideMsg(self)); } }star IConfig,IConfigPath
modify 使用 一个配置文件对应一个类
使用类似,这样就可以方便地找到数据模型类对应的配置文件
多个数据模型类对应一个配置文件,就用抽象类,在抽象类里面写路径
public class EnemyData :IJson { public int id; public double attackTime; public int attack; public double fireRate; ...... public string ConfigPath() { return ResourcesPath.CONFIG_ENEMY; }modify 使用 一个配置文件对应多个类 结合基类
#region ICreaterData /// LevelEnemyDataConfig public interface ICreatorData { } public abstract class CreatorDataBase : ICreatorData { public string ConfigPath() { return ResourcesPath.CONFIG_LEVEL_ENEMY_DATA; } } public class PlaneCreatorData : CreatorDataBase //json数据没乱改字段名 { ...... /// 导弹 public class MissileCreatorData : CreatorDataBase { ......star IConfig,IConfigPath
IConfig表示这个类关系配置文件,不能单独改
IConfigPath表示这个类有相应的配置文件路径
#region IConfig /// /// 啥都没有,表明这有关配置文件,不要随便改字段名 ///
或者可以加config路径方便查找。但我暂不需要 /// public interface IConfig { } public interface IJson : IConfig { } #endregion #region IConfigPath public interface IConfigPath { string ConfigPath(); } public interface IJsonPath : IConfigPath { } ......star MultipleMore MultipleLess 最接近num的factor的最倍数
public static partial class ExtendMathNumber //最小最小倍数 { /// 补足,返回factor的倍数,大于等于num的factor的最小倍数 public static int MultipleMore(this int num, int factor) { if (num 8,9=>12 } } } /// 砍掉,返回factor的倍数,小于等于num的factor的最大倍数 public static int MultipleLess(this int num, int factor) { if (num 1, (4,5)=> 1 { return 1; } else { if (num % factor == 0) { return num; } else { return (num / factor) * factor ; //(7,4)=>4,(9,4)=>8 } } } }star 一轮
/// 一轮后重新计算 /// 比如12生肖,13年后还是1 public static int Round(this int num, int round) { return num%round; } }star 向量指向
吃屎喝我自定义的枚举Dir冲突后,选择改成EDir(EnumDir的意思),这个永远不会冲突
/// 符合直觉点 public static Vector3 Dir(this Vector3 fromDir, Vector3 toDir) { return toDir - fromDir; } public static Vector3 Dir(this Vector3[] posArr, int fromIdx, int toIdx) { return posArr[toIdx] - posArr[fromIdx]; }star Camera深度模式下的宽和高,整理Camera
public static partial class ExtendCamera//深度模式下的宽和高 { /// 宽/高 public static float Aspect(this Camera camera) { return camera.aspect; } #region Size Height Width public static float OrthographicHeight(this Camera camera) { return camera.orthographicSize * 2.0f; //return (camera.OrthographicMaxPoint() - camera.OrthographicMinPoint()).y; } public static float OrthographicWidth(this Camera camera) { return camera.Aspect() * camera.OrthographicHeight(); //return (camera.OrthographicMaxPoint() - camera.OrthographicMinPoint()).x; } public static Vector2 OrthographicSize(this Camera camera) { return new Vector2(camera.OrthographicWidth(),camera.OrthographicHeight()); } #endregion #region min max Point public static Vector2 OrthographicMaxPoint(this Camera camera) { var pos = camera.transform.position; var size = camera.OrthographicSize(); var maxPoint = ExtendVector.Add(pos, size * 0.5f); return maxPoint; } public static Vector2 OrthographicMinPoint(this Camera camera) { var pos = camera.transform.position; var size = camera.OrthographicSize(); var minPoint = ExtendVector.Sub(pos, size * 0.5f); return minPoint; } #endregion }bug Boss死了但是显示游戏输了
GameProgressSystem监听属性是,不监听就默认 lose
this.GetModel().IsFinishOneLevel;找到Boss死亡的位置
GameProcessSystem监听MsgEvent.EVENT_LEVEL_END, 触发LevelEnd
所以找SendMsg的地方
//
有跳到SendMsg的地方
有跳到LevelEnd方法
bug Boss血扣了但是血条有时没变化
测试发现_minHp=0,没变化
…
猜测从对象池去除对象的血条Bar没处理好初始化,
Boss2000血,看到有50血的情况出现, 出现0的情况更多(2000血,应该是200,400, 600… 结果看都是0,0,0)
//
LifeComponent会SendMsg hp合hpMax,很可能LifeItem 的监听慢于SendMsg ,导致初始化失败,血条情况还是老的(上一次死在对象池).
01 视觉上, 所以EnemyLifeComponent .OnDisable()记得Show,不然取出的老的飞机有时会缺血块
02 数值上, 手动控制LifeItem的Init,一是没必要重复计算eachLife,二是触发一次血块(监听的初始值问题,你发了,但我还没AddListener)
modify EnemyLifeComponent的
public class EnemyLifeComponent : MonoBehaviour { private LifeComponent _lifeComponent; private int LifeItemCount { get { return transform.childCount; } } public EnemyLifeComponent Init(LifeComponent lifeComponent, SpriteRenderer sr) { _lifeComponent = lifeComponent; float tarDRealRatio = GetRatio(sr); transform.localScale *= tarDRealRatio;//缩放10个血块 transform.position= InitPos(sr); //血块Pos InitLifeItems(); return this; } private void OnDisable() { foreach (Transform trans in transform) //预制体自带了10个LifeItem { trans.Show();//这样也行,不用特意地Init,(基类操作了),Show加上去自动Show } } #region pri private void InitLifeItems() { int eachLife = _lifeComponent.LifeMax / LifeItemCount;//摘出来,防重复计算 foreach (Transform trans in transform) //预制体自带了10个LifeItem { trans.GetOrAddComponent().Init(eachLife);//这样也行,不用特意地Init,(基类操作了),Show加上去自动Show } }modify LifeItem的
public class EnemyLifeItem : SetPosZByLayerLevelView ,ICanGetSystem { [SerializeField] MessageMgrComponent _messageMgrComponent; /// 不确定InitComponent是否跑了 [SerializeField] bool _initComponent=false; [SerializeField] int _minHp=0; public override Entity2DLayer E_Entity2DLayer { get { return Entity2DLayer.EFFECT;//层级Posz在Effect上,父节点不在Effect上 } } public void Init(int eachLife) { _messageMgrComponent = transform.GetComponentInParentRecent(); _messageMgrComponent.AddListener(MsgEvent.EVENT_HP, UpdateLife); // //比如2000血分成10块,那就是200血/块 _minHp = transform.GetSiblingIndex() * eachLife;//第3节点第3块,就是600血 _initComponent = true; } ......bug Boss有时子弹打不了,也撞不死
出现在马上按"B"键生成Boss
现在不会了,恢复不到Bug时候
bug Boss的LifeItem有时看不到
将基类 transform.SePosZ(z); 改成 transform.SetLocalPosZ(z);
起码不会出现-4.2, 2.77775之类的
//
仔细看,LifeItem的父节点EnemyLifeComponent进行了缩放,
整个EnemyLfie预制体的scale也不是(1,1,1)
导致PosZ有时小于飞机的PosZ,那就被挡住了
//
LiefItem是比较特殊的, 它的祖宗节点是PLANE下的飞机, PosZ是EFFECT相同的PosZ
/// 根据层次枚举设置posZ public abstract class SetLocalPosZByLayerLevelView : GameLevelViewBase { protected override void OnEnable() { float z = E_Entity2DLayer.Enum2Int(); transform.SetLocalPosZ(z); Init(); } }bug 碰撞逻辑做得有点乱
出现在调试去掉玩家的CollideMsgFromPlaneComponent和单/多枪口整合时(枪口挂着CollideMsgFromBulletComponent)
处理好了暂且用着
modify 定义Attribute从定义到装入字典的整个过程
使用情景
使得
Dictionary
Dictionary
之类的操作更加规范方便
//
主要时尝试统合两个Attribute, 但是静态类就用不了接口
接口是暂时统合的结果
//
需要注意的是IBulletModelUtil需要在Pool之前注册,Pool生成子弹,
子弹需要BulletModel, 顺序不正确,撞击就报空了
//
两个Util, BulletModelUtil, BindPrefabUtil还是分开比较好
使用举例
using IAddTypeByAttribute = ExtendAttribute.IAddTypeByAttribute; public class CustomAttributesUtil : IInit ,ICanGetUtility { public void Init() { ExtendAttribute.InitData(this.GetUtility().Init); ExtendAttribute.InitData(this.GetUtility().Init); } #region 实现 public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } #endregion } #region BulletModelUtil public interface IBulletModelUtil :IUtility, IAddTypeByAttribute { IBulletModel GetBulletModel(BulletType type); } public class BulletModelUtil : IBulletModelUtil { private static Dictionary _bulletDic = new Dictionary(); #region IBulletUtil /// /// bulletType,类前标签 /// type ,实现类BossBullet:IBullet /// 这里字典Dic,A是attribute中的一个字段类型 /// public void Init(Attribute atb, Type type) { BulletAttribute after = atb as BulletAttribute; if (!_bulletDic.ContainsKey(after.E_BulletType)) { _bulletDic.Add(after.E_BulletType, ExtendAttribute.GetInstance(type)); } else { Debug.LogError("当前数据绑定类型存在重复,重复的类名称为:" + _bulletDic[after.E_BulletType] + "和" + type); } } public IBulletModel GetBulletModel(BulletType type) { if (_bulletDic.ContainsKey(type)) { return _bulletDic[type]; } else { Debug.LogError("BulletUtil当前未绑定对应类型的数据,类型为:" + type); return null; } } #endregion } #endregion #region BindPrefabUtil public interface IBindPrefabUtil : IUtility, IAddTypeByAttribute { List GetType(string path); } public class BindPrefabUtil : IBindPrefabUtil { private static readonly Dictionary _pathDic = new Dictionary(); private static readonly Dictionary _priorityDic = new Dictionary(); #region 辅助 /// 初始化内部字典 public void Init(Attribute atb, Type type) { BindPrefabAttribute after= atb as BindPrefabAttribute; string path = after.Path; if (!_pathDic.ContainsKey(path)) { _pathDic.Add(path, new List()); } if (!_pathDic[path].Contains(type)) { _pathDic[path].Add(type); _priorityDic.Add(type, after.Priority); _pathDic[path].Sort(new BindPriorityComparer()); } } /// 根据路径返回类型 public List GetType(string path) { if (_pathDic.ContainsKey(path)) { return _pathDic[path]; } Debug.LogError("当前数据中未包含路径:" + path); return null; } #endregion #region 内部类 /// 预制体优先级比较器 class BindPriorityComparer : IComparer { public int Compare(Type x, Type y) { if (x == null) { return 1; } if (y == null) { return -1; } return _priorityDic[x] - _priorityDic[y]; } } #endregion } #endregion复用模块
public static partial class ExtendAttribute { public interface IAddTypeByAttribute { void Init(Attribute atb, Type type); } /// T用接口,比如IBulletModel,实际是实现了接口的类 public static T GetInstance(Type type) where T : class { object instance = Activator.CreateInstance(type); if (instance is T) { return instance as T; } else { Debug.LogError($"当前绑定类未继承{typeof(T)}接口,类名为:" + type); return null; } } /// /// T xxxAttribute /// Type 系统类 /// /// /// public static void InitData(Action cb) where T : Attribute { Type[] types; types = ExtendReflection.GetTypes(); foreach (var type in types) { foreach (var attribute in Attribute.GetCustomAttributes(type, true)) { if (attribute is T) { T data = attribute as T; cb(data, type); } } } } }bug Boss死后curLevel+=2
01 一个是DeadEnemyCommand,Boss时会发送一次LevelEnd事件
02 触发了GameProgressSystem对该事件的监听,触发了LevelEnd方法
03 LevelEnd方法会修改StateModel的值,修改值又会再发送一次LevelEnd事件
04 敌人关卡数据只有两关,所以就超出范围了
//
总结是逻辑错误
修改01,直接修改StateModel中LevelState的值,让StateMOdel自己去发送LevelEnd事件
修改02,方法中不能有LevelState=LevelState.End,这都死循环了
//
下图是2秒刷新一次数据的脚本,主要 看杀死Boss后CurLevel
AirCombatInspector
/**************************************************** 文件:AirCombatInspector.cs 作者:lenovo 邮箱: 日期:2024/6/19 12:46:38 功能: *****************************************************/ using System.Collections; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; namespace QFramework.AirCombat { public class AirCombatInspector : MonoBehaviour ,IController { #region 属性 [SerializeField] float _timing = 0f; [SerializeField] IAirCombatAppModel _model; [SerializeField] IAirCombatAppStateModel _stateModel; [Header("Plane")] [SerializeField] int SelectPlaneID; /// 子弹样式 [SerializeField] int TmpPlaneLevel; [Header("Hero")] /// 实际使用的是在父节点中的索引 [SerializeField] int SelectHeroID; [Header("Game")] [SerializeField] GameState E_GameState; [Header("Level")] [SerializeField] LevelState E_LevelState; /// 实际使用的是在父节点中的索引 [SerializeField] int SelectedLevel; [SerializeField] int CurLevel; /// 已经通关数.+1就是最大的可以解锁的关卡 [SerializeField] int PassedLevel; [SerializeField] bool IsFinishOneLevel; #endregion #region 生命 /// 首次载入且Go激活 void Start() { _timing = 0f; _model = this.GetModel(); _stateModel = this.GetModel(); } /// 固定更新 void FixedUpdate() { _timing = this.Timer(_timing,2f,()=> { IsFinishOneLevel = _stateModel.IsFinishOneLevel; SelectedLevel = _stateModel.SelectedLevel; CurLevel = _stateModel.CurLevel; PassedLevel = _model.PassedLevel; E_GameState = _stateModel.E_GameState; E_LevelState = _stateModel.E_LevelState; }); } public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } #endregion } }整理GameProgressSystem两关间的切换
定义与周期:IUnRegisterList
实现:IUnRegisterList的生命
{
public List UnregisterList{ get { return _unregisterList; } }
List _unregisterList = new List();
public void DestroyFunc()
{
this.UnRegisterAll();
…
}
}
Model.Register的使用
放在GameProgressSystem的override OnInit()中
GameState.Start, End是开始战斗到死了/自行退出的周期
LevelState.Start, End是塞在GameState.Start, End的小周期
private void OnInited() { this.GetSystem().Add(LifeName.UPDATE, this); this.GetSystem().Add(LifeName.DESTROY, this); //以下都是状态改变,以及之后的回调,所以在回调中不要写条件大的状态 this.GetModel().E_GameState.Register(state => { if (state != GameState.START) return; OnGameStart(); }).AddToUnregisterList(this); this.GetModel().E_GameState.Register(state => { if (state != GameState.END) return; OnGameEnd(); }).AddToUnregisterList(this); this.GetModel().E_LevelState.Register(state => { if (state != LevelState.START) return; OnLevelStart(); }).AddToUnregisterList(this); this.GetModel().E_LevelState.Register(state => { if (state != LevelState.END) return; OnLevelEnd(); }).AddToUnregisterList(this); } #region OnStateXXX private void OnGameStart() { this.GetModel().TmpPlaneLevel.Value = this.GetModel().PlaneLevel; this.GetModel().E_LevelState.Value = LevelState.START; } /// OnLevelEnd(注意是后,方法是因变量)后会执行的 this.GetSystem StateModel.IsFinishOneLevel.Value = false; this.GetSystem ClearData(); StateModel.CurLevel.Value++; StateModel.IsFinishOneLevel.Value = true; this.GetSystem { StateModel.E_LevelState.Value = LevelState.START; }); } #endregionwatch RegisterEvent 与MessageSystem的选择
监听的增加和移除,显然QF的管理比较容易
//
01 事件的发送,左边需要自定义事件
0201 QF如果在Model根据值的不同(圈圈里面),进行Switch,也需要自定义事件
0202 QF如果在(黑色线位置),就是注册监听时,在各自的方法写
state=> if(state!=)
Msg.AddListener(MsgEvent.EVENT_GAME_START, OnGameStart); // this.GetModel().E_GameState.Register(state => { if (state != GameState.START) { return; } });star ForeachAction
测试一种场景, 没报错
private void OnTriggerEnter2D(Collider2D otherCollider) { EnterLst.ForeachAction(otherCollider); }public static void ForeachAction(this List lst, T t) { if (lst==null || lst.Count ==0) { return; } for (int i = 0; istar RR(this (float, float) os)
测试一种场景, 没报错
一开始是类内方法RR(float min,float max),不泛用.
//
用数组,列表要声明,比较麻烦,偶然发现能这样写
(float, float) 参数名
参数名.Item1, 参数名.Item2
protected override IEffect[] GetEffects(Transform transform) { ...... slowSpeed = (startSpeed > 0) ? (0.3f, 1f).RR() : (-0.3f, -1f).RR(); ...... return effects; } float RR(float min,float max) { return UnityEngine.Random.Range(min, max); }public static partial class ExtendMathNumber { public static float RR(this (float, float) os) { return UnityEngine.Random.Range(os.Item1, os.Item2); }modify 部分代码插件化
尝试失败,以后再试.
主要原因是 QF要使用Command等需要 return AirCombatApp.Interface. 不符合目的
modify 初始化顺序
bug 属性
_mono在System初始化时赋值,但是发生_mono使用时为空
改为属性
public class CoroutineSystem : QFramework.AbstractSystem,ICoroutineSystem { ...... private MonoBehaviour _mono; MonoBehaviour Mono { get { if (_mono == null) { // _mono = new MonoBehaviour(); //这种是不行,为null Transform t = this.GetSystem().GetOrAdd(GameObjectPath.System_CoroutineSystem); _mono = t.GetOrAddComponent(); //这种是不行,为null.readonly只能在和实例和构造时使用 _mono.CkeckNull(); } return _mono; } }bug 模块间顺序
01 之前直接设置GameState.Start, 游戏开始跑, 但是GameDataMgr初始化未完成,导致取AllBulletModel报错
//
01 现在改成,GameDataMgr初始化完成, 会被NSOrderSystem 监听
02 GameStateStartCommand 设置GameState.Start时用一个协程, 只有在NSOrderSystem 的相关监听属性为true时,才会设置GameState.Start
NSOrderSystem
/**************************************************** 文件:SCOrderSystem.cs 作者:lenovo 邮箱: 日期:2024/7/1 13:9:14 功能:发现很多单例,System,实例类等的依赖顺序,导致空指针 所以做这个用event来控制 即使不优雅,也算个汇总,方便以后改 // SIOrderSystem的SI, Singleton+Instance,单例类常用的两个属性名 ST,SC,static class,择用 *****************************************************/ using QFramework; using QFramework.AirCombat; using System.Collections; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; public class NSOrderSystem : NormalSingleton ,ICanRegisterEvent { public bool GameDataMgrInited ; public void Init() { GameDataMgrInited = false; this.RegisterEvent(_=> GameDataMgrInited = true) ; } public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } }GameStateStartCommand
/// 改变状态有时需要等待其他模块的加载 public class GameStateStartCommand : AbstractCommand { protected override void OnExecute() { this.GetSystem().StartInner(GameStart()); } IEnumerator GameStart() { //第一种写法 while ( !NSOrderSystem.Single.GameDataMgrInited)//等待条件 { yield return new WaitForEndOfFrame(); //等一帧 } //上面条件改变跳出后,执行后面的 this.GetModel().E_GameState.Value = GameState.START;//NULL!=START触发广播 } }watch 奖励Item
01 进行打印日志,除了Star,只出现了AddBullet
02 Item来源于
public static readonly string CONFIG_ENEMY = CONFIG_FOLDER + "/EnemyConfig.json";03 数据时 “itemRange”: [0,0], || “itemRange”: [1,1], || “itemRange”: [2,3],
04 itenRange只有一处使用地方.
结合 出货率 “itemProbability”, 下面else的默认,所以ADD_BULLET次数多,逻辑上可以说得通
private ItemType GetItemType(ItemType[] itemRange) { if (itemRange.Length == 2) { int index = ((int)itemRange[0], (int)itemRange[1] + 1).RR(); return index.Int2Enum(); } else { return ItemType.ADD_BULLET; } }05 ItemType的定义
public enum ItemType { ADD_BULLET, ADD_EXP, SHIELD, POWER }bug Star的出局
01 如果用原来的的枚举RewardType, Star得单拎出来处理, 不优雅
所以新加了EItemType, 整体见 代码一段
02 用法如下, 代码二段 (E_ItemType定义抽象类里面)
#region 说明 /// 原名ItemType,但它不处理Star(不好统一处理),所以让名,改为RewardType public enum RewardType { ADD_BULLET, ADD_EXP, SHIELD, POWER } /// 杀死敌人后的Item奖励.本来就用ItemType,弃用 public enum EItemType { STAR, /// 实际是一列变两列三列 ADD_BULLET, ADD_EXP, SHIELD, /// 原来叫POWER(改过Bomb),图片路径是Assets / Resources / Picture / Item / Power.png POWER, //TODO BOSSBULLET1 } #endregion public static class SEffectContainerFactory { public static IEffectContainer New(EItemType type) { switch (type) { case EItemType.STAR: return new StarEffectContainer();//属于必得奖励,不归ItemType管理 case EItemType.ADD_EXP: return new DefaultEffectContainer(); case EItemType.ADD_BULLET: return new DefaultEffectContainer(); case EItemType.SHIELD: return new DefaultEffectContainer(); case EItemType.POWER: return new DefaultEffectContainer(); //TODO :Boss1BulletEffectContainer // case Boss1BulletEffectContainer: return new StarEffectContainer();// 有交集,先放这里待处理 default: return new DefaultEffectContainer(); } } }public class AddExpItemView : ItemInPlaneLevelViewBase, ICanGetModel { public override EItemType E_ItemType { get { return EItemType.ADD_EXP; } } protected override IEffectContainer GetEffectContainer() { return SEffectContainerFactory.New(E_ItemType); } ......bug 奖励特效的交集
没必要分两个ADD_EXP, ADD_BULLET
/// /// 原来ADD_EXP, ADD_BULLET两个类都是一样的类体函数,参数完全没变, /// 直接统一default public class DefaultEffectContainer : EffectContainerBase { protected override IEffect[] GetEffects(Transform transform) { IEffect[] effects = new IEffect[1]; SlowSpeedEffect ySlow = new SlowSpeedEffect(); ySlow.Init(transform, Vector2.up, 0, (2f, 4f).RR(), -5f); effects[0] = ySlow; return effects; } }watch Power,Shield使用没写,就只写一个监听的数量变化
可以看UI右下角的显示和最右边的Inspector
bug Model的xxx在xxxMax的限制
比如等级不大于最高等级之类,在Model中怎么处理
方式一(没有用) 加一个赋值限制的回调
缺点:
01 一不小心用.Value赋值
02 赋值限制可能有顺序问题(就是赋值限制的回调为空时,就开始赋值)
03 污染了Model,臃肿了
//
在QF这里这里写,定义一个Func(Func). 和一个limtValue(最大值,最小值)
public class BindableProperty : IBindableProperty { ...... private Action mOnValueChanged = (v) => { }; #endregion #region pub /// /// 也就是单独的一个mValue = newValue /// 暂时不清楚目的 /// public void SetValueWithoutEvent(T newValue) { mValue = newValue; }方式二 用Command
至于详细到 change(变化量),set(变后值),要不要进行?我这里 change==set,因为c在26字母靠前,方便找
缺点:
01 一不小心用.Value赋值
public class ChangeTmpPlaneLevelCommand : AbstractCommand { int _tmpPlaneLevel; public ChangeTmpPlaneLevelCommand(int tmpPlaneLevel) { _tmpPlaneLevel = tmpPlaneLevel; } protected override void OnExecute() { int planeLevelMax = this.GetModel().PlaneLevelMax; if (_tmpPlaneLevel this.GetModel "fireRate": 1, "bulletSpeed": 5, "trajectoryType": 0, "trajectory": [ [ 90 ], [ 95, 85 ], [ 100, 90, 80 ], [ 105, 95, 85, 75 ], [ 110, 100, 90, 80, 70 ] ] }, if (Input.GetKeyDown(KeyCode.L)) { this.SendCommand(new ChangeTmpPlaneLevelCommand(_tmpPlaneLevel)); } ...... #region 属性 //[SerializeField] private bool _invincible; public bool Invincible { get = this.GetModel().Invincible; set => this.GetModel().Invincible.Value = value; } [SerializeField] private float _invincibleTimer; [SerializeField] private float _invincibleTime; [SerializeField] private SpriteRenderer _sr; /// 有点像状态机的Enter [SerializeField] private bool _onInvincibleEnter; [SerializeField] private bool _fade; #endregion #region 生命 public InvincibleComponent InitParas(params object[] os) { if (os != null && os.Length == 2) { _invincibleTime = (float)os[0]; _sr = (SpriteRenderer)os[1]; Invincible =false; _onInvincibleEnter = false; this.GetSystem().Add(LifeName.UPDATE,this); return this; } throw new System.Exception("异常:参数"); } public int Framing { get; set; } public int Frame { get; } float t2; public void FrameUpdate() { if (Invincible)//霸体就进行计时 { if (!_onInvincibleEnter) //相当OnStateEnter { //int loopCnt = 1; //float t1 = _invincibleTime ; //时间或者帧数 // t2 = (t1/ loopCnt) / 2f;//来回 _sr.DOFade(0.5f, 0.1f) ; _onInvincibleEnter = true; } // _invincibleTimer = this.Timer(_invincibleTimer, _invincibleTime, () => { //相当OnStateExit _invincibleTimer = 0f; Invincible = false; _onInvincibleEnter = false; _sr.DOFade(1f, 0.1f); }); } } #endregion public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } }CollideMsgFromPlaneCommand
/// 挂在Plane(敌人,玩家)的CollideMsg public class CollideMsgFromPlaneCommand : AbstractCommand { private IDespawnCase _destroyCase; private IBullet _selfBullet; //挂Tag的几节点 Transform _other; Transform _self; InvincibleComponent _invincibleComponent; public CollideMsgFromPlaneCommand(IDespawnCase destroyCase, IBullet selfBullet, Transform self, Transform other, InvincibleComponent invincibleComponent=null) { _destroyCase = destroyCase ?? throw new ArgumentNullException(nameof(destroyCase)); _selfBullet = selfBullet ?? throw new ArgumentNullException(nameof(selfBullet)); _other = other ?? throw new ArgumentNullException(nameof(other)); _self = other ?? throw new ArgumentNullException(nameof(self)); _invincibleComponent = invincibleComponent ; } protected override void OnExecute() { IBullet otherBullet = _other.GetComponentInChildren(); // if (_other.tag == Tags.BULLET && otherBullet != null && _selfBullet != null && otherBullet.ContainsDamageTo(_selfBullet.Owner)) //BulletEnemyCtrl//受伤 { _destroyCase.DoIfNotNull(() => { if (_self.CompareTag(Tags.PLAYER)) STest.PlayerCollidedByBulletCnt++; // STest.Injure(_destroyCase, otherBullet, _other, _invincibleComponent);//也就是以下 _destroyCase.DoIfNotNull(() => { if (_invincibleComponent != null) { //_destroyCase.Injure(-otherBullet.GetAttack()); _destroyCase.Injure(-1); _invincibleComponent.Invincible = true; } else { _destroyCase.Injure(-otherBullet.GetAttack()); } }); return; _destroyCase.Injure(-otherBullet.GetAttack()); }); } else if (_selfBullet != null && _selfBullet.ContainsDeadFrom(_other.tag)) //死亡 { if (_self.CompareTag(Tags.PLAYER)) STest.PlayerCollidedByPlaneCnt++; //STest.IsBossPlane(_other.GetComponentInChildren()); //也是以下 bool invincible = this.GetModel().Invincible; _destroyCase.DoIfNotNull(() => { if (_other.CompareTag(Tags.ENEMY))//other是敌人,所以这是玩家 { //_destroyCase.Injure(-otherBullet.GetAttack());石 _destroyCase.Injure(-1); this.GetModel().Invincible.Value = true; } else if (_other.CompareTag(Tags.PLAYER))//敌人 { if (invincible)//玩家无敌闪避,撞不死敌人 { return; } _destroyCase.Dead();//敌人直接死 } }); return;//测试太菜了,不能直接死 _destroyCase.DoIfNotNull(() => { _destroyCase.Dead(); }); } else if (_other.tag == Tags.ITEM) //Buff debuff { this.SendCommand(new CollideItemCommand(_other)); } } }star Color与Hex互转
Hex是因为unity的Color里面有十六进制,赋值起来相对于 RGB格式更加方便
public static partial class ExtendColor //16互转Color { public static Color Hex2Color(this string hex) { if (hex.Length != 7 && !hex.Contains("#")) //照理说末尾的/n应该是8 { throw new System.Exception("异常:Hex2Color"); } try { Color color; ColorUtility.TryParseHtmlString(hex, out color); //int len=hex.Length; return color; } catch (Exception) { throw new System.Exception("异常:Hex2Color"); } } /// public static string Color2Hex(this Color color) { try { string hex = ColorUtility.ToHtmlStringRGB(color); return hex; } catch (Exception) { throw new System.Exception("异常:Color2Hex"); } } }bug PlayerPrefs.DeleteAll();没有立即删除
写了一个测试脚本, this.GetUtility().ClearAll();底层是PlayerPrefs.DeleteAll();
有时停止运行再开始运行,设置了开始ClearAll(),但是值还是不变(0level, 意思是id为0的飞机的等级的key). 升级后清空还是非0级,
//
方式一 停止运行再开始运行的时间间隔长一点,可以避免这个未及时清空
方式二 或者存一个key的列表,ClearAll重写为foreach(key列表)逐个Delete
namespace QFramework.AirCombat { public class AirCombatInspectorCtrl : MonoBehaviour ,IController { [SerializeField] PlanePlayerCtrl Player; [SerializeField] bool _listenPlayer; [SerializeField] int _tmpPlaneLevel; // /// 升满级了,就不能突出飞机升级图片的变化 [SerializeField] bool _clearAllPlaterPres; public IArchitecture GetArchitecture() { return AirCombatApp.Interface; } private void Start() { { //InitpLayer之前监听 _listenPlayer = false; this.RegisterEvent(_ => _listenPlayer = true); } // if (_clearAllPlaterPres) { this.GetUtility().ClearAll(); } } ......modify 两个PlaneLevel的分清
01 一个是升级就改飞机图片,每种id的飞机四张图 id_0 id_1 id_2 id_3,所以初始等于3(按索引值计算,条件范围包括3)
02 一个时战斗时升级就加子弹列数,原来叫TmpPlaneLevel,我改成 PlaneBulletLevel和PlaneBulletLevelMax .主要看配置文件BulletConfig的数组长度,我加了一个,所以是4(也是按索引值计算,从0开始)
//
之前测试飞机选择就出错了,两个Level混一起,在等于4时,就越界了(max3)
public interface IAirCombatAppModel : IModel { ..... /// /// ,0_0 0_1 0_2 0_3, 一个id四个level图 , /// 注意和PlaneBulletLevelMax区别开来 /// BindableProperty SelectedPlaneSpriteLevelMax { get; } /// 该你来拿子弹列数的haul,玩家是12345,数组索引是01234,所以等于 BindableProperty PlaneBulletLevelMax { get; }{ "PLAYER": { "fireRate": 1, "bulletSpeed": 5, "trajectoryType": 0, "trajectory": [ [ 90 ], [ 95, 85 ], [ 100, 90, 80 ], [ 105, 95, 85, 75 ], [ 110, 100, 90, 80, 70 ] ] },bug 超出界限自动销毁在左右上的缓冲距离
//左右要多一个身位,玩家看不到再销毁
public class AutoDespawnOtherCollideCameraBorderCommand : AbstractCommand { ...... bool OutLeft() { float x1 = _sr.BoundsMinX(); float x2 = this.GetUtility().CameraMinPoint().x- _sr.BoundsSize().x; //左右要多一个身位,玩家看不到再销毁 if (x1 x2) { //Debug.Log(x1 + "," + x2); return true; } return false; } #endregion }bug 火箭没有发生Collide玩家
从PlaneEnemyCtrl赋值大部分
碰撞的结果就是那个命令 CollideMsgFromPlaneCommand, 触发IDestroyCase的Injure()或Dead()(因为测试,统一改成扣一点血)
/// 导弹 public class MissileView : PlaneLevelView,IUpdate, QFramework.IController ,ICollidePlayer { [SerializeField]private int _numOfWarning; [SerializeField]private float _eachWarningTime; [SerializeField] private Action _endAction; [Header("主体")] [SerializeField] SpriteRenderer _sr; [SerializeField]Trigger2DComponent _trigger2DComponent; [SerializeField] BulletType _bulletType; [SerializeField] IBulletModel _bulletModel; [SerializeField] string _despawnKey; [Header("Bullet")] [SerializeField] private Transform _bulletTrans; [SerializeField] BulletModelComponent _bulletModelComponent; [Header("Move")] [SerializeField] private Transform _moveTrans; [SerializeField]private float _bulletSpeed; [SerializeField]private float _cameraSpeed; [SerializeField] MoveOtherComponent _moveOther; [SerializeField]private CameraMoveSelfComponent _cameraMove; [SerializeField]private bool _startMove; [SerializeField] private Vector2 _dir; [Header("Life")] [SerializeField] private Transform _lifeTrans; [SerializeField] AutoDespawnOtherComponent _autoDespawnOtherComponent; [SerializeField] DespawnBulletComponent _despawnBulletComponent; [Header("碰撞Collider")] [SerializeField] private Transform _colliderTrans; [SerializeField] private CollideMsgComponent _collideMsgComponent; //取决将火箭导弹看成是子弹还是飞机敌人 //[SerializeField] private CollideMsgFromBulletComponent _collideMsgFromBulletComponent; [SerializeField] private CollideMsgFromPlaneComponent _collideMsgFromPlaneComponent; #region 生命 protected override void InitComponent() { GameObject go = gameObject; { _bulletType=BulletType.POWER; _despawnKey = ResourcesPath.PREFAB_ENEMY_MISSILE; _bulletModel = this.GetUtility().GetBulletModel(_bulletType); _trigger2DComponent = go.GetOrAddComponent().InitComponent(); _sr = go.GetComponentOrLogError(); } go.GetOrAddComponent(); transform.FindOrNew(GameObjectName.Collider).GetOrAddComponent().Init(CollidePlayer); //导弹自身还有另外的MoveComponent,所以不能GetOrAdd float _cameraSpeed = this.GetModel().CameraSpeed; _cameraMove = go.GetOrAddComponent().InitComponent(_cameraSpeed); // _moveTrans = transform.FindOrNew(GameObjectName.Move); { //_dir = _pathCalc.GetDir().normalized; _dir = Vector2.down; ; _moveOther = MoveOtherComponent.InitMoveComponentKeepDesption(gameObject, _moveTrans.gameObject, _moveOther, _bulletSpeed, ISpeed.SpeedDes.BULLETSPEED); } _lifeTrans = transform.FindOrNew(GameObjectName.Life); { _despawnBulletComponent = _lifeTrans.GetOrAddComponent().Init(transform, _despawnKey); _autoDespawnOtherComponent = _lifeTrans.GetOrAddComponent().Init(transform, _despawnKey, _sr); } _colliderTrans = transform.FindOrNew(GameObjectName.Collider); //自己就是子弹||自己就是飞机的一种 { _collideMsgComponent = _colliderTrans.GetOrAddComponent().InitComponent(_trigger2DComponent); //_bulletEffectComponent = _bulletTrans.GetOrAddComponent().Init(_bulletType, _bulletTrans); //_collideMsgFromBulletComponent = _colliderTrans.GetOrAddComponent().InitComponent(_bulletModel, _despawnBulletComponent); _collideMsgFromPlaneComponent = _colliderTrans.GetOrAddComponent().InitComponent(_bulletModel, _despawnBulletComponent); _bulletModelComponent = _colliderTrans.GetOrAddComponent().Init(_bulletModel); } }bug 分辨率
左右最好统一
//
因为做UI预制体,认的是左边那个
如果右边那个不是一样的,会出问题(除了锚点靠边靠四角较少问题,其它的很容易越界,翻转,大小缩放不理想)
bug Can’t remove RectTransform because Image (Script), Image (Script) depends on it
Unity报错:Destroy销毁物体失败
完结
HeroSelect
Strength强化
LevelSelect 和Loading
Battle
子弹列数的奖励(概率低,上面没吃到)