【制作100个unity游戏之26】unity2d横版卷轴动作类游13(附带项目源码)

最终效果

在这里插入图片描述

系列导航

文章目录

  • 最终效果
  • 系列导航
  • 前言
  • 存储点
  • 灯光
  • 后处理
  • 存储位置信息
  • 存储更多数据
  • 存储场景信息
  • 持久化存储数据
    • 引入Unity 的可序列化字典类
    • 调用
  • 游戏结束
  • 源码
  • 完结

前言

欢迎来到【制作100个Unity游戏】系列!本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第26篇中,我们将探索如何用unity制作一个unity2d横版卷轴动作类游戏,我会附带项目源码,以便你更好理解它。

本节主要实现灯光 后处理 存储和持久化存储

存储点

存储点的实现和宝箱类似

新增SavePoint

public class SavePoint : MonoBehaviour, IInteractable {
    private SpriteRenderer spriteRenderer;
    public Sprite darkSprite;
    public Sprite lightSprite;
    public bool isDone;

    private void Awake() {
        spriteRenderer = GetComponent<SpriteRenderer>();    
    }

    private void OnEnable() {
        spriteRenderer.sprite = isDone ? lightSprite : darkSprite;
    }

    public void TriggerAction()
    {
        if(!isDone){
            spriteRenderer.sprite = lightSprite;
            GetComponent<Collider2D>().enabled = false;
            isDone = true;

			Save();
        }
    }

    //存储数据
    private void Save(){
        Debug.Log("存储数据");
    }
}

配置
在这里插入图片描述

效果
在这里插入图片描述

灯光

具体可以查看文章:【实现100个unity特效之6】Unity2d光源的使用

调低全局灯光
在这里插入图片描述
石头添加点灯光
在这里插入图片描述
效果
在这里插入图片描述

后处理

后处理效果,我之前也做过不少,感兴趣的可以回头去看看
【用unity实现100个游戏之14】Unity2d做一个建造与防御类rts游戏
在这里插入图片描述
unity实战】3D水系统,游泳,潜水,钓鱼功能实现
在这里插入图片描述

为了方便测试,记得勾选显示后处理效果,默认都是勾选的
在这里插入图片描述
主相机勾选渲染后处理
在这里插入图片描述
添加一些简单的后处理效果

在这里插入图片描述
实现上面区域比下面区域亮
在这里插入图片描述
效果
在这里插入图片描述

存储位置信息

新增Data

public class Data
{
    /// <summary>
    /// 存储角色位置信息的字典,键为角色名称,值为对应的位置坐标(Vector3)。
    /// </summary>
    public Dictionary<string, Vector3> characterPosDict = new Dictionary<string, Vector3>();
}

新增DataManager,为了保证Data Manager可以优先其他代码执行,为它添加特性[DefaultExecutionOrder(-100)]。很多小伙伴没有留意后面会提到的这个内容,发现有ISaveable的注册报错。[DefaultExecutionOrder(-100)] 是 Unity 中的一个属性,用于指定脚本的默认执行顺序。参数 -100 表示该脚本的执行顺序优先级,数值越小,优先级越高,即越先执行。

新输入系统获取键盘的输入,按下L按键读取一下进度。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

//指定脚本的默认执行顺序,数值越小,优先级越高
[DefaultExecutionOrder(-100)]
public class DataManager : MonoBehaviour
{
    public static DataManager instance;

    [Header("事件监听")]
    public VoidEventSO saveDataEvent; // 保存数据事件

    /// <summary>
    /// 存储需要保存数据的 ISaveable 实例的列表。
    /// </summary>
    private List<ISaveable> saveableList = new List<ISaveable>();

    /// <summary>
    /// 保存数据到 Data 对象中。
    /// </summary>
    private Data saveData;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(gameObject);
        }
        saveData = new Data();
    }

    private void Update()
    {
        // 按L 加载测试
        if(Keyboard.current.lKey.wasPressedThisFrame){
            Debug.Log("加载");
            Load();
        }
    }

    /// <summary>
    /// 注册需要保存数据的 ISaveable 实例。
    /// </summary>
    /// <param name="saveable">需要保存数据的 ISaveable 实例。</param>
    public void RegisterSaveData(ISaveable saveable)
    {
        if (!saveableList.Contains(saveable))
        {
            saveableList.Add(saveable);
        }
    }

    public void UnRegisterSaveData(ISaveable saveable){
		if (saveableList.Contains(saveable))
      	{
           // 如果在,就从列表中移除
           saveableList.Remove(saveable);
       	}
	}

    private void OnEnable()
    {
        saveDataEvent.OnEventRaised += Save; // 监听保存数据事件
    }

    private void OnDisable()
    {
        saveDataEvent.OnEventRaised -= Save; // 取消监听保存数据事件
    }

    /// <summary>
    /// 保存数据。
    /// </summary>
    public void Save()
    {
        foreach (var saveable in saveableList)
        {
            saveable.GetSaveData(saveData);
        }
    }

    /// <summary>
    /// 加载数据并应用到相应的 ISaveable 实例中。
    /// </summary>
    public void Load()
    {
        foreach (var saveable in saveableList)
        {
            saveable.LoadData(saveData);
        }
    }
}

挂载配置
在这里插入图片描述

新增接口ISaveable

public interface ISaveable
{
    DataDefination GetDataID();
    
    /// <summary>
    /// 将该实例注册到数据管理器以便保存数据。
    /// </summary>
    void RegisterSaveData() => DataManager.instance.RegisterSaveData(this);

    /// <summary>
    /// 将该实例从数据管理器中注销,停止保存数据。
    /// </summary>
    void UnRegisterSaveData() => DataManager.instance.UnRegisterSaveData(this);

    /// <summary>
    /// 获取需要保存的数据并存储到指定的 Data 对象中。
    /// </summary>
    /// <param name="data">保存数据的 Data 对象。</param>
    void GetSaveData(Data data);

    /// <summary>
    /// 从指定的 Data 对象中加载数据并应用到该实例中。
    /// </summary>
    /// <param name="data">包含加载数据的 Data 对象。</param>
    void LoadData(Data data);
}

那么如果有三个野猪的名字完全一样,我们怎么区分每一只野猪具体存储的位置呢,所以接下来我们要创建一个唯一的标识,我们可以直接使用c#为我们设置好的全局唯一标识符,GUID就是个16位的串码,保证它的唯一性
在这里插入图片描述
新增枚举

/// <summary>
/// 指示数据定义的持久化类型。
/// </summary>
public enum PersistentType
{
    /// <summary>
    /// 可读写的持久化类型,数据会被持久化保存。
    /// </summary>
    ReadWrite,

    /// <summary>
    /// 不持久化类型,数据不会被持久化保存。
    /// </summary>
    DoNotPerst
}

新增DataDefination

public class DataDefination : MonoBehaviour
{
    /// <summary>
    /// 持久化类型,指示数据定义的持久化方式。
    /// </summary>
    public PersistentType persistentType;

    /// <summary>
    /// 数据定义的唯一标识符。
    /// </summary>
    public string ID;

    /// <summary>
    /// 当编辑器中的属性值发生更改时调用,用于自动设置默认的ID值。
    /// </summary>
    private void OnValidate()
    {
        if (persistentType == PersistentType.ReadWrite)
        {
            if (ID == string.Empty)
            {
                ID = System.Guid.NewGuid().ToString();
            }
        }
        else
        {
            ID = string.Empty;
        }
    }
}

配置挂载脚本,比如我们放在人物身上,生成唯一的UID
在这里插入图片描述
修改PlayerController,调用接口

public class PlayerController : MonoBehaviour, ISaveable
{
	//...
	
	private void OnEnable()
    {
        ISaveable saveable = this;
        saveable.RegisterSaveData();
    }

    private void OnDisable()
    {
        ISaveable saveable = this;
        saveable.UnRegisterSaveData();
    }
    
    // 获取数据ID,用于唯一标识当前对象的位置信息
    public DataDefination GetDataID()
    {
        return GetComponent<DataDefination>();
    }

    // 将对象的位置信息保存到数据中
    public void GetSaveData(Data data)
    {
        // 检查数据中是否已经存在当前对象的位置信息
        if (data.characterPosDict.ContainsKey(GetDataID().ID))
        {
            // 如果已经存在,则更新位置信息
            data.characterPosDict[GetDataID().ID] = transform.position;
        }
        else
        {
            // 如果不存在,则添加新的位置信息
            data.characterPosDict.Add(GetDataID().ID, transform.position);
        }
    }

    // 从数据中加载对象的位置信息
    public void LoadData(Data data)
    {
        // 检查数据中是否存在当前对象的位置信息
        if (data.characterPosDict.ContainsKey(GetDataID().ID))
        {
            // 如果存在,则将位置信息设置为对应的数值
            transform.position = data.characterPosDict[GetDataID().ID];
        }
    }
}

修改SavePoint,调用存储数据

public class SavePoint : MonoBehaviour, IInteractable {
    private SpriteRenderer spriteRenderer;
    public Sprite darkSprite;
    public Sprite lightSprite;
    public bool isDone;
    public VoidEventSO saveDataEvent; // 保存数据事件

    private void Awake() {
        spriteRenderer = GetComponent<SpriteRenderer>();    
    }

    private void OnEnable() {
        spriteRenderer.sprite = isDone ? lightSprite : darkSprite;
    }

    public void TriggerAction()
    {
        if(!isDone){
            Save();

            spriteRenderer.sprite = lightSprite;
            GetComponent<Collider2D>().enabled = false;
            isDone = true;
        }
    }

    //存储数据
    private void Save(){
        Debug.Log("存储数据");
        saveDataEvent.RaiseEvent();
    }
}

效果,按L测试读取数据,角色回到存储的位置
在这里插入图片描述

存储更多数据

修改Data,定义通用的float的类型,所有和float相关的类型都可用它保存

public class Data
{
    //...
    
    public Dictionary<string, float> floatSaveData = new Dictionary<string, float>();
}

但是如何区分是人物的血条还是能量呢?我们可以加入不同的后缀,修改PlayerController

// 将对象的位置信息保存到数据中
public void GetSaveData(Data data)
{
    // 检查数据中是否已经存在当前对象的位置信息
    if (data.characterPosDict.ContainsKey(GetDataID().ID))
    {
        // 如果已经存在,则更新位置信息
        data.characterPosDict[GetDataID().ID] = transform.position;

        data.floatSaveData[GetDataID().ID + "Health"] = GetComponent<Character>().currentHealth;
        data.floatSaveData[GetDataID().ID + "Power"] = GetComponent<Character>().currentPower;
    }
    else
    {
        // 如果不存在,则添加新的位置信息
        data.characterPosDict.Add(GetDataID().ID, transform.position);

        //存储玩家血量和能量
        data.floatSaveData.Add(GetDataID().ID + "Health", GetComponent<Character>().currentHealth);
        data.floatSaveData.Add(GetDataID().ID + "Power", GetComponent<Character>().currentPower);
    }
}

// 从数据中加载对象的位置信息
public void LoadData(Data data)
{
    // 检查数据中是否存在当前对象的位置信息
    if (data.characterPosDict.ContainsKey(GetDataID().ID))
    {
        // 如果存在,则将位置信息设置为对应的数值
        transform.position = data.characterPosDict[GetDataID().ID];

        GetComponent<Character>().currentHealth = data.floatSaveData[GetDataID().ID + "Health"];
        GetComponent<Character>().currentPower = data.floatSaveData[GetDataID().ID + "Power"];

        //更新血条能量UI
        GetComponent<Character>().OnHealthChanged?.Invoke(GetComponent<Character>());
    }
}

效果
在这里插入图片描述
同理你可以存储其他的比如宝箱,野猪等信息

存储场景信息

修改Data,将场景信息转为json数据进行存取

public string sceneToSave;
    
public void SaveGameScene(SceneField savedScene){
    sceneToSave = JsonUtility.ToJson(savedScene);
}

public SceneField GetSavedScene(){
    SceneField loadedData = JsonUtility.FromJson<SceneField>(sceneToSave);
    return loadedData;
}

修改SavePoint,存储场景信息

public SceneField currentLoadedScene;

public class SavePoint : MonoBehaviour, IInteractable, ISaveable
{
	//...
	
	public DataDefination GetDataID()
	{
	    return null;
	}
	
	public void GetSaveData(Data data)
	{
	    data.SaveGameScene(currentLoadedScene);//存储场景
	}
	
	public void LoadData(Data data)
	{
	    
	}
}

配置当前场景
在这里插入图片描述

修改DataManager,我们希望加载存储场景完成后,再进行其他的LoadData操作,所以加载存储场景的操作我们就不放在LoadData里执行了。可以加入场景过渡渐变,让效果更好,这里我就不加了

/// <summary>
/// 加载数据并应用到相应的 ISaveable 实例中。
/// </summary>
public void Load()
{
    //获取存储的场景
    var scence = saveData.GetSavedScene();
    if (scence != null)
    {
        // 获取当前活动的场景
        Scene activeScene = SceneManager.GetActiveScene();
        // 获取所有加载的场景
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            Scene loadedScene = SceneManager.GetSceneAt(i);
            Debug.Log("Loaded Scene " + i + ": " + loadedScene.name);

            if (activeScene.name != loadedScene.name) SceneManager.UnloadSceneAsync(loadedScene.name); // 异步卸载所有非主场景
        }

        //加载scence场景
        SceneManager.LoadSceneAsync(scence.SceneName, LoadSceneMode.Additive).completed += operation =>
        {
            if (operation.isDone)
            {
                //获取相机边界方法
                cameraControl.GetNewCameraBounds();

                //加载其他数据
                foreach (var saveable in saveableList)
                {
                    saveable.LoadData(saveData);
                }
            }
        };
        //控制按钮的显示隐藏
        sceneLoadTrigger.StartMenu();
    }
}

效果
在这里插入图片描述

持久化存储数据

具体可以看我这篇文章:【unity小技巧】Unity存储存档保存——PlayerPrefs、JsonUtility和MySQL数据库的使用

需要注意的是,Dictionary 类型不能直接序列化为 JSON 字符串,因为 JsonUtility.ToJson() 方法只能序列化 Unity 引擎内置支持的类型。解决这个问题的一种方法是创建一个自定义类。其实我在之前的文章早就有用到这种方法:【用unity实现100个游戏之12】unity制作一个俯视角2DRPG《类星露谷物语、浮岛物语》资源收集游戏(附项目源码)

引入Unity 的可序列化字典类

Unity 无法序列化标准词典。这意味着它们不会在检查器中显示或编辑,
也不会在启动时实例化。一个经典的解决方法是将键和值存储在单独的数组中,并在启动时构造字典。

我们使用gitthub大佬的源码即可,此项目提供了一个通用字典类及其自定义属性抽屉来解决此问题。
源码地址:https://github.com/azixMcAze/Unity-SerializableDictionary

你可以选择下载源码,也可以直接复制我下面的代码,我把主要代码提出来了
SerializableDictionary.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using UnityEngine;

public abstract class SerializableDictionaryBase
{
	public abstract class Storage {}

	protected class Dictionary<TKey, TValue> : System.Collections.Generic.Dictionary<TKey, TValue>
	{
		public Dictionary() {}
		public Dictionary(IDictionary<TKey, TValue> dict) : base(dict) {}
		public Dictionary(SerializationInfo info, StreamingContext context) : base(info, context) {}
	}
}

[Serializable]
public abstract class SerializableDictionaryBase<TKey, TValue, TValueStorage> : SerializableDictionaryBase, IDictionary<TKey, TValue>, IDictionary, ISerializationCallbackReceiver, IDeserializationCallback, ISerializable
{
	Dictionary<TKey, TValue> m_dict;
	[SerializeField]
	TKey[] m_keys;
	[SerializeField]
	TValueStorage[] m_values;

	public SerializableDictionaryBase()
	{
		m_dict = new Dictionary<TKey, TValue>();
	}

	public SerializableDictionaryBase(IDictionary<TKey, TValue> dict)
	{	
		m_dict = new Dictionary<TKey, TValue>(dict);
	}

	protected abstract void SetValue(TValueStorage[] storage, int i, TValue value);
	protected abstract TValue GetValue(TValueStorage[] storage, int i);

	public void CopyFrom(IDictionary<TKey, TValue> dict)
	{
		m_dict.Clear();
		foreach (var kvp in dict)
		{
			m_dict[kvp.Key] = kvp.Value;
		}
	}

	public void OnAfterDeserialize()
	{
		if(m_keys != null && m_values != null && m_keys.Length == m_values.Length)
		{
			m_dict.Clear();
			int n = m_keys.Length;
			for(int i = 0; i < n; ++i)
			{
				m_dict[m_keys[i]] = GetValue(m_values, i);
			}

			m_keys = null;
			m_values = null;
		}
	}

	public void OnBeforeSerialize()
	{
		int n = m_dict.Count;
		m_keys = new TKey[n];
		m_values = new TValueStorage[n];

		int i = 0;
		foreach(var kvp in m_dict)
		{
			m_keys[i] = kvp.Key;
			SetValue(m_values, i, kvp.Value);
			++i;
		}
	}

	#region IDictionary<TKey, TValue>
	
	public ICollection<TKey> Keys {	get { return ((IDictionary<TKey, TValue>)m_dict).Keys; } }
	public ICollection<TValue> Values { get { return ((IDictionary<TKey, TValue>)m_dict).Values; } }
	public int Count { get { return ((IDictionary<TKey, TValue>)m_dict).Count; } }
	public bool IsReadOnly { get { return ((IDictionary<TKey, TValue>)m_dict).IsReadOnly; } }

	public TValue this[TKey key]
	{
		get { return ((IDictionary<TKey, TValue>)m_dict)[key]; }
		set { ((IDictionary<TKey, TValue>)m_dict)[key] = value; }
	}

	public void Add(TKey key, TValue value)
	{
		((IDictionary<TKey, TValue>)m_dict).Add(key, value);
	}

	public bool ContainsKey(TKey key)
	{
		return ((IDictionary<TKey, TValue>)m_dict).ContainsKey(key);
	}

	public bool Remove(TKey key)
	{
		return ((IDictionary<TKey, TValue>)m_dict).Remove(key);
	}

	public bool TryGetValue(TKey key, out TValue value)
	{
		return ((IDictionary<TKey, TValue>)m_dict).TryGetValue(key, out value);
	}

	public void Add(KeyValuePair<TKey, TValue> item)
	{
		((IDictionary<TKey, TValue>)m_dict).Add(item);
	}

	public void Clear()
	{
		((IDictionary<TKey, TValue>)m_dict).Clear();
	}

	public bool Contains(KeyValuePair<TKey, TValue> item)
	{
		return ((IDictionary<TKey, TValue>)m_dict).Contains(item);
	}

	public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
	{
		((IDictionary<TKey, TValue>)m_dict).CopyTo(array, arrayIndex);
	}

	public bool Remove(KeyValuePair<TKey, TValue> item)
	{
		return ((IDictionary<TKey, TValue>)m_dict).Remove(item);
	}

	public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
	{
		return ((IDictionary<TKey, TValue>)m_dict).GetEnumerator();
	}

	IEnumerator IEnumerable.GetEnumerator()
	{
		return ((IDictionary<TKey, TValue>)m_dict).GetEnumerator();
	}

	#endregion

	#region IDictionary

	public bool IsFixedSize { get { return ((IDictionary)m_dict).IsFixedSize; } }
	ICollection IDictionary.Keys { get { return ((IDictionary)m_dict).Keys; } }
	ICollection IDictionary.Values { get { return ((IDictionary)m_dict).Values; } }
	public bool IsSynchronized { get { return ((IDictionary)m_dict).IsSynchronized; } }
	public object SyncRoot { get { return ((IDictionary)m_dict).SyncRoot; } }

	public object this[object key]
	{
		get { return ((IDictionary)m_dict)[key]; }
		set { ((IDictionary)m_dict)[key] = value; }
	}

	public void Add(object key, object value)
	{
		((IDictionary)m_dict).Add(key, value);
	}

	public bool Contains(object key)
	{
		return ((IDictionary)m_dict).Contains(key);
	}

	IDictionaryEnumerator IDictionary.GetEnumerator()
	{
		return ((IDictionary)m_dict).GetEnumerator();
	}

	public void Remove(object key)
	{
		((IDictionary)m_dict).Remove(key);
	}

	public void CopyTo(Array array, int index)
	{
		((IDictionary)m_dict).CopyTo(array, index);
	}

	#endregion

	#region IDeserializationCallback

	public void OnDeserialization(object sender)
	{
		((IDeserializationCallback)m_dict).OnDeserialization(sender);
	}

	#endregion

	#region ISerializable

	protected SerializableDictionaryBase(SerializationInfo info, StreamingContext context) 
	{
		m_dict = new Dictionary<TKey, TValue>(info, context);
	}

	public void GetObjectData(SerializationInfo info, StreamingContext context)
	{
		((ISerializable)m_dict).GetObjectData(info, context);
	}

	#endregion
}

public static class SerializableDictionary
{
	public class Storage<T> : SerializableDictionaryBase.Storage
	{
		public T data;
	}
}

[Serializable]
public class SerializableDictionary<TKey, TValue> : SerializableDictionaryBase<TKey, TValue, TValue>
{
	public SerializableDictionary() {}
	public SerializableDictionary(IDictionary<TKey, TValue> dict) : base(dict) {}
	protected SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) {}

	protected override TValue GetValue(TValue[] storage, int i)
	{
		return storage[i];
	}

	protected override void SetValue(TValue[] storage, int i, TValue value)
	{
		storage[i] = value;
	}
}

[Serializable]
public class SerializableDictionary<TKey, TValue, TValueStorage> : SerializableDictionaryBase<TKey, TValue, TValueStorage> where TValueStorage : SerializableDictionary.Storage<TValue>, new()
{
	public SerializableDictionary() {}
	public SerializableDictionary(IDictionary<TKey, TValue> dict) : base(dict) {}
	protected SerializableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) {}

	protected override TValue GetValue(TValueStorage[] storage, int i)
	{
		return storage[i].data;
	}

	protected override void SetValue(TValueStorage[] storage, int i, TValue value)
	{
		storage[i] = new TValueStorage();
		storage[i].data = value;
	}
}

调用

修改Data,Dictionary 全部改为SerializableDictionary

public class Data
{
    /// <summary>
    /// 存储角色位置信息的字典,键为角色名称,值为对应的位置坐标(Vector3)。
    /// </summary>
    public SerializableDictionary <string, Vector3> characterPosDict = new SerializableDictionary <string, Vector3>();
    public SerializableDictionary <string, float> floatSaveData = new SerializableDictionary <string, float>();
    public SerializableDictionary <string, bool> boolSaveData = new SerializableDictionary <string, bool>();

    public string sceneToSave;
    
    public void SaveGameScene(SceneField savedScene){
        sceneToSave = JsonUtility.ToJson(savedScene);
    }

    public SceneField GetSavedScene(){
        SceneField loadedData = JsonUtility.FromJson<SceneField>(sceneToSave);
        return loadedData;
    }
}

修改DataManager

String savePath = "test.json";

/// <summary>
/// 保存数据。
/// </summary>
public void Save()
{
	//。。。
	
    //持久化存储数据
    String jsonData = JsonUtility.ToJson(saveData);
    File.WriteAllText(savePath, jsonData);
}

/// <summary>
/// 加载数据并应用到相应的 ISaveable 实例中。
/// </summary>
public void Load()
{
    //读取数据
    string jsonData = File.ReadAllText(savePath);
    //将JSON数据反序列化为游戏数据对象
    Data saveData = JsonUtility.FromJson<Data>(jsonData);

    //。。。
}

查看存储的test.json数据
在这里插入图片描述
在这里插入图片描述

效果
在这里插入图片描述

游戏结束

比如触碰水死亡,我们直接加个Attack脚本就可以了,把伤害设置很高
在这里插入图片描述
人物死亡,返回菜单,修改PlayerController

//死亡
public void PlayerDead()
{
    AudioManager.Instance.PlaySFX("人物死亡");
    isDead = true;
    inputControl.Player.Disable();

    //多少秒后重新加载场景
    Invoke("RestartGame", 1.5f);
}

//重新开始
public void RestartGame()
{
    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

效果
在这里插入图片描述

源码

源码不出意外的话我会放在最后一节

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,以便我第一时间收到反馈,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇,https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/651032.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Appium系列(2)元素定位工具appium-inspector

背景 如实现移动端自动化&#xff0c;依赖任何工具时&#xff0c;都需要针对于页面中的元素进行识别&#xff0c;通过识别到指定的元素&#xff0c;对元素进行事件操作。 识别元素的工具为appium官网提供的appium-inspector。 appium-inspector下载地址 我这里是mac电脑需要下…

使用numpy手写一个神经网络

本文主要包含以下内容&#xff1a; 推导神经网络的误差反向传播过程使用numpy编写简单的神经网络&#xff0c;并使用iris数据集和california_housing数据集分别进行分类和回归任务&#xff0c;最终将训练过程可视化。 1. BP算法的推导过程 1.1 导入 前向传播和反向传播的总体…

编程-辅助工具-Git下载

文章目录 1、前言2、Git官网地址3、迅雷下载 1、前言 采用Git能下载github上的代码&#xff0c;其下载是采用官网下载的&#xff0c;但是下载速度比较慢&#xff0c;网上也推荐了镜像的方式&#xff0c;但是有些链接失效了&#xff0c;突然有一天想起用迅雷是不是合适&#xf…

在通过跨网文件交换时,如何保障科研结构核心研究数据?

当今科研领域&#xff0c;数据如同生命线&#xff0c;支撑着每一个突破性发现的诞生。随着国际合作的加深&#xff0c;跨网文件交换成了常态&#xff0c;但这也为科研机构的核心研究数据安全带来了一系列挑战。想象一下&#xff0c;那些精心搜集和分析的宝贵数据&#xff0c;在…

OpenBayes 教程上新 |全球首个开源的文生视频 DiT 模型!对标 Sora,保姆级 Latte 文生视频使用指南

小朋友不爱背诗怎么办&#xff1f;《千秋诗颂》试试看。 2 月 26 日&#xff0c;中国首部文生视频 AI 系列动画《千秋诗颂》于 CCTV-1 频道正式播出&#xff0c;这部动画由上海人工智能实验室和「央妈」&#xff08;中央广播电视总台&#xff09;强强联手&#xff0c;借助「央视…

Discuz!X3.4论坛网站公安备案号怎样放到网站底部?

Discuz&#xff01;网站的工信部备案号都知道在后台——全局——站点信息——网站备案信息代码填写&#xff0c;那公安备案号要添加在哪里呢&#xff1f;并没有看到公安备案号填写栏&#xff0c;今天驰网飞飞和你分享 1&#xff09;工信部备案号和公安备案号统一填写到网站备案…

安装appium自动化测试环境,我自己的版本信息

教程来自&#xff1a;Appium原理与安装 - 白月黑羽 我的软件的版本&#xff1a; 安装是选择为自己安装而不是选all user pip install appium-python-client命令在项目根目录下安装appium-python-client sdk的话最简单的安装方式就是去Android官网下一个android studio然后在…

强化学习_06_pytorch-PPO2实践(Humanoid-v4)

一、PPO优化 PPO的简介和实践可以看笔者之前的文章 强化学习_06_pytorch-PPO实践(Pendulum-v1) 针对之前的PPO做了主要以下优化&#xff1a; -笔者-PPO笔者-PPO2refdata collectone episodeseveral episode(one batch)activationReLUTanhadv-compute-compute adv as one seri…

鸿蒙ArkTS声明式开发:跨平台支持列表【按键事件】

按键事件 按键事件指组件与键盘、遥控器等按键设备交互时触发的事件&#xff0c;适用于所有可获焦组件&#xff0c;例如Button。对于Text&#xff0c;Image等默认不可获焦的组件&#xff0c;可以设置focusable属性为true后使用按键事件。 说明&#xff1a; 开发前请熟悉鸿蒙开…

【STM32踩坑】HAL固件库版本过高导致烧录后无法运行问题

问题引入 目前STM32CUBEMX已经更新到了6.11版本&#xff0c;对应的固件库也一直在更新&#xff1b; 以STM32F1库为例&#xff0c;目前最新的库对应版本为1.8.5 但是我们会发现&#xff0c;如果直接使用1.8.5版本的固件库生成HAL源码后&#xff0c;烧录是可以烧录&#xff0c;但…

【IC】良率模型-yield model

缺陷密度Default Density(D0),表示单位面积的缺陷数D。 单位面积有M个部件,一个部件的平均失效率为: 一个面积为A的系统(芯片)良率: Possion模型: 当M趋于无穷时,系统良率为possion模型。 Murphy模型:(D~对称三角分布) 大芯片或大系统possion模型预估良率过于…

Python自动化测试进阶:性能测试与持续集成实践

Python自动化测试进阶包括性能测试和持续集成实践两个关键方面。以下是对这两个领域的简要介绍&#xff0c;并附带一些示例代码。 性能测试 性能测试是评估软件在特定条件下的性能表现的过程。对于Python应用程序&#xff0c;可以使用一些工具来进行性能测试&#xff0c;例如ps…

linux centos tomcat启动内存泄漏

tomcat启动内存泄漏 经过与开发沟通&#xff0c;结果是开发写了死循环&#xff0c;导致内存泄漏&#xff0c;上一次是开发少打了一个jar包&#xff0c;让开发查代码就行&#xff0c;重新更新代码

C++学习/复习6---内存管理(数据的位置/大小)/new、delete/内存相关面试题(malloc与new/内存泄漏)

一、内存中区域 1.不同数据的存储位置 2.数据占用内存大小 二、动态内存函数 三、new与delete 1.操作内置类型 2.操作自定义类型 四、operator new与operator delete 1.底层源码&#xff08;malloc/free&#xff09; 2.内置/自定义与构造和析构 3.举例 五、定位new表达式 1.举…

TiDB学习3:TiKV

目录 1. TiKV架构和作用 2. RocksDB 2.1 写入 2.2 查询 2.3 Column Families列簇 3. 分布式事务 3.1 事务流程 3.2 分布式事务流程 3.3 MVCC 4. Raft与Multi Raft 4.1 Raft日志复制 4.2 Raft Leader选举 5. TiKV- 读写 5.1 数据的写入 5.2 数据的读取ReadIndex …

教育大模型的发展现状、创新架构及应用展望

引言 从通用大模型到教育领域的专用大模型&#xff0c;是人工智能大模型技术深化发展的必然趋势。教育大模型不是在通用大模型基础上的微调和优化&#xff0c;而是以重构未来教育图景为目标、以开放算法模型架构为基础、以创新教育应用场景为核心的系统性变革。如何厘清教育大…

Linux汉化Jupyter Notebook

要在Linux系统中使Jupyter Notebook汉化&#xff0c;可以通过安装jupyterlab-language-pack-zh-CN扩展来实现。以下是具体步骤和示例代码&#xff1a; 打开终端。 执行以下命令以安装Jupyter Notebook的中文语言包&#xff1a; pip install jupyterlab-language-pack-zh-CN …

实现UI显示在最上面的功能

同学们肯定遇到过UI被遮挡的情况&#xff0c;那如何让UI显示在最前面呢&#xff0c;先看效果 原理:UI的排序方式是和unityHierarchy窗口的层级顺序有关的&#xff0c;排序在下就越后显示&#xff0c;所以按照这个理论&#xff0c;当我们鼠标指到UI的时候把层级设置到最下层就好…

香橙派 AIpro开发体验:使用YOLOV8对USB摄像头画面进行目标检测

香橙派 AIpro开发体验&#xff1a;使用YOLOV8对USB摄像头画面进行目标检测 前言一、香橙派AIpro硬件准备二、连接香橙派AIpro1. 通过网线连接路由器和香橙派AIpro2. 通过wifi连接香橙派AIpro3. 使用vscode 通过ssh连接香橙派AIpro 三、USB摄像头测试1. 配置ipynb远程开发环境1.…

超越连接:ZL-450边缘网关全面评测与应用案例

前言 在现代工业自动化和智能设备管理的背景下&#xff0c;对实时数据通信与设备监控的需求日益增加。ZL450边缘网关作为一款先进的串口通信解决方案&#xff0c;不仅满足了这些要求&#xff0c;还通过其多样的连接性和高效的数据处理能力&#xff0c;为企业带来了显著的效率提…