【用unity实现100个游戏之12】unity制作一个俯视角2DRPG《类星露谷物语》资源收集游戏demo

前言

采集收集生存类游戏一直是我的最爱,今天就来用unity制作一个俯视角2DRPG类星露谷物语资源收集游戏

先来看看最终效果
在这里插入图片描述

加快编辑器运行速度

修改项目配置
在这里插入图片描述
这样,运行时我们就不需要再等待很久了

素材

(1)场景人物

https://cypor.itch.io/12x12-rpg-tileset
在这里插入图片描述

(2)工具

https://dantepixels.itch.io/tools-asset-16x16
在这里插入图片描述

一、人物移动和动画切换

这个直接功能实现我之前炸弹人那期文章已经说过了,直接抄答案就行了,这里就不再重复介绍了,具体实现可以看文章:
【用unity实现100个游戏之8】用Unity制作一个炸弹人游戏
在这里插入图片描述

最终效果(这里用TileMap简单绘制了一下地图,TileMap的使用可以看我主页之前的文章)
在这里插入图片描述

在这里插入图片描述

二、走路灰尘粒子效果

探究

要实现走路灰尘效果,Unity的粒子系统(Particle System)中有属性RateOverDistance:根据移动距离发射粒子,不移动时不发射。恰好可满足当前需求

实际使用时发现,不管怎么移动都不发射粒子,但RateOverTime(随时间推移发射粒子)的功能是正常的

解决方案
粒子系统有一属性:EmitterVelocity(发射器速度模式),它有2种模式

Transform:通过Transform中Position的变化计算粒子系统的移动速度
Rigidbody:将刚体(若有)的速度作为粒子系统的移动速度

看了上述解释即可想到,若EmitterVelocity设置为Rigidbody模式,当该粒子系统没有刚体时,系统会认为该发射器是不动的,因此移动速度为0,因此移动距离为0:因此RateOverDistance不会发射粒子

所以将EmitterVelocity(发射器速度模式)设置为Transform(变换)即可

实现

素材图片
在这里插入图片描述
材质
在这里插入图片描述

完整粒子系统配置
在这里插入图片描述

在这里插入图片描述
效果
在这里插入图片描述

三、树木排序设计

我们希望实现人物走到树前,人物遮挡树木,当人物走到树后,树又遮挡玩家

如果我们直接修改图层排序方式肯定是不行的,玩家要么直接被遮挡,要么直接遮挡树木

当然你可以通过代码的方式,动态的修改人物图层排序值,当时太麻烦了,这里我们就不探讨了

方法一

最简单的方法就是将树叶和树根分开,树叶图层排序比人物高,树根图层排序比人物低,当然这样绘制会比较麻烦一些
在这里插入图片描述
效果
在这里插入图片描述

方法二

使用透视排序。也就是“伪造”透视图。根据直觉,玩家希望角色在立方体前面时首先绘制角色,而角色在立方体后面时最后绘制角色。

如果用更技术性的语言来说,你需要做的是指示 Unity 根据游戏对象的 y 坐标来绘制游戏对象。屏幕上位置较低的游戏对象(y 坐标较小)应在屏幕上位置较高的游戏对象(y 坐标较大)之后绘制。这样将使位置较低的对象显示在上层。

要指示 Unity 根据游戏对象的 y 坐标来绘制游戏对象,请执行以下操作

  • 选择 Edit > Project Settings。

  • 在左侧类别菜单中,单击 Graphics

  • 在 Camera Settings 中,找到 Transparency Sort Mode (透明度排序模式)字段。此字段决定了精灵的绘制顺序。使用下拉菜单将此设置从 Default 更改为 Custom Axis(自定义轴),修改Transparency Sort Axis(透明排序轴)为(0,1,0),告诉Unity y轴绘制精灵

在这里插入图片描述

  • 找到树木的 Sprite Sort Point (Sprite 排序点)字段。目前,此字段设置为 Center,这意味着会使用精灵的中心点来决定这个游戏对象应该在另一个游戏对象的前面还是后面。将 Sprite Sort Point (Sprite 排序点)更改为 Pivot(轴心)
    注意:记得树木图层排序顺序要和主角人物设置为一样
    在这里插入图片描述
  • 修改树木图片的轴心位置到树木根部
    在这里插入图片描述
    这样就实现了人物在树木轴心下面,先绘制树木,人物在轴心以上,先绘制角色
    在这里插入图片描述

四、绘制拿工具的角色动画

记得配置好动画后,修改为横定曲线,让动画过渡更加丝滑
在这里插入图片描述
效果
在这里插入图片描述
其他配置同理,并加入攻击动画和代码控制切换

if(Input.GetKeyDown(KeyCode.F)){
    
    
	animator.SetBool("isAttack", true);
}
if(Input.GetKeyUp(KeyCode.F)){
    
    
    animator.SetBool("isAttack", false);
}

动画控制器
在这里插入图片描述

最终效果
在这里插入图片描述

五、砍树实现

给树木添加代码,我已经加了详细注释就不过多解释了,其中使用了DOTween实现生成资源的弹出动画,不懂DOTween的小伙伴可以看我之前的文章

using UnityEngine;
using DG.Tweening;

public class TreeController : MonoBehaviour
{
    
    
    public GameObject woodPrefab;   // 木头预制体
    public int minWoodCount = 2;    // 随机掉落的最小木头数量
    public int maxWoodCount = 3;    // 随机掉落的最大木头数量
    public int maxAttackCount = 3;  // 最大攻击次数
    public int maxCreateCount = 3;  // 最大生成物品次数
    public float maxOffsetDistance = 1f;  // 随机偏移的最大距离

    private int currentCreateCount; // 生成物品次数
    private int currentAttackCount; // 当前攻击次数

    void Start()
    {
    
    
        currentAttackCount = 0;
        currentCreateCount = 0;
    }

    private void OnTriggerEnter2D(Collider2D other) {
    
    
        //碰到带Axe标签物体
        if(other.CompareTag("Axe")){
    
    
            //攻击三次生成物品
            if(currentAttackCount >= maxAttackCount){
    
    
                TakeDamage();
                currentAttackCount = 0;
            }
            currentAttackCount ++;
        }
    }

    public void TakeDamage()
    {
    
    
        // 每次受到攻击时,生成随机数量的木头
        int woodCount = Random.Range(minWoodCount, maxWoodCount + 1);
        for (int i = 0; i < woodCount; i++)
        {
    
    
            Vector3 randomOffset = new Vector3(Random.Range(-maxOffsetDistance, maxOffsetDistance), Random.Range(-maxOffsetDistance, maxOffsetDistance), 0f);
            GameObject wood = Instantiate(woodPrefab, transform.position + randomOffset, Quaternion.identity);
            
            // 使用DOJump方法实现物体的弹跳
            wood.transform.DOJump(wood.transform.position + new Vector3(randomOffset.x, randomOffset.y, 0f), 2, 1, 1).SetEase(Ease.OutSine);
        }

        currentCreateCount++;

        // 如果生成物品次数达到最大值,则销毁木头并重置生成物品次数
        if (currentCreateCount >= maxCreateCount)
        {
    
    
            Destroy(gameObject);
            currentCreateCount = 0;
        }
    }
}

效果
在这里插入图片描述

六、存储拾取物品

使用ScriptableObject定义物品

using UnityEngine;

[CreateAssetMenu(fileName = "Resource", menuName = "GathererTopDownRPG/Resource")]
public class Resource : ScriptableObject
{
    
    
    [field: SerializeField] public string DisplayName {
    
     get; private set; }
    [field: SerializeField] public Sprite Icon {
    
     get; private set; }
    [field: SerializeField] public string Description {
    
     get; private set; }
    [field: SerializeField] public float Value {
    
     get; private set; }
}

新建ScriptableObject物品,配置参数
在这里插入图片描述
物品脚本

using UnityEngine;

public class ResourcePickup : MonoBehaviour
{
    
    
    [field: SerializeField] public Resource ResourceType {
    
     get; private set; }
}

木头挂载脚本,并配置对应的ScriptableObject
在这里插入图片描述

引入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;
	}
}

库存基类

TryGetValue 方法会尝试从字典中获取指定键 type 对应的值。如果找到了该键,它会将对应的值赋值给 currentCount 变量,并返回 true。

using UnityEngine;

public class Inventory : MonoBehaviour
{
    
    
    [field: SerializeField] private SerializableDictionary <Resource, int> Resources {
    
     get; set;}
    //<summary>
    // 查找并返回字典中某个资源的数量
    ///</summary>
    //<param name="type">要检查的资源类型</param>
    //<returns>如果有资源则返回其数量,否则返回0</returns>
    public int GetResourceCount(Resource type)
    {
    
    
        if (Resources.TryGetValue(type, out int currentCount))
            return currentCount;
        else
            return 0;
    }

    /// <summary>
    /// 向字典中添加资源
    /// </summary>
    /// <param name="type">要添加的资源类型</param>
    /// <param name="count">要添加的数量</param>
    /// <returns>成功添加的数量</returns>
    public int AddResources(Resource type, int count)
    {
    
    
        if (Resources.TryGetValue(type, out int currentCount))
        {
    
    
            return Resources[type] += count;
        }
        else
        {
    
    
            Resources.Add(type, count);
            return count;
        }
    }
}

拾取物品
在这里插入图片描述
拾取物品代码,挂载在人物身上

using UnityEngine;

public class PickupResources : MonoBehaviour
{
    
    
    [field: SerializeField] public Inventory Inventory {
    
     get; private set; }
    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        ResourcePickup pickup = collision.gameObject.GetComponent<ResourcePickup>();
        if (pickup)
        {
    
    
            Inventory.AddResources(pickup.ResourceType, 1);
            Destroy(pickup.gameObject);
        }
    }
}

在这里插入图片描述
运行效果
在这里插入图片描述

检测是否入库,数量是否增加
在这里插入图片描述

七、实现靠近收获物品自动吸附

新增两个节点,一个为吸附范围,一个为拾取物品范围,同时去除原本角色的PickupResources脚本挂载
在这里插入图片描述
在这里插入图片描述

编写吸附脚本PickupGravity

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PickupGravity : MonoBehaviour
{
    
    
    public float GravitySpeed = 5f;
    private List<ResourcePickup> _nearbyResources = new();
    private void FixedUpdate()
    {
    
    
        foreach (ResourcePickup pickup in _nearbyResources)
        {
    
    
            Vector2 directionToCenter = (transform.position - pickup.transform.position).normalized;
            pickup.transform.Translate(directionToCenter * GravitySpeed * Time.fixedDeltaTime);
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        ResourcePickup pickup = collision.gameObject.GetComponent<ResourcePickup>();
        if (pickup) _nearbyResources.Add(pickup);
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
    
    
        ResourcePickup pickup = collision.gameObject.GetComponent<ResourcePickup>();
        if (pickup) _nearbyResources.Remove(pickup);
    }
}

也可以使用DOtween实现,会更加简单

using UnityEngine;
using DG.Tweening;
public class PickupGravity : MonoBehaviour
{
    
    
    public float speed = 0.6f;

    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        ResourcePickup pickup = collision.gameObject.GetComponent<ResourcePickup>();
        if (pickup){
    
    
            pickup.transform.DOMove(transform.position, speed);
        }
    }
}

挂载脚本
在这里插入图片描述
在这里插入图片描述
运行效果
在这里插入图片描述

八、树木被砍掉的粒子效果

粒子效果配置,材质和颜色选择自己想要的就行
在这里插入图片描述

在这里插入图片描述
代码调用

public GameObject cutDownParticleSystem;//粒子效果

//生成特效
Instantiate(cutDownParticleSystem, transform.position, Quaternion.identity);

效果
在这里插入图片描述

九、新增更多可收集物

直接按前面的树木生成预制体变体
在这里插入图片描述
修改相应的配置即可,最终效果
在这里插入图片描述

十、更多工具切换

新增ScriptableObject Tool类,定义不同类型的工具

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;



[CreateAssetMenu(fileName = "Tool", menuName = "GathererTopDownRPG/Tool")]
public class Tool : ScriptableObject
{
    
    
    [field: SerializeField] public string DisplayName {
    
     get; private set; }
    [field: SerializeField] public Sprite sprite {
    
     get; private set; }
    [field: SerializeField] public Sprite Icon {
    
     get; private set; }
    [field: SerializeField] public string Description {
    
     get; private set; }
    [field: SerializeField] public ToolType toolType {
    
     get; private set; }
    [field: SerializeField] public int minCount {
    
     get; private set; } = 1;//随机掉落的最小资源数量
    [field: SerializeField] public int maxCount {
    
     get; private set; } = 1;//随机掉落的最大资源数量
}


// 在此添加其他工具类型
public enum ToolType
{
    
    
    Axe,//斧头
    Pickaxe,//镐子
}

在这里插入图片描述
新增工具代码,挂载在工具节点上

public class ToolController : MonoBehaviour
{
    
    
    public Tool tool;
}

修改可破坏物(树,石头)的代码,及TreeController脚本

public ToolType toolType;

private void OnTriggerEnter2D(Collider2D other)
{
    
    
    //碰到带Axe标签物体
    if (other.CompareTag("Axe"))
    {
    
    
        //只有对应的工具才可以
        if (other.GetComponent<ToolController>().tool.toolType == toolType)
        {
    
    
        		//。。。
                TakeDamage(other.GetComponent<ToolController>().tool);
                //。。。
        }
        else
        {
    
    
            Debug.Log("工具不对");
        }
    }
}

public void TakeDamage(Tool tool)
{
    
    
     int minWoodCount = tool.minCount;    // 随机掉落的最小木头数量
     int maxWoodCount = tool.maxCount;    // 随机掉落的最大木头数量
}

ps:我这里只配置了不同武器的 随机掉落的数量,你可以将其他数据也进行不同的工具配置,如最大攻击次数,最大生成物品次数

页面绘制不同武器的切换按钮
在这里插入图片描述
切换工具按钮脚本,脚本挂载各个按钮上即可

using UnityEngine;
using UnityEngine.UI;

//切换工具
public class SwitchTool : MonoBehaviour
{
    
    
    public Tool tool;
    private ToolController toolController;

    private void Start()
    {
    
    
        toolController = FindObjectOfType<PlayerController>().GetComponentInChildren<ToolController>();
        //修改当前按钮icon图片
        GetComponent<Image>().sprite = tool.Icon;
        //绑定点击事件
        GetComponent<Button>().onClick.AddListener(ChangeTool);
    }

    //切换工具
    public void ChangeTool()
    {
    
    
        if (toolController != null)
        {
    
    
            if(toolController.GetComponent<SpriteRenderer>().sprite != tool.sprite){
    
    
                toolController.tool = tool;
                //修改工具角色手拿工具图片
                toolController.GetComponent<SpriteRenderer>().sprite = tool.sprite;
            }else{
    
    
                toolController.tool = null;
                toolController.GetComponent<SpriteRenderer>().sprite = null;
            }
            
        }else{
    
    

            Debug.LogWarning("没找到目标ToolController组件");
        }
    }
}

最终效果
在这里插入图片描述

十一、扩展

音乐音效系统
背包系统
经验血量系统
制作系统,不同等级可以解锁不同品质的工具
种植系统
建造系统
钓鱼、烹饪系统
天气、四季变化系统
任务系统
打怪系统
种子、商城系统

后续大家可以自行扩展,这里就不过多赘述了。至于后续是否继续完善开发,就看大家想不想看了,点赞越多更新越快哦!

源码

整理好后我会放上来

完结

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

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

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_36303853/article/details/132921209