用QFramework重构飞机大战(Siki Andy的)(下02)(06-0? 游戏界面及之后的所有面板)

用QFramework重构飞机大战(Siki Andy的)(下02)(06-0? 游戏界面及之后的所有面板)

GitHub

//

官网的
全民飞机大战(第一季)-----框架设计篇(Unity 2017.3)
全民飞机大战(第二季)-----游戏逻辑篇(Unity 2017.3)
全民飞机大战(第三季)-----完善功能篇(Unity 2017.3)
全民飞机大战(第四季)-----新手引导篇

//

B站各放几集
全民飞机大战(第一季)-----框架设计篇(Unity 2017.3)
全民飞机大战(第二季)-----游戏逻辑篇(Unity 2017.3)
全民飞机大战(第三季)-----完善功能篇(Unity 2017.3)
全民飞机大战(第四季)-----新手引导篇

bug QF处理单例类中的使用this.Getxxx的

Single.GetModel不行,会一直跳空,使用System

csharp 复制代码
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中的参数,怎么作为一个方法的参数来传入

目前做不到注释中的做法

csharp 复制代码
    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类型");
            }
        }
    }

效果

csharp 复制代码
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为空

注释的那句是不行的,所以在接口中要写抽象方法

csharp 复制代码
        ISceneSystem sys = this.GetSystem<ISceneSystem>();
        //SceneSystem sys = this.GetSystem<SceneSystem>();
        sys.AddSceneLoaded(SceneName.Game, callBack =>

----------------------------------------

Audio

先替换掉AudioMgr

可以省略掉缓存字典、列表,不用自己做

官方案例

三种喇叭

Music

Sound

Voice

csharp 复制代码
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

csharp 复制代码
    public void PlayBGM()
    {
        AudioKit.PlayMusic( $"resources://Audio/BGM/{BGAudio.Game_BGM.Enum2String()}"  );
    }

播放Sound

角色出场语音的切换

csharp 复制代码
    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

csharp 复制代码
    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的方式来操作

csharp 复制代码
    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)中

csharp 复制代码
        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 拆掉。

csharp 复制代码
        void Clear(string key);
        void ClearAll();
        bool ContainsKey(string key);

完成类似这种效果

csharp 复制代码
    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里面啥都没有)改成接口

csharp 复制代码
    public interface ISetJsonData
    {
        void SetJsonData(string key, JsonData value);
    }

04 StorageSystem

不行,因为Util(System不能在Model中使用,不用Systm就没有Init重写),所以又改回来

05 StorageUtil

csharp 复制代码
    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

csharp 复制代码
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

csharp 复制代码
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 消息机制底层委托的结构

csharp 复制代码
 	private Dictionary<Type, IEasyEvent> mTypeEvents 
    public class EasyEvent : IEasyEvent
    {
        private Action mOnEvent = () => { };
        。。。
    public class EasyEvent<T> : IEasyEvent
    {
        private Action<T> mOnEvent = e => { };

而飞机大战用了HashSet

csharp 复制代码
/// <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)去了

csharp 复制代码
    public void AddListener(KeyCode code, KeyState state, Action<object[]> callback)
     public void RemoveListener(KeyCode code, KeyState state, Action<object[]> callback)

详细看下一条

modify InputMgr => InputSystem

有初始化

watch 先贴一下组织的

csharp 复制代码
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版的)

类似于这样改,不展示全部

csharp 复制代码
#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 &lt;int, CoroutineController  &gt;
/// </summary>

public class CoroutineSystem : QFramework.AbstractSystem,ICoroutineSystem
{

modify LifeCycleMgr => LifeCycleSystem

bug QFramework.AbstractSystem与MonoBehaviour

不能同时:QFramework.AbstractSystem与MonoBehaviour,选择:QFramework.AbstractSystem,然后添加一个MonoBehaviour

watch LifeCycleSystem

csharp 复制代码
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&lt;object&gt;</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的

跑通一次,就不多试了

csharp 复制代码
        Transform t = Camera.main.transform.FindTopOrNewPath(GameObjectPath.System_LifeCycleSystem);
       _mono= t.GetOrAddComponent<LifeCycleSystemMono>();
        _mono.DoUpdate(Update);
csharp 复制代码
    public static MonoBehaviour DoUpdate(this MonoBehaviour mono,Action action)
    {
        Observable
            .EveryUpdate()
            .Subscribe(_ => action())
            .AddTo(mono)
            .DisposeWhenGameObjectDestroyed(mono);
        return mono;
    }
csharp 复制代码
    /// <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)

csharp 复制代码
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

csharp 复制代码
/// <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

因为是存储

csharp 复制代码
    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,一波敌人

csharp 复制代码
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

csharp 复制代码
    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 的初始化。

csharp 复制代码
public class PlaneEnemyView : PlaneView,IUpdate  ,ICanGetSystem
{
    private PathMgr _pathMgr;

所以不会将PathMgr 放在PlaneEnemyView ,所以创建它的 PlaneEnemyCreator

放在这里。

csharp 复制代码
    /// <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

csharp 复制代码
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,所以那样

csharp 复制代码
    if (_dir == Vector2.up) return _sR.bounds.min.y <= _camera.CameraSizeMax().y; //up
csharp 复制代码
	//这种写法挺新奇的。以后可能会改
    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)

关于C#:无法将类型隐式转换为Func

_destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ;

_destroyCase.DoIfNotNull(_destroyCase.Dead);

位置是PlaneCollideMsgComponent

。。。

意思是_destroyCase!=null,就执行后面括号里面的Action。主要是第一句 _destroyCase.DoIfNotNull(() => _destroyCase.Injure(bullet.GetAttack())) ;的写法,要加() => ,表示这是一个Func,而不只是object

。。。

对比第二句,第二句应该是C#的语法默认设置,省事 () =>方法()

csharp 复制代码
    /// <summary>o不为空,就执行cb</summary>
    public static object DoIfNotNull(this object o,Action cb)
    {
        if (o != null)
        {
            cb();
        }
        return o;
    }

出处

csharp 复制代码
    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

csharp 复制代码
/****************************************************
    文件: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("死亡"));
        }

    }
}
csharp 复制代码
    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)

csharp 复制代码
    /// <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,没报错

有取消注释,也没报错

csharp 复制代码
    //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>();
    }

csharp 复制代码
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

csharp 复制代码
    public void PlaySound(string name)
    {
        if (name == GameAudio.Null)
        {
            return;
        }
        AudioKit.PlaySound($"{_path}{name}");
    }

02 找GameAudio.Null的引用

两个BulletModel引用了,所以改成一个默认的音频路径(有音频的)

顺便加上

csharp 复制代码
        if (name == GameAudio.Null)
        {
            return;
        }

bug 导弹MissileView未加CameraMove

那就加上去

csharp 复制代码
    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的接口)

csharp 复制代码
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

csharp 复制代码
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修改后(为了看得懂方便打印而改)出错

csharp 复制代码
    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;
    }

改成了

csharp 复制代码
        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的原因,顶部不算进超范围

出现原因是子弹也需要这个组件, 所以需要把它改成可选项

csharp 复制代码
   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需要跟随相机变化,这样就设置了飞机的初始位置

csharp 复制代码
		enemyTrans.SetPosX(creatorPos.x);	//x需要creator传过来,不动的的,所以不能取y
		enemyTrans.SetPosY(  GetY(enemyTrans) );  //y需要跟随相机的移动(竖屏),加上一点点偏移
csharp 复制代码
/// <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,

csharp 复制代码
	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

csharp 复制代码
	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,(此时还没写子弹不能打子弹)

触发了这个

csharp 复制代码
	/// <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代码

csharp 复制代码
        {
            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 朝向

做子弹朝向时(旋转角度与四元数,欧拉角的设置),有个旋转的效果挺好玩的

csharp 复制代码
        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 朝向"

csharp 复制代码
        #region IUpdate
        public int Framing { get; set; }

        public int Frame {  get;   }
        public void FrameUpdate()
        {

            transform.FaceTo(_dir,Dir.UP);
            ......
        }
        #endregion
csharp 复制代码
    #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 希腊字母

csharp 复制代码
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 计算位子偏移量

csharp 复制代码
/// <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 根据偏移量计算实际位置

csharp 复制代码
	/// <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 自定义的椭圆类

csharp 复制代码
    /// <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

csharp 复制代码
			Vector3 center = new Vector3(_muzzleTrans.position.x, _muzzleTrans.position.y-0.3f); 
			Ellipse ellipse = new Ellipse(boundsSizeX/2.0f, 0.3f,center );
csharp 复制代码
	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

csharp 复制代码
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

csharp 复制代码
    /// <summary>无时间信息的路径</summary>
    public const string Path = "Path";
    /// <summary>轨迹,路径(Path)和轨迹(Trajectory)的区别就在于,轨迹还包含了时间信息</summary>
    public const string Trajectory = "Trajectory";

02 显示Boss的PathMgr用到的PathName

方法一 在实现类中直接返回, 比较累, 每个实现类都要重写

方法二 各种EnterPath原来用的是接口, 所以用抽象类来写, 接口的还是接口, 减少修改

csharp 复制代码
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 )就会撞击

csharp 复制代码
	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轴移动看看效果如下

csharp 复制代码
	/// <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

csharp 复制代码
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=>实现类有很多相似之处

csharp 复制代码
#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类似

csharp 复制代码
    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

csharp 复制代码
/****************************************************
    文件: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

csharp 复制代码
/****************************************************
	文件: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 使用 一个配置文件对应一个类

使用类似,这样就可以方便地找到数据模型类对应的配置文件

多个数据模型类对应一个配置文件,就用抽象类,在抽象类里面写路径

csharp 复制代码
public class EnemyData   :IJson
{
    public int id;
    public double attackTime;
    public int attack;
    public double fireRate;
	......

    public string ConfigPath()
    {
      return ResourcesPath.CONFIG_ENEMY;
    }

modify 使用 一个配置文件对应多个类 结合基类

csharp 复制代码
#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表示这个类有相应的配置文件路径

csharp 复制代码
#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的最倍数

csharp 复制代码
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 一轮

csharp 复制代码
    /// <summary>一轮后重新计算
    /// 比如12生肖,13年后还是1</summary>
    public static int Round(this int num, int round)
    { 
           return num%round; 
    }
}

star 向量指向

吃屎喝我自定义的枚举Dir冲突后,选择改成EDir(EnumDir的意思),这个永远不会冲突

csharp 复制代码
	    /// <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

csharp 复制代码
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

csharp 复制代码
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的

csharp 复制代码
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的

csharp 复制代码
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

csharp 复制代码
/// <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还是分开比较好

使用举例

csharp 复制代码
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

复用模块

csharp 复制代码
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

csharp 复制代码
/****************************************************
    文件: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的小周期

csharp 复制代码
    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!=)

csharp 复制代码
        Msg.AddListener(MsgEvent.EVENT_GAME_START, OnGameStart);
        //
        this.GetModel<IAirCombatAppStateModel>().E_GameState.Register(state =>
        {
            if (state != GameState.START)
            {
                return;
            }
        });

star ForeachAction

测试一种场景, 没报错

csharp 复制代码
    private void OnTriggerEnter2D(Collider2D otherCollider)
    {
        EnterLst.ForeachAction(otherCollider);
    }
csharp 复制代码
    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

csharp 复制代码
    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);
    }
csharp 复制代码
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使用时为空

改为属性

csharp 复制代码
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

csharp 复制代码
/****************************************************
    文件: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

csharp 复制代码
    /// <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来源于

csharp 复制代码
    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次数多,逻辑上可以说得通

csharp 复制代码
        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的定义

csharp 复制代码
public enum ItemType
{
	ADD_BULLET,
	ADD_EXP,
	SHIELD,
	POWER
}

bug Star的出局

01 如果用原来的的枚举RewardType, Star得单拎出来处理, 不优雅

所以新加了EItemType, 整体见 代码一段

02 用法如下, 代码二段 (E_ItemType定义抽象类里面)

csharp 复制代码
#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();
        }
    }
}
csharp 复制代码
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

csharp 复制代码
/// <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(最大值,最小值)

csharp 复制代码
	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赋值

csharp 复制代码
    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,就不变了

csharp 复制代码
  "PLAYER": {
    "fireRate": 1,
    "bulletSpeed": 5,
    "trajectoryType": 0,
    "trajectory": [
      [
        90
      ],
      [
        95,
        85
      ],
      [
        100,
        90,
        80
      ],
      [
        105,
        95,
        85,
        75
      ],
      [
        110,
        100,
        90,
        80,
        70
      ]
    ]
  },
csharp 复制代码
	.....
    [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

撞了就透明, 时间结束就恢复

csharp 复制代码
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

csharp 复制代码
    /// <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格式更加方便

csharp 复制代码
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

csharp 复制代码
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)

csharp 复制代码
	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; }
csharp 复制代码
{
  "PLAYER": {
    "fireRate": 1,
    "bulletSpeed": 5,
    "trajectoryType": 0,
    "trajectory": [
      [
        90
      ],
      [
        95,
        85
      ],
      [
        100,
        90,
        80
      ],
      [
        105,
        95,
        85,
        75
      ],
      [
        110,
        100,
        90,
        80,
        70
      ]
    ]
  },

bug 超出界限自动销毁在左右上的缓冲距离

//左右要多一个身位,玩家看不到再销毁

csharp 复制代码
	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()(因为测试,统一改成扣一点血)

csharp 复制代码
/// <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预制体,认的是左边那个

如果右边那个不是一样的,会出问题(除了锚点靠边靠四角较少问题,其它的很容易越界,翻转,大小缩放不理想)

bug Can't remove RectTransform because Image (Script), Image (Script) depends on it

Unity报错:Destroy销毁物体失败

完结

HeroSelect

Strength强化

LevelSelect 和Loading

Battle

子弹列数的奖励(概率低,上面没吃到)

相关推荐
Envyᥫᩣ2 小时前
C#语言:从入门到精通
开发语言·c#
小春熙子6 小时前
Unity图形学之Shader结构
unity·游戏引擎·技术美术
Footprint_Analytics7 小时前
Footprint Analytics 助力 Sei 游戏生态增长
游戏·web3·区块链
IT技术分享社区8 小时前
C#实战:使用腾讯云识别服务轻松提取火车票信息
开发语言·c#·云计算·腾讯云·共识算法
Sitarrrr9 小时前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧9 小时前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
半盏茶香11 小时前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
△曉風殘月〆15 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm
逐·風16 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i18 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl