用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<AniMgr>
{
public void PlaneDestroyAni(Vector3 pos)
{
var go = PoolMgr.Single.Spawn(ResourcesPath.EFFECT_FRAME_ANI);
var view = go.GetOrAddComponent<PlaneDestroyAniView>();
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<BulletDestroyAniView>();
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<PlaneDestroyAniEvent>(OnPlaneDestroyAni);
this.RegisterEvent<BulletDestroyAniEvent>(OnBulletDestroyAni);
}
public void OnPlaneDestroyAni(PlaneDestroyAniEvent e)
{
Vector3 pos = e.pos;
var go = this.GetSystem<PoolSystem>().Spawn(ResourcesPath.EFFECT_FRAME_ANI);
var view = go.GetOrAddComponent<PlaneDestroyAniView>();
view.Init();
view.SetScale(Vector3.one * 0.5f);
view.SetPos(pos);
}
public void OnBulletDestroyAni(BulletDestroyAniEvent e)
{
Vector3 pos = e.pos;
var go = this.GetSystem<PoolSystem>().Spawn(ResourcesPath.EFFECT_FRAME_ANI);
var view = go.GetOrAddComponent<BulletDestroyAniView>();
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<int>();
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;
/// <summary>检测MsgEvent是否有重复项</summary>
public class MsgEventTest : ITest
{
public IEnumerator Execute()
{
(typeof(MsgEvent)).RepeatConstException();
yield return null;
}
}
bug QF this.GetSystem为空
注释的那句是不行的,所以在接口中要写抽象方法
ISceneSystem sys = this.GetSystem<ISceneSystem>();
//SceneSystem sys = this.GetSystem<SceneSystem>();
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<Button>();
var btnPlayGame = transform.Find("BtnPlayGame").GetComponent<Button>();
var btnPlaySound = transform.Find("BtnPlaySoundClick").GetComponent<Button>();
var btnPlayVoiceA = transform.Find("BtnPlayVoice").GetComponent<Button>();
var btnSoundOn = transform.Find("BtnSoundOn").GetComponent<Button>();
var btnSoundOff = transform.Find("BtnSoundOff").GetComponent<Button>();
var btnMusicOn = transform.Find("BtnMusicOn").GetComponent<Button>();
var btnMusicOff = transform.Find("BtnMusicOff").GetComponent<Button>();
var btnVoiceOn = transform.Find("BtnVoiceOn").GetComponent<Button>();
var btnVoiceOff = transform.Find("BtnVoiceOff").GetComponent<Button>();
var btnStopAllSound = transform.Find("BtnStopAllSounds").GetComponent<Button>();
var musicVolumeSlider = transform.Find("MusicVolume").GetComponent<Slider>();
var voiceVolumeSlider = transform.Find("VoiceVolume").GetComponent<Slider>();
var soundVolumeSlider = transform.Find("SoundVolume").GetComponent<Slider>();
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<T>(string key);
void Set<T>(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<T>(string key);
void Set<T>(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<T>(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<Type, object> _defaultValues = new Dictionary<Type, object>
{
{typeof(int), default(int)},
{typeof(string), ""},
{typeof(float), default(float)}
};
//这两个原来是readonly,但我需要在OnInit中体现出初始赋值的过程(有可能别的初始读取方式),所以改了static
// 但是因为Util(System不能在Model中使用,不用Systm就没有Init重写),所以又改回来
private readonly Dictionary<Type, Func<string, object>> _dataGetter = new Dictionary<Type, Func<string, object>>
{
{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<Type, Action<string, object>> _dataSetter = new Dictionary<Type, Action<string, object>>
{
{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<T>(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<T>(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<string, IReader>_readerDic = new Dictionary<string, IReader>();
/// <summary>通过路径获取一个填充好数据Configd(json)的IReader</summary>
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<IEnemyCreator> InitEnemyCreator(EnemyType type
, Transform parent
, AllEnemyData allEnemyData
, EnemyTrajectoryDataMgr trajectoryData
, LevelData levelData);
public GameProcessNormalEvent GetNormalEvent(Action spawnAction
, Func<int> spawnedNum
, int spawnTotalNum);
public GameProcessTriggerEvent GetTriggerEvent(float prg
, Action action
, bool needPauseProcess
, Func<bool> isEnd);
}
miodify 消息MessageMgr => MessageSystem
IMessageSystem
MessageSystem:IMessageSystem
SubMsgMgr : MonoBehaviour,IMessageSystem
MessageMgr : NormalSingleton, IMessageSystem
ActionMgr
同统一改成
IMessageSystem
MessageSystem:IMessageSystem
ActionMgr
modify 消息机制底层委托的结构
private Dictionary<Type, IEasyEvent> mTypeEvents
public class EasyEvent : IEasyEvent
{
private Action mOnEvent = () => { };
。。。
public class EasyEvent<T> : IEasyEvent
{
private Action<T> mOnEvent = e => { };
而飞机大战用了HashSet
/// <summary>维护了一个HashSet</summary>
public class ActionMgr<T>
{
#region 字属构造
/// <summary>委托链</summary>
private HashSet<Action<T>> _actionHs;
private Action<T> _action;
public ActionMgr()
{
_actionHs = new HashSet<Action<T>>();
_action = null;
}
#endregion
public void Add(Action<T> action)
{
if (_actionHs.Add(action))
{
_action += action;
}
}
public void Remove(Action<T> action)
{
if (_actionHs.Remove(action))
{
_action -= action;
}
}
public void Execute(T t)
{
_action.DoIfNotNull(t);
}
public bool Contains(Action<T> action)
{
return _actionHs.Contains(action);
}
}
modify 移动
因为觉得和KeyCode 有关,所以扔到InputSystem(原InputMgr)去了
public void AddListener(KeyCode code, KeyState state, Action<object[]> callback)
public void RemoveListener(KeyCode code, KeyState state, Action<object[]> 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);
}
/// <summary>按键与按键状态的组合key</summary>
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<object[]> callback);
void RemoveListener(KeyCode keyCode, KeyState KeyState, Action<object[]> 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<object[]> callback)
{
var key = GetKey(keyCode, keyState);
this.GetSystem<IMessageSystem>().AddListener(key, callback);
}
public void RemoveListener(KeyCode keyCode, KeyState keyState, Action<object[]> callback)
{
var key = GetKey(keyCode, keyState);
this.GetSystem<IMessageSystem>().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<IMessageSystem>().DispatchMsg(GetKey(code, state), state);
}
private void SendMouse(int code, KeyState state)
{
this.GetSystem<IMessageSystem>().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<KeyCode, int> _keyCodeDic;
private readonly Dictionary<int, int> _mouseDic;
private Action<KeyCode, KeyState> _keyEvent;
private Action<int, KeyState> _mouseEvent;
public InputModule()
{
_keyCodeDic = new Dictionary<KeyCode, int>();
_mouseDic = new Dictionary<int, int>();
}
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] <= 0) _keyCodeDic.Remove(code);
}
else
{
Debug.LogError("当前移除指令并没有被监听,Keycode:" + code);
}
}
public void RemoveMouseListener(int code)
{
if (_mouseDic.ContainsKey(code))
{
_mouseDic[code] -= 1;
if (_mouseDic[code] <= 0) _mouseDic.Remove(code);
}
else
{
Debug.LogError("当前移除指令并没有被监听,Keycode:" + code);
}
}
#endregion
#region 辅助
public void AddSendEvent(Action<KeyCode, KeyState> keyEvent)
{
_keyEvent = keyEvent;
}
public void AddSendEvent(Action<int, KeyState> keyEvent)
{
_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,
/// <summary>一直按着</summary>
PREE,
UP
}
#endregion
modify AudioMgr =>AudioSystem
那就没有了GameObject,不用设置IInitParent
用的是QF中的AudioSources
modify 按钮+音效的拓展 ExtendUtil
bug 拓展方法必须在非泛型静态类中定义
类似于这样的拓展用不了。因为要加一个点击音效的Action,音效相关不会放在拓展方法里面(太杂了,要涉及Audio管理)。
所以直接传参Transform
modify CoroutineMgr => 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);
}
/// <summary>维护了两个同类型字典,一个跑一次,一个存起来
/// <para /> Dictionary <int, CoroutineController >
/// </summary>
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);
}
/// <summary>有Json数据和PlayerPrefers数据的初始化</summary>
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<LifeCycleSystemMono>();
_mono.DoUpdate(Update);
}
#region 生命
void Update()
{
if (this.GetModel<IAirCombatAppStateModel>().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<T>(Action<T> execute);
}
/// <summary>维护一个List<object></summary>
public class LifeCycle<T> : ILifeCycle
{
private readonly List<object> _objLst = new List<object>();
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<T1>(Action<T1> execute)
{
for (int i = 0; i < _objLst.Count; i++)
{
execute((T1)_objLst[i]);
}
}
}
#endregion
modify GuideMgr => GuideSystem
stars FindTopOrNewPath
Camera.main基本都有
DoUpdate是UniRx的
跑通一次,就不多试了
Transform t = Camera.main.transform.FindTopOrNewPath(GameObjectPath.System_LifeCycleSystem);
_mono= t.GetOrAddComponent<LifeCycleSystemMono>();
_mono.DoUpdate(Update);
public static MonoBehaviour DoUpdate(this MonoBehaviour mono,Action action)
{
Observable
.EveryUpdate()
.Subscribe(_ => action())
.AddTo(mono)
.DisposeWhenGameObjectDestroyed(mono);
return mono;
}
/// <summary>A/B/C => A B(父节点A) C(父节点B) 。返回了C</summary>
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; i < names.Length; i++)//生成了节点B ,B的父节点是A。生成了节点C ,C的父节点是B
{
Transform parent = ts[i-1];
Transform cur = parent.Find(names[i]);
if (cur.IsNullObject())
{
cur = new GameObject(names[i]).transform;
cur.SetParent(parent);
}
ts[i] = cur;
}
return ts[names.Length - 1];
}
modify 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
/// <summary>为空就会New()</summary>
public class SimpleSingleton<T> 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>(T key);
}
/// <summary>Key值,Value值,两个Equals</summary>
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>(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));
}
}
#endregion
modify 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;
}
/// <summary>
/// 路径接口,提供具体的路径的计算方法
/// </summary>
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<List<IEnemyCreator>>
{
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<IEnemyCreator> OnExecute()
{
List<IEnemyCreator> list = new List<IEnemyCreator>();
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
放在这里。
/// <summary>在PlaneEnemyCreator中有</summary>
private PlaneEnemyView InitPlaneEnemyView(int posIdxInRange, IPathData pathData)
{
var plane = this.GetSystem<IGameObjectPoolSystem>().Spawn(ResourcesPath.PREFAB_PLANE);
// 需要 plane
if (posIdxInRange == 0)
{
_pathMgr = new PathMgr();
_pathMgr.Init(plane.transform, _enemyData, pathData);
}
// 需要_pathMgr
var view = plane.GetOrAddComponent<PlaneEnemyView>();
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.CameraSizeMax().y; //up
//这种写法挺新奇的。以后可能会改
public class InCameraBorderCommand : AbstractCommand<bool>
{
Camera _camera;
Vector2 _dir;
SpriteRenderer _sR;
public InCameraBorderCommand(Transform t, Vector2 dir)
{
_camera = t.MainOrOtherCamera();
_sR = t.gameObject.GetComponent<SpriteRenderer>();
_dir = dir;
//Debug.LogFormat(Common.Log_ClassFunction()+"{0},{1},{2}", _camera,_sR,_dir);
}
protected override bool OnExecute()
{
if (_dir == Vector2.up) return _sR.bounds.min.y <= _camera.CameraSizeMax().y; //up
if (_dir == Vector2.down) return _sR.bounds.min.y >= _camera.CameraSizeMin().y; //down
if (_dir == Vector2.right) return _sR.bounds.max.x <= _camera.CameraSizeMax().x; //right
if (_dir == Vector2.left) return _sR.bounds.min.x >= _camera.CameraSizeMin().x; //left
return true;
}
}
star 关于C#:无法将类型隐式转换为Func(DoIfNotNull)
_destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ;
_destroyCase.DoIfNotNull(_destroyCase.Dead);
位置是PlaneCollideMsgComponent
。。。
意思是_destroyCase!=null,就执行后面括号里面的Action。主要是第一句 _destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ;的写法,要加() => ,表示这是一个Func,而不只是object
。。。
对比第二句,第二句应该是C#的语法默认设置,省事 () =>方法()
/// <summary>o不为空,就执行cb</summary>
public static object DoIfNotNull(this object o,Action cb)
{
if (o != null)
{
cb();
}
return o;
}
出处
public void ColliderMsg(Transform other)
{
var bullet = other.GetComponentInChildren<IBullet>();
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
/// <summary>首次载入</summary>
void Awake()
{
_slider=GetComponentInChildren<Slider>();
_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 <= 0)
{
cur = 0;
return cur;
}
var after = cur - change;
if (after <= 0)
{
after = 0;
deadAction();
}
else
{
injureAction();
}
cur = after;
return cur;
}
modify 射击 属性太多太杂,拆开
modify 星星 也是拆开
脚本陌生,因为是在后面往前推上来的, 同类修改放一块好看点
modify bug 一个mono脚本使用了此时未注册的IModel
BUG unity查找DontDestroyOnLoad的物体
以下的区别,一种是激活的场景中(不包括DontDestroyOnLoad)
一种是所有的(包括DontDestroyOnLoad)
/// <summary>
/// 场景中根节点
/// 不激活也可以找到
/// 缺点是DontDestroyOnLoad找不到</summary>
public static Transform FindTopInActiveScene(this Transform t, string tarName)
{
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<GameObject>();
foreach (GameObject go in gos)
{
if (go.name == tarName)
{
return go.transform;
}
}
return null;
}
bug 根节点有很多(Clone)
用了GameObject.Instantiate,改为Load
bug 无法直接启动带有类库输出类型的项目
[Unity3D] VisualStudio无法调试,报错:无法直接启动带有类库输出类型的项目
bug 拆箱、装箱
Init(params object[] os)是实现接口,原来想着这样比较统一。但是会报错,数据石油,但没转成功(已经检查了EnemyData的字段和顺序)
暂时直接限定类型
。。。
尝试了下,注释掉ToString,没报错
有取消注释,也没报错
//public void Init(params object[] os)//因为一下的两个没用,所以先直接限定
public void Init(EnemyData data)
{
//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<LifeComponent>();
}
_enemyData = data;
_lifeComponent = gameObject.GetOrAddComponent<LifeComponent>();
}
类
public class EnemyData :IJson
{
public int id;
public double attackTime;
public int attack;
public double fireRate;
public int life;
public double speed;
//
public TrajectoryType trajectoryType;
/// <summary>-1代表当前是随机轨迹,大于0的值,代表轨迹id</summary>
public int trajectoryID;
public BulletType[] bulletType; //看json文件没有加s
public int starNum;
public int score;
//
/// <summary> 掉落道具的可能性,例如值为10,就代表百分之十的概率 </summary>
public int itemProbability;
/// <summary> 掉落道具的范围,应该是长度为2的数组 </summary>
public ItemType[] itemRange;
/// <summary>
/// 掉落道具的数量,每个道具都在范围内随机
/// <para />例如:数量是2,范围是[0,1],那么可能会出一个0,一1.或者是两个1,或者是两个0
/// </summary>
public int itemCount;
public override string ToString()
{
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;
}
}
bug QFAudioPlayer中音频路径错误未能显示关键信息
报空,但是没有明确的路径信息
。。。
01
一步步找哪个位置可以兼具判断AudioClip和Path
在自定义的AudioSystem.PlaySound中打印,名字就是Null
public void PlaySound(string name)
{
if (name == GameAudio.Null)
{
return;
}
AudioKit.PlaySound($"{_path}{name}");
}
02 找GameAudio.Null的引用
两个BulletModel引用了,所以改成一个默认的音频路径(有音频的)
顺便加上
if (name == GameAudio.Null)
{
return;
}
bug 导弹MissileView未加CameraMove
那就加上去
protected override void InitComponent()
{
GameObject go = gameObject;
go.GetOrAddComponent<AutoDespawnComponent>();
go.GetOrAddComponent<Collider2DComponent>();
go.GetOrAddComponent<CollideMsgFromItemComponent>().Init(CollidePlayer);
go.GetOrAddComponent<AutoDespawnComponent>().Init(ResourcesPath.PREFAB_ENEMY_MISSILE);
//导弹自身还有另外的MoveComponent,所以不能GetOrAdd
_selfMove = MoveComponent.InitMoveComponentKeepDesption(gameObject,_selfMove,_speed,SpeedDes.PLANESPEED);
_cameraMove = go.GetOrAddComponent<CameraMoveComponent>().Init();
}
bug StarView被撞击后未被回收,超出界限还存在
被撞击后未被回收
PlaneView定义了实例时在PLANE节点下,会与玩家发生碰撞
。。。
基类的ItemLogic()是销毁,我们不用,用对象池回收,所以不base,在子类StarView中override回收
。。。
一个ItemViewBase对应一个EffectViewBase,比如“星星”对应被撞了“会爆炸”的特效。基类的ItemLogic()就是在EffectViewBase的主要逻辑后回调,就是 _effectView.Stop(ItemLogic);。
并且ItemViewBase引用EffectViewBase(下面用的是EffectViewBase的接口)
public abstract class ItemViewBase : PlaneView
{
private IEffectView _effectView;
private void CollideEvent()
{
AudioMgr.Single.PlayOnce(GetGameAudio().ToString());
_effectView.Stop(ItemLogic);
}
protected virtual void ItemLogic()
{
Destroy(gameObject);
}
超出界限还存在
基类中加回收的脚本AutoDespawnComponent
public abstract class ItemViewBase : PlaneView
{
private IEffectView _effectView;
protected override void InitComponent()
{
gameObject.GetOrAddComponent<AutoDespawnComponent>().Init(ResourcesPath.PREFAB_ITEM_ITEM); //后面回收需要key,但用的是一样的预制体
gameObject.GetOrAddComponent<CollideMsgFromItemComponent>().Init(CollideEvent);
if (_effectView == null)
_effectView = GetEffectView();
_effectView.Init(transform);
}
bug 敌人飞机队伍只有一队
GetValidCreator修改后(为了看得懂方便打印而改)出错
private IEnemyCreater GetCreater(List<IEnemyCreater> list)
{
_cur = null;
foreach (IEnemyCreater tmp in list)
{
if (_cur == null || _cur.GetSpawnRatio() > tmp.GetSpawnRatio())
{
if (!tmp.IsSpawning())
{
_cur = tmp;
}
}
}
return _cur;
}
改成了
IEnemyCreator GetCreatorByPrg(List<IEnemyCreator> list, IEnemyCreator curCreator)
{
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
{
/// <summary>回收到pool的key,这里用path</summary>
string _poolKey;
/// <summary>因为一般以图片消失视觉最近人</summary>
SpriteRenderer _sr;
/// <summary>需要despawn的物体</summary>
Transform _despawnTrans;
/// <summary>测试用到,不用传参</summary>
Dir _dir;
public AutoDespawnOtherCollideCameraBorderCommand(string path, SpriteRenderer sr, Transform despawnTrans)
{
_poolKey = path;
_sr = sr;
_despawnTrans = despawnTrans;
}
protected override void OnExecute()
{
if (JudgeBeyondBorder())
{
//this.GetSystem<IGameObjectPoolSystem>().DespawnWhileKeyIsName(_despawnTrans.gameObject, _poolKey) ;
this.GetSystem<IGameObjectPoolSystem>().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<IGameUtil>().GetCameraMinPoint().x;
if (x1 < x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
bool OutRight()
{
float x1 = _sr.BoundsMaxX();
float x2 = this.GetUtility<IGameUtil>().GetCameraMaxPoint().x;
if (x1 > x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
bool OutTop()
{
float y1 = _sr.BoundsMinY();
float y2 = this.GetUtility<IGameUtil>().GetCameraMaxPoint().y;
if (y1 > y2)
{
//Debug.Log(y1+","+y2);
return true;
}
return false;
}
bool OutBottom()
{
float y1 = _sr.BoundsMaxY();
float y2 = this.GetUtility<IGameUtil>().GetCameraMinPoint().y;
if (y1 < y2)
{
//Debug.Log(y1 + "," + y2);
return true;
}
return false;
}
#endregion
}
bug 两个Normal敌人Creator生成的飞机位置靠中间
PathMgr的初始化问题,它是每个敌人都会new一个,不是队伍层级的,所以我注释还有"leaderPlane"等的遗留注释
主要是这一块.竖屏,所以可以用creatorPos的x值,y需要跟随相机变化,这样就设置了飞机的初始位置
enemyTrans.SetPosX(creatorPos.x); //x需要creator传过来,不动的的,所以不能取y
enemyTrans.SetPosY( GetY(enemyTrans) ); //y需要跟随相机的移动(竖屏),加上一点点偏移
/// <summary>管理一架飞机,路径的初始位置,方向等</summary>
public class PathMgr :ICanGetUtility ,ICanSendQuery
{
#region 字属构造
private IPath _path;
/// <summary>
/// 根据飞机的初始位置,生成相应的pathMgr
/// 所以需要全部生成完再移动,这样避免第一种情况
/// </summary>
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
/// <summary>leaderPlane的出场位置
/// <para/>需要Sprite</summary>
// public void InitComponentEnemy(Transform enemyTrans, EnemyData enemyData, IPathData _pathData)
//{
// }
float GetY(Transform enemyTrans)
{
float posY = this.GetUtility<IGameUtil>().GetCameraMaxPoint().y;
float height = enemyTrans.GetComponent<SpriteRenderer>().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<AwakeBossCreatorEvent>(_ =>
{
_start = true;
});
this.GetSystem<ILifeCycleSystem>().Add(LifeName.UPDATE, this);
}
public void FrameUpdate()
{
if (!_start)
return;
if (true)
//if (this.GetSystem<IGameObjectPoolSystem>().ActiveCount(ResourcesPath.PREFAB_PLANE) == 0)
{
this.GetUtility<IGameUtil>().ShowWarnning();
SpawnAQueueBoss();
// this.GetSystem<ICoroutineSystem>().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
{
/// <summary>回收到pool的key,这里用path</summary>
string _poolKey;
/// <summary>因为一般以图片消失视觉最近人</summary>
SpriteRenderer _sr;
/// <summary>需要despawn的物体</summary>
Transform _despawnTrans;
/// <summary>有的物体的方向不需要进行自动销毁. 一般敌人忽略顶部,Bosss略全部</summary>
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<IGameObjectPoolSystem>().DespawnWhileKeyIsName(_despawnTrans.gameObject, _poolKey) ;
this.GetSystem<IGameObjectPoolSystem>().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<IGameUtil>().GetCameraMinPoint().x;
if (x1 < x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
bool OutRight()
{
float x1 = _sr.BoundsMaxX();
float x2 = this.GetUtility<IGameUtil>().GetCameraMaxPoint().x;
if (x1 > x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
bool OutTop()
{
float y1 = _sr.BoundsMinY();
float y2 = this.GetUtility<IGameUtil>().GetCameraMaxPoint().y;
if (y1 > y2)
{
//Debug.Log(y1+","+y2);
return true;
}
return false;
}
bool OutBottom()
{
float y1 = _sr.BoundsMaxY();
float y2 = this.GetUtility<IGameUtil>().GetCameraMinPoint().y;
if (y1 < y2)
{
//Debug.Log(y1 + "," + y2);
return true;
}
return false;
}
#endregion
}
bug Boss没出现
触发了超过相机范围顶部的组件
上面说了,增加对边界检测的一些方向的排除
bug 警报界面没出现
初始化问题,y,scale都不正常
bug 子弹的几种bug
散乱弹
原因
子弹我写了两个脚本,BulletEnemyCtrl,BulletPlayerCtrl中的CollideMsgFromBulletComponent有的加错了,
导致一个Ctrl物体有两个CollideMsgFromBulletComponent,(此时还没写子弹不能打子弹)
触发了这个
/// <summary>挂在Bullet的Collide</summary>
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<IBullet>(); //分开节点方便看,这个不是子弹是飞机上的子弹信息节点
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.Framing < update.Frame) //没满就加
{
update.Framing++;
return;
}
update.FrameUpdate(); //满了就执行重置
update.Framing = 0;
});
}
},
bug 图
测试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 (degree < 0)
{
degree = degreeOffset + degree;//90+(-80)
}
else
{
degree = degree - degreeOffset;//80-90
}
//t.localEulerAngles = new Vector3(e.x, e.y, degree);
t.rotation = Quaternion.Euler(e.x, e.y, degree);
return t;
}
#endregion
watch 希腊字母
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 < EGreekAlphabet.COUNT.Enum2Int(); i++)
{
if (i % 4 == 0)
{
str += "\n";
}
str += i.Int2String<EGreekAlphabet>()+"\t";
}
Debug.Log(str);
}
/// <summary>希腊字母大小写,对应英文,汉语音译
/// <br/>测试过VS+Unity可以打印</summary>
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 计算位子偏移量
/// <summary>
/// 因为理解错误,敌人发生过Y值过高的bug
/// <br/>这里椭圆中心为Vector3,与muzzle的作用在外面计算
/// </summary>
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; i < colCnt; i++)
{
x += offset;
y = ellipse.GetYBottom(x); //椭圆底部端点的y值
_pointArr[i] = new Vector3(x, y);
}
}
else if (muzzleDir == Dir.UP)
{
for (int i = 0; i < colCnt; i++)
{
x += offset;
y = ellipse.GetYTop(x); //椭圆顶部端点的y值
_pointArr[i] = new Vector3(x, y);
}
}
else
{
throw new System.Exception("未定义");
}
return _pointArr;
}
watch 根据偏移量计算实际位置
/// <summary>多少列子弹射击方向.向霰弹枪一样,发射一圈又一圈</summary>
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; i < columCnt; i++)
{
tempArr[i] = ExtendVector3.Vector3Add(muzzlePos, posArr[i]);
}
return tempArr;
}
watch 自定义的椭圆类
/// <summary>
/// 椭圆 (x/a).Pow2()+(y/b).Pow2()=1 ,(k,h)=(0,0)时
/// 椭圆 ((x-k)/a).Pow2()+((y-h)/b).Pow2()=1
/// <para/>椭圆(Ellipse)是平面内到定点F1、F2的距离之和等于常数(大于|F1F2|)的动点P的轨迹,
/// <br/>F1、F2称为椭圆的两个焦点。
/// <br/>其数学表达式为:|PF1|+|PF2|=2a(2a>|F1F2|)。
/// </summary>
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; } }
/// <summary>焦点1 小的</summary>
public Vector2 Focus1 {
get
{
if (XHalfAxis > YHalfAxis) //焦点x轴
{
return new Vector2(Center.x - (float)c, Center.y);
}
else if (XHalfAxis < YHalfAxis) //焦点y轴
{
return new Vector2(Center.x , Center.y-(float)c);
}
else//圆
{
return Center;
}
}
}
/// <summary>焦点2 大的</summary>
public Vector2 Focus2 { get { return -Focus1; } }
/// <summary>焦半径1</summary>
public float FocusRadius1 { get; }
/// <summary>焦半径2</summary>
public float FocusRadius2 { get; }
/// <summary>焦距 |F1-F2| = 2c</summary>
public float FocusDistance { get { return Vector2.Distance(Focus1, Focus2); } }
/// <summary>X半轴长.构造得来</summary>
public float XHalfAxis;
/// <summary>Y半轴长.构造得来</summary>
public float YHalfAxis;
#endregion
#region 次生参数
/// <summary>X轴长</summary>
public double XAxis { get { return XHalfAxis * 2; } }
/// <summary>Y轴长</summary>
public double YAxis { get { return YHalfAxis * 2; } }
/// <summary>短半轴长</summary>
public double ShortHalfAxis { get
{
if (XHalfAxis >= YHalfAxis)
{
return YHalfAxis;
}
else if (XHalfAxis < YHalfAxis)
{
return XHalfAxis;
}
throw new System.Exception("空值异常");
} }
/// <summary>长半轴长</summary>
public double LongHalfAxis
{
get
{
if (XHalfAxis >= YHalfAxis)
{
return XHalfAxis;
}
else if (XHalfAxis < YHalfAxis)
{
return YHalfAxis;
}
throw new System.Exception("空值异常");
}
}
/// <summary>短轴长</summary>
public double ShortAxis { get { return ShortHalfAxis * 2; } }
/// <summary>长轴长</summary>
public double LongAxis { get { return LongHalfAxis * 2; } }
//
public int VertextPointCnt = 4;
/// <summary>4个端点</summary>
public Vector2[] VertextPointPosArr
{
get
{
Vector2[] vs= new Vector2[VertextPointCnt];
vs[0] = new Vector2((float)(Pos.x-XAxis), Pos.y-0f);
vs[1] = new Vector2(Pos.x - 0f, (float)(Pos.y - YAxis));
vs[2] = new Vector2((float)(Pos.x-XAxis), Pos.y - 0f);
vs[3] = new Vector2(Pos.x - 0f, (float)(Pos.y-YAxis));
return vs;
}
}
#endregion
#region 数学公式常用
/// <summary>(x-radius)/l</summary>
public double a { get { return XHalfAxis; } }
/// <summary>(y-h)/m</summary>
public double b { get { return YHalfAxis; } }
/// <summary>x-radian</summary>
public double k { get { return Pos.x; } }
/// <summary>y-h</summary>
public double h { get { return Pos.y; } }
/// <summary></summary>
public double c { get { return (a.Pow2() - b.Pow2().Abs().Sqrt()); } }
/// <summary>离心率 焦距与长轴比例 2c/2a</summary>
public double e { get { return FocusDistance / LongAxis; } }
#endregion
#region 四个端点
public Vector3 Top { get { return new Vector3(Center.x, Center.y+YHalfAxis); } }
public Vector3 Bottom { get { return new Vector3(Center.x,Center.y - YHalfAxis); } }
public Vector3 Left { get { return new Vector3(Center.x - XHalfAxis,Center.y); } }
public Vector3 Right { get { return new Vector3(Center.x + XHalfAxis, Center.y); } }
#endregion
public Ellipse(float xHalfAxis, float yHalfAxis, Vector2 pos)
{
XHalfAxis = xHalfAxis;
YHalfAxis = yHalfAxis;
Pos = pos;
}
/// <summary>2PIb+4*(l-m)</summary>
public double Circumference()
{
return 2 * Mathf.PI * b + 4*(a - b);
}
/// <summary>面积 PI*l*b或PI*A*B/4</summary>
public double Square()
{
return Mathf.PI * a * b;
}
#region IGetX,IGetY
/// <summary>
/// 根据y求两个x ,先小后大
/// <br/>v1 = a.Pow2();
/// <br/>v2 = 1 - ((y-h) / b).Pow2();
/// <br/>v3 = (v1 * v2).Sqrt();
/// </summary>
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;
}
/// <summary>
/// 根据x求两个y ,先小后大
/// <br/>v1 = b.Pow2();
/// <br/>v2 = 1 - (( x - k) / a).Pow2();
/// <br/>v3 = (v1*v2).Sqrt();
/// </summary>
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 < count; i++)
{
x += offsetX;
y = ellipse.GetYArr(x)[1] ; //飞机头上面
_pointArr[i] = new Vector3(x - top.x, y);
}
return _pointArr;
}
}
效果
可以看到玩家和敌人射出子弹的 时机 和 路径 都正确了
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
/// <summary>无时间信息的路径</summary>
public const string Path = "Path";
/// <summary>轨迹,路径(Path)和轨迹(Trajectory)的区别就在于,轨迹还包含了时间信息</summary>
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() < 0.01f)
在索引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轴移动看看效果如下
/// <summary>图形边长的点坐标</summary>
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 < halfPrecision + 1; i++)//+1包括了可能正在两个半圈中间的那个
{
yArr = GetYArr(xTmp, _data.Center);//只需要方向,用原来的坐标(所以不需要移动中心)就可以求一样的方向,我用自定义的Ellipse用 GetYArr(xTmp, Vector3.zero)也一样
if ((yArr[0] - _data.Center.y).Abs() < 0.01f)//坐标归一化(归一到相对于中心为Vector.zero),中间的往下跑
{
posArr[i] = new Vector3(xTmp, yArr[0]);
}
else
{
symIdx = posArr.Length - i - 1;
//排大小 ,我有自制的Ellipse类,已经排好了大小,索引小,值就小
posArr[i] = new Vector3(xTmp, yArr[1]);
posArr[symIdx] = new Vector3(xTmp, yArr[0]); //对称的
//if (yArr[0] < _data.Center.y) //下面的
//{
// posArr[i] = new Vector3(xTmp, yArr[1]);
// posArr[symIdx] = new Vector3(xTmp, yArr[0]); //对称的
//}
//else if (yArr[0] > _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<IGameUtil>().CameraMinPoint().y + _halfHeight; } }
......
watch Factory
发现用枚举来new不同对象时用Factory(静态类)命名的很多
EnetrPathFactory
PathFactory
modify PathMgr合IPath=>PathBase=>实现类有很多相似之处
#region IPath ,PathBase
/// <summary>这名字原本是IPathBase的,有部分方法PathMgr完全一样,所以再拆分组合</summary>
public interface IPath
{
Vector3 GetFromPos(int id);
Vector2 GetDir();
PathState GetPathState();
bool FollowCamera();
}
/// <summary>
/// 路径接口,提供具体的路径的计算方法
/// </summary>
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;
/// <summary>提供一个Trigger,处理外部的事件</summary>
[RequireComponent(typeof(Collider2D)) ]
public class Trigger2DComponent : MonoBehaviour
{
public List<Action<Collider2D>> EnterLst = new List<Action<Collider2D>>();
//public List<Action<Collider2D>> ExitLst = new List<Action<Collider2D>>();
//public List<Action<Collider2D>> StayLst = new List<Action<Collider2D>>();
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; i < EnterLst.Count; i++)
{
EnterLst[i](otherCollider);
}
}
}
CollideMsgComponent
/****************************************************
文件: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<ICollideMsg> selfMsg = self.GetComponentsInChildren<ICollideMsg>().ToList();
List<ICollideMsg> otherMsg = otherCollider.GetComponentsInChildren<ICollideMsg>().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
/// <summary>LevelEnemyDataConfig</summary>
public interface ICreatorData
{
}
public abstract class CreatorDataBase : ICreatorData
{
public string ConfigPath()
{
return ResourcesPath.CONFIG_LEVEL_ENEMY_DATA;
}
}
public class PlaneCreatorData : CreatorDataBase //json数据没乱改字段名
{
......
/// <summary>导弹</summary>
public class MissileCreatorData : CreatorDataBase
{
......
star IConfig,IConfigPath
IConfig表示这个类关系配置文件,不能单独改
IConfigPath表示这个类有相应的配置文件路径
#region IConfig
/// <summary>
/// 啥都没有,表明这有关配置文件,不要随便改字段名
/// <br/>或者可以加config路径方便查找。但我暂不需要
/// </summary>
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 //最小最小倍数
{
/// <summary>补足,返回factor的倍数,大于等于num的factor的最小倍数</summary>
public static int MultipleMore(this int num, int factor)
{
if (num < factor)
{
return factor;
}
else
{
if (num % factor == 0)
{
return num;
}
else
{
return (num / factor) * factor + factor; //7=>8,9=>12
}
}
}
/// <summary>砍掉,返回factor的倍数,小于等于num的factor的最大倍数</summary>
public static int MultipleLess(this int num, int factor)
{
if (num < factor) //比如(3,4)=>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 一轮
/// <summary>一轮后重新计算
/// 比如12生肖,13年后还是1</summary>
public static int Round(this int num, int round)
{
return num%round;
}
}
star 向量指向
吃屎喝我自定义的枚举Dir冲突后,选择改成EDir(EnumDir的意思),这个永远不会冲突
/// <summary>符合直觉点</summary>
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//深度模式下的宽和高
{
/// <summary>宽/高</summary>
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<IAirCombatAppModel>().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<EnemyLifeItem>().Init(eachLife);//这样也行,不用特意地Init,(基类操作了),Show加上去自动Show
}
}
modify LifeItem的
public class EnemyLifeItem : SetPosZByLayerLevelView ,ICanGetSystem
{
[SerializeField] MessageMgrComponent _messageMgrComponent;
/// <summary>不确定InitComponent是否跑了</summary>
[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>();
_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
/// <summary>根据层次枚举设置posZ</summary>
public abstract class SetLocalPosZByLayerLevelView : GameLevelViewBase
{
protected override void OnEnable()
{
float z = E_Entity2DLayer.Enum2Int();
transform.SetLocalPosZ(z);
Init();
}
}
bug 碰撞逻辑做得有点乱
出现在调试去掉玩家的CollideMsgFromPlaneComponent和单/多枪口整合时(枪口挂着CollideMsgFromBulletComponent)
处理好了暂且用着
modify 定义Attribute从定义到装入字典的整个过程
使用情景
使得
Dictionary<Path, 面板>
Dictionary<敌人类型枚举, 敌人实现类>
之类的操作更加规范方便
//
主要时尝试统合两个Attribute, 但是静态类就用不了接口
接口是暂时统合的结果
//
需要注意的是IBulletModelUtil需要在Pool之前注册,Pool生成子弹,
子弹需要BulletModel, 顺序不正确,撞击就报空了
//
两个Util, BulletModelUtil, BindPrefabUtil还是分开比较好
使用举例
using IAddTypeByAttribute = ExtendAttribute.IAddTypeByAttribute;
public class CustomAttributesUtil : IInit ,ICanGetUtility
{
public void Init()
{
ExtendAttribute.InitData<BindPrefabAttribute>(this.GetUtility<IBindPrefabUtil>().Init);
ExtendAttribute.InitData<BulletAttribute>(this.GetUtility<IBulletModelUtil>().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<BulletType, IBulletModel> _bulletDic = new Dictionary<BulletType, IBulletModel>();
#region IBulletUtil
/// <summary>
/// bulletType,类前标签
/// type ,实现类BossBullet:IBullet
/// 这里字典Dic<A,B>,A是attribute中的一个字段类型
/// </summary>
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<IBulletModel>(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<Type> GetType(string path);
}
public class BindPrefabUtil : IBindPrefabUtil
{
private static readonly Dictionary<string, List<Type>> _pathDic = new Dictionary<string, List<Type>>();
private static readonly Dictionary<Type, int> _priorityDic = new Dictionary<Type, int>();
#region 辅助
/// <summary>初始化内部字典</summary>
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<Type>());
}
if (!_pathDic[path].Contains(type))
{
_pathDic[path].Add(type);
_priorityDic.Add(type, after.Priority);
_pathDic[path].Sort(new BindPriorityComparer());
}
}
/// <summary>根据路径返回类型</summary>
public List<Type> GetType(string path)
{
if (_pathDic.ContainsKey(path))
{
return _pathDic[path];
}
Debug.LogError("当前数据中未包含路径:" + path);
return null;
}
#endregion
#region 内部类
/// <summary>预制体优先级比较器</summary>
class BindPriorityComparer : IComparer<Type>
{
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);
}
/// <summary>T用接口,比如IBulletModel,实际是实现了接口的类</summary>
public static T GetInstance<T>(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;
}
}
/// <summary>
/// T xxxAttribute
/// Type 系统类
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="cb"></param>
public static void InitData<T>(Action<T, Type> cb) where T : Attribute
{
Type[] types;
types = ExtendReflection.GetTypes<T>();
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;
/// <summary>子弹样式</summary>
[SerializeField] int TmpPlaneLevel;
[Header("Hero")]
/// <summary>实际使用的是在父节点中的索引</summary>
[SerializeField] int SelectHeroID;
[Header("Game")]
[SerializeField] GameState E_GameState;
[Header("Level")]
[SerializeField] LevelState E_LevelState;
/// <summary>实际使用的是在父节点中的索引</summary>
[SerializeField] int SelectedLevel;
[SerializeField] int CurLevel;
/// <summary>已经通关数.+1就是最大的可以解锁的关卡</summary>
[SerializeField] int PassedLevel;
[SerializeField] bool IsFinishOneLevel;
#endregion
#region 生命
/// <summary>首次载入且Go激活</summary>
void Start()
{
_timing = 0f;
_model = this.GetModel<IAirCombatAppModel>();
_stateModel = this.GetModel<IAirCombatAppStateModel>();
}
/// <summary>固定更新</summary>
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<ILifeCycleSystem>().Add(LifeName.UPDATE, this);
this.GetSystem<ILifeCycleSystem>().Add(LifeName.DESTROY, this);
//以下都是状态改变,以及之后的回调,所以在回调中不要写条件大的状态
this.GetModel<IAirCombatAppStateModel>().E_GameState.Register(state =>
{
if (state != GameState.START) return;
OnGameStart();
}).AddToUnregisterList(this);
this.GetModel<IAirCombatAppStateModel>().E_GameState.Register(state =>
{
if (state != GameState.END) return;
OnGameEnd();
}).AddToUnregisterList(this);
this.GetModel<IAirCombatAppStateModel>().E_LevelState.Register(state =>
{
if (state != LevelState.START) return;
OnLevelStart();
}).AddToUnregisterList(this);
this.GetModel<IAirCombatAppStateModel>().E_LevelState.Register(state =>
{
if (state != LevelState.END) return;
OnLevelEnd();
}).AddToUnregisterList(this);
}
#region OnStateXXX
private void OnGameStart()
{
this.GetModel<IAirCombatAppStateModel>().TmpPlaneLevel.Value = this.GetModel<IAirCombatAppStateModel>().PlaneLevel;
this.GetModel<IAirCombatAppStateModel>().E_LevelState.Value = LevelState.START;
}
/// <summary>OnLevelEnd(注意是后,方法是因变量)后会执行的</summary
private void OnGameEnd()
{
this.GetSystem<IUISystem>().Open(ResourcesPath.PREFAB_GAME_RESULT_VIEW);
}
/// <summary>OnLevelEnd(注意是后,方法是因变量)后会执行的</summary>
private void OnLevelStart()
{
StateModel.IsFinishOneLevel.Value = false;
this.GetSystem<IUISystem>().Close(ResourcesPath.PREFAB_GAME_RESULT_VIEW);
_curTriggerEventLst = new List<GameProcessTriggerEvent>();
InitDomCreatorMgr();
}
private void OnLevelEnd()
{
ClearData();
StateModel.CurLevel.Value++;
StateModel.IsFinishOneLevel.Value = true;
this.GetSystem<IUISystem>().Open(ResourcesPath.PREFAB_GAME_RESULT_VIEW);
this.GetSystem<ICoroutineSystem>().Delay(Const.WAIT_LEVEL_START_TIME, () =>
{
StateModel.E_LevelState.Value = LevelState.START;
});
}
#endregion
watch RegisterEvent 与MessageSystem的选择
监听的增加和移除,显然QF的管理比较容易
//
01 事件的发送,左边需要自定义事件
0201 QF如果在Model根据值的不同(圈圈里面),进行Switch,也需要自定义事件
0202 QF如果在(黑色线位置),就是注册监听时,在各自的方法写
state=> if(state!=)
Msg.AddListener(MsgEvent.EVENT_GAME_START, OnGameStart);
//
this.GetModel<IAirCombatAppStateModel>().E_GameState.Register(state =>
{
if (state != GameState.START)
{
return;
}
});
star ForeachAction
测试一种场景, 没报错
private void OnTriggerEnter2D(Collider2D otherCollider)
{
EnterLst.ForeachAction(otherCollider);
}
public static void ForeachAction<T>(this List<Action<T>> lst, T t)
{
if (lst==null || lst.Count ==0)
{
return;
}
for (int i = 0; i < lst.Count; i++)
{
lst[i](t);
}
}
star 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<IDontDestroyOnLoadSystem>().GetOrAdd(GameObjectPath.System_CoroutineSystem);
_mono = t.GetOrAddComponent<CoroutineSystemMono>(); //这种是不行,为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<NSOrderSystem> ,ICanRegisterEvent
{
public bool GameDataMgrInited ;
public void Init()
{
GameDataMgrInited = false;
this.RegisterEvent<GameDataMgrInitedEvent>(_=> GameDataMgrInited = true) ;
}
public IArchitecture GetArchitecture()
{
return AirCombatApp.Interface;
}
}
GameStateStartCommand
/// <summary>改变状态有时需要等待其他模块的加载</summary>
public class GameStateStartCommand : AbstractCommand
{
protected override void OnExecute()
{
this.GetSystem<ICoroutineSystem>().StartInner(GameStart());
}
IEnumerator GameStart()
{
//第一种写法
while ( !NSOrderSystem.Single.GameDataMgrInited)//等待条件
{
yield return new WaitForEndOfFrame(); //等一帧
}
//上面条件改变跳出后,执行后面的
this.GetModel<IAirCombatAppStateModel>().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<ItemType>();
}
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 说明
/// <summary>原名ItemType,但它不处理Star(不好统一处理),所以让名,改为RewardType</summary>
public enum RewardType
{
ADD_BULLET,
ADD_EXP,
SHIELD,
POWER
}
/// <summary>杀死敌人后的Item奖励.本来就用ItemType,弃用</summary>
public enum EItemType
{
STAR,
/// <summary>实际是一列变两列三列</summary>
ADD_BULLET,
ADD_EXP,
SHIELD,
/// <summary>原来叫POWER(改过Bomb),图片路径是Assets / Resources / Picture / Item / Power.png</summary>
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
/// <summary>
/// 原来ADD_EXP, ADD_BULLET两个类都是一样的类体函数,参数完全没变,
/// 直接统一default</summary>
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<T,T>(Func<返回值,新值>). 和一个limtValue(最大值,最小值)
public class BindableProperty<T> : IBindableProperty<T>
{
......
private Action<T> mOnValueChanged = (v) => { };
#endregion
#region pub
/// <summary>
/// 也就是单独的一个mValue = newValue
/// 暂时不清楚目的
/// </summary>
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<IAirCombatAppStateModel>().PlaneLevelMax;
if (_tmpPlaneLevel <= planeLevelMax)
{
this.GetModel<IAirCombatAppStateModel>().TmpPlaneLevel.Value=_tmpPlaneLevel;
}
}
}
03 watch tmpPlaneLevel与枪数
_tmpPlaneLevel用的是索引值(01234, 懒得用+1),对应BulletConfig
//
图可以看到
大于4,就不变了
"PLAYER": {
"fireRate": 1,
"bulletSpeed": 5,
"trajectoryType": 0,
"trajectory": [
[
90
],
[
95,
85
],
[
100,
90,
80
],
[
105,
95,
85,
75
],
[
110,
100,
90,
80,
70
]
]
},
.....
[SerializeField] int _tmpPlaneLevel;
private void Update()
{
if (Input.GetKeyDown(KeyCode.L))
{
this.SendCommand(new ChangeTmpPlaneLevelCommand(_tmpPlaneLevel));
}
......
modify 玩家被撞后的闪避状态
01 可能用状态机会好点
02 Command的碰撞觉得挺乱的
03 被撞死亡只传InvincibleComponent(敌人死亡需要null判断为敌人,e) 时,如果玩家Invincible,又需要InvincibleComponent.Invincibl来使得敌人被撞也不会死,冲突了,所以暂时把Invincibl实际存在StateModel
InvincibleComponent
撞了就透明, 时间结束就恢复
public class InvincibleComponent : MonoBehaviour ,IInitParas<InvincibleComponent> ,ICanGetSystem ,IUpdate ,ICanGetModel
{
#region 属性
//[SerializeField] private bool _invincible;
public bool Invincible { get => this.GetModel<IAirCombatAppStateModel>().Invincible; set => this.GetModel<IAirCombatAppStateModel>().Invincible.Value = value; }
[SerializeField] private float _invincibleTimer;
[SerializeField] private float _invincibleTime;
[SerializeField] private SpriteRenderer _sr;
/// <summary>有点像状态机的Enter</summary>
[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<ILifeCycleSystem>().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
/// <summary>挂在Plane(敌人,玩家)的CollideMsg</summary>
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<BulletModelComponent>(); //
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<BulletModelComponent>()); //也是以下
bool invincible = this.GetModel<IAirCombatAppStateModel>().Invincible;
_destroyCase.DoIfNotNull(() =>
{
if (_other.CompareTag(Tags.ENEMY))//other是敌人,所以这是玩家
{
//_destroyCase.Injure(-otherBullet.GetAttack());石
_destroyCase.Injure(-1);
this.GetModel<IAirCombatAppStateModel>().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");
}
}
/// <summary></summary>
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;
//
/// <summary>升满级了,就不能突出飞机升级图片的变化</summary>
[SerializeField] bool _clearAllPlaterPres;
public IArchitecture GetArchitecture()
{
return AirCombatApp.Interface;
}
private void Start()
{
{ //InitpLayer之前监听
_listenPlayer = false;
this.RegisterEvent<InitPlayerEvent>(_ => _listenPlayer = true);
}
//
if (_clearAllPlaterPres)
{
this.GetUtility<IStorageUtil>().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
{
.....
/// <summary>
/// ,0_0 0_1 0_2 0_3, 一个id四个level图 ,
/// 注意和PlaneBulletLevelMax区别开来
/// </summary>
BindableProperty<int> SelectedPlaneSpriteLevelMax { get; }
/// <summary>该你来拿子弹列数的haul,玩家是12345,数组索引是01234,所以等于</summary>
BindableProperty<int> 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<IGameUtil>().CameraMinPoint().x- _sr.BoundsSize().x; //左右要多一个身位,玩家看不到再销毁
if (x1 < x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
bool OutRight()
{
float x1 = _sr.BoundsMaxX();
float x2 = this.GetUtility<IGameUtil>().CameraMaxPoint().x+_sr.BoundsSize().x; //左右要多一个身位,玩家看不到再销毁
if (x1 > x2)
{
//Debug.Log(x1 + "," + x2);
return true;
}
return false;
}
#endregion
}
bug 火箭没有发生Collide玩家
从PlaneEnemyCtrl赋值大部分
碰撞的结果就是那个命令 CollideMsgFromPlaneCommand, 触发IDestroyCase的Injure()或Dead()(因为测试,统一改成扣一点血)
/// <summary>导弹</summary>
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<IBulletModelUtil>().GetBulletModel(_bulletType);
_trigger2DComponent = go.GetOrAddComponent<Trigger2DComponent>().InitComponent();
_sr = go.GetComponentOrLogError<SpriteRenderer>();
}
go.GetOrAddComponent<Trigger2DComponent>();
transform.FindOrNew(GameObjectName.Collider).GetOrAddComponent<CollideMsgFromItemComponent>().Init(CollidePlayer);
//导弹自身还有另外的MoveComponent,所以不能GetOrAdd
float _cameraSpeed = this.GetModel<IAirCombatAppStateModel>().CameraSpeed;
_cameraMove = go.GetOrAddComponent<CameraMoveSelfComponent>().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<DespawnBulletComponent>().Init(transform, _despawnKey);
_autoDespawnOtherComponent = _lifeTrans.GetOrAddComponent<AutoDespawnOtherComponent>().Init(transform, _despawnKey, _sr);
}
_colliderTrans = transform.FindOrNew(GameObjectName.Collider); //自己就是子弹||自己就是飞机的一种
{
_collideMsgComponent = _colliderTrans.GetOrAddComponent<CollideMsgComponent>().InitComponent(_trigger2DComponent);
//_bulletEffectComponent = _bulletTrans.GetOrAddComponent<BulletEffectComponent>().Init(_bulletType, _bulletTrans);
//_collideMsgFromBulletComponent = _colliderTrans.GetOrAddComponent<CollideMsgFromPlaneComponent>().InitComponent(_bulletModel, _despawnBulletComponent);
_collideMsgFromPlaneComponent = _colliderTrans.GetOrAddComponent<CollideMsgFromPlaneComponent>().InitComponent(_bulletModel, _despawnBulletComponent);
_bulletModelComponent = _colliderTrans.GetOrAddComponent<BulletModelComponent>().Init(_bulletModel);
}
}
bug 分辨率
左右最好统一
//
因为做UI预制体,认的是左边那个
如果右边那个不是一样的,会出问题(除了锚点靠边靠四角较少问题,其它的很容易越界,翻转,大小缩放不理想)