这里记录各种开发时遇到的问题和解决办法
持續更新



目录

  • 知识点
  • DoTween
  • UnityEngine.Events
  • HashSet
  • 多场景配置方法
  • 获取当前场景
  • 光标划入划出点击交互(接口)
  • 光标点击交互(光标类)
  • 数据保存和加载
  • 导入Newtonsoft包
  • 创建存储接口
  • 创建数据类
  • 创建存储加载数据管理类
  • 使用样例
  • 动态根据枚举名称获取枚举值
  • 添加音乐音效
  • 打包注意
  • 打包配置
  • Q&A
  • MoveTool点击不了
  • (float)(70 / 100) = 0
  • shaderGraph透明通道无效
  • 文字模糊
  • 协程未执行


知识点

DoTween

动作插件,可以再UnityStore中导入
这次用到的是DOPunchRotation来回旋转的效果

gearSprite.DOPunchRotation(Vector3.forward * 180, 1, 1, 0);

unity 图片fillAmount跟随倒计时_unity

UnityEngine.Events

可以在属性栏中匹配响应事件

// 定义
public UnityEvent OnFinish; //小游戏结束事件
// 触发
OnFinish?.Invoke();

响应,直接拖入要响应的节点和方法就可以了,还挺方便的

unity 图片fillAmount跟随倒计时_Source_02


但这种需要明确知道响应对象,感觉还是订阅-发布者模式好点

HashSet

想使用List但是不想元素有重复,可以用HashSet

多场景配置方法

这次学到了一个多场景的配置方法

unity 图片fillAmount跟随倒计时_json_03

  • 建一个公用场景可以命名为Persistent,将一些类似GameManager需要持续使用的放到这个场景中
  • 其他场景创建后直接拖入到Hierarchy窗口,删除场景中camera,因为Persistent场景中有了
  • 可以通过右键场景选择切换Load和Unload来进行操作
  • 代码中切换场景如下
yield return SceneManager.UnloadSceneAsync(fromScene); //关闭激活当前场景
 yield return SceneManager.LoadSceneAsync(toScene, LoadSceneMode.Additive); //使用additive方式加载场景

获取当前场景

首先引用命名空间

using UnityEngine.SceneManagement;

就可以获取了

var currentScene = SceneManager.GetActiveScene().name;

光标划入划出点击交互(接口)

这里引入了UnityEngine.EventSystems
让需要实现交互的类实现IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler这三个接口, 分别是指针点击,指针移入,指针移出
接着直接实现接口方法就可以了

// 鼠标点击事件
public void OnPointerClick(PointerEventData eventData){}
// 鼠标划入事件
public void OnPointerEnter(PointerEventData eventData){}
// 鼠标划出事件
public void OnPointerExit(PointerEventData eventData){}

光标点击交互(光标类)

使用上面的方法应该也可以,但如果有很多需要交互的类都来实现接口,可能有些麻烦
将需要交互的物品添加上碰撞体, 并设置tag
创建一个光标类, 这个类将每帧获取光标位置,通过Physics2D.OverlapPoint判断当前位置是否有碰撞体,并根据碰撞体的tag进行相应的操作
贴下代码吧

///<summary>
///Author: Qiu
///Describe: 光标控制器
///</summary>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.PlayerLoop;

public class CursorManager : MonoBehaviour
{

    // 鼠标所在位置的世界坐标
    private Vector3 cursorWorldPos => Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0));
    private ItemName selectedItemName; //选择使用的物品名称

    public RectTransform hand; //手

    void OnEnable()
    {
        EventHandler.ItemSelectedEvent += OnItemSelectedEvent;
        EventHandler.ItemUseEvent += OnItemUseEvent;
        EventHandler.ItemChangeEvent += OnItemChangeEvent;
        EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
    }

    void OnDisable()
    {
        EventHandler.ItemSelectedEvent -= OnItemSelectedEvent;
        EventHandler.ItemUseEvent -= OnItemUseEvent;
        EventHandler.ItemChangeEvent -= OnItemChangeEvent;
        EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
    }

    private void OnBeforeSceneUnloadEvent()
    {
        OnItemSelectedEvent(null, false);

    }

    private void OnItemUseEvent(ItemName name)
    {
        OnItemSelectedEvent(null, false);
    }


    private void OnItemSelectedEvent(ItemDetails details, bool isSelected)
    {
        selectedItemName = isSelected ? details.itemName : ItemName.None;
        hand.gameObject.SetActive(isSelected);
    }

    private void OnItemChangeEvent(int index)
    {
        OnItemSelectedEvent(null, false);
    }

    private void Update()
    {
        GameObject clickObj = ObjectAtMousePosition()?.gameObject;

        // 鼠标手已激活
        if (hand.gameObject.activeInHierarchy)
        {
            hand.position = Input.mousePosition;
        }

        if (InteractWithUI()) return;

        // 鼠标点击 && 有碰撞体 
        if (clickObj && Input.GetMouseButtonDown(0))
        {
            // 检测鼠标互动情况
            ClickAction(clickObj);
        }
    }

    // 根据点击节点标签判断互动类型
    private void ClickAction(GameObject clickObj)
    {
        switch (clickObj.tag)
        {
            // 场景传送
            case "Teleport":
                var teleport = clickObj.GetComponent<Teleport>();
                teleport?.TeleportToScene();
                break;
            // 物品
            case "Item":
                var item = clickObj.GetComponent<Item>();
                item?.ItemClicked();
                break;
            // 交互
            case "Interactive":
                var interactive = clickObj.GetComponent<Interactive>();
                interactive.CheckInteractive(selectedItemName);
                break;

            default: break;
        }
    }

    // 获取鼠标所在位置的碰撞体 
    private Collider2D ObjectAtMousePosition()
    {
        // 根据世界坐标获取碰撞体
        return Physics2D.OverlapPoint(cursorWorldPos);
    }

    // 判断当前是否与UI互动
    // 解决穿透点击问题
    private bool InteractWithUI()
    {
        if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

数据保存和加载

导入Newtonsoft包

这次用的是Newtonsoft Json这种和JsonUnity优点是可以直接序列表列和字典

导入方法是在Package Manager中右上角加号选择"Add Package From Git URL"

unity 图片fillAmount跟随倒计时_游戏引擎_04


输入com.untiy.nuget.newtonsoft-json等会儿就好了

创建存储接口

需要保存或拿数据的类可以通过实现这个接口用来自定义需要保存的数据和需要获取的数据内容

public interface ISaveable
{
    // untiy2022版本可以支持接口内直接实现方法
    // void SaveableRegister()
    // {
    //     SaveLoadManager.Instance.Register(this);
    // }

    //生成存储数据
    GameSaveData GenerateSaveData();

    // 加载数据
    void RestoreGameData(GameSaveData saveData);
}
创建数据类

这个类里列出了所有需要保存的数据

using System.Collections.Generic;

/// <summary>
/// 游戏需要保存的数据
/// </summary>
public class GameSaveData
{
    public int gameWeek;
    public SceneName curScene;
    public Dictionary<ItemName, bool> itemAvailableDict = new Dictionary<ItemName, bool>();
    public Dictionary<string, bool> interactiveStateDict = new Dictionary<string, bool>();
    public Dictionary<SceneName, bool> miniGameIsPassDict = new Dictionary<SceneName, bool>();
    public List<ItemName> itemList = new List<ItemName>();
}
创建存储加载数据管理类

这个类实现了加载和存储的功能,以及保存所有需要进行存储加载数据的类
将数据用key=类名 value=数据的方式进行保存, 加载数据时就通过key值调取相应类中实现的接口方法来加载保存数据

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using System.IO;

public class SaveLoadManager : Singleton<SaveLoadManager>
{
    private string jsonFolder;

    private List<ISaveable> saveables = new List<ISaveable>();

    private Dictionary<string, GameSaveData> saveDataDict = new Dictionary<string, GameSaveData>();


    protected override void Awake()
    {
        base.Awake();
        jsonFolder = Application.persistentDataPath + "/SAVE/"; //保存路径不同系统都不同 需要查看手册
    }

    private void OnEnable()
    {
        EventHandler.StartNewGameEvent += OnStartNewGameEvent;
    }

    private void OnDisable()
    {
        EventHandler.StartNewGameEvent -= OnStartNewGameEvent;
    }

    private void OnStartNewGameEvent(int gameWeek)
    {
        // 新游戏时删除之前的整个保存数据路径
        var resultPath = jsonFolder + "data.sav";
        if (File.Exists(resultPath))
        {
            File.Delete(resultPath);
        }
    }


    // 注册所有拥有需要保存数据的类
    public void Register(ISaveable saveable)
    {
        saveables.Add(saveable);
    }

    // 存储数据
    public void Save()
    {
        saveDataDict.Clear();
        foreach (var saveable in saveables)
        {
            // 用类名和对应数据对象配对保存
            saveDataDict.Add(saveable.GetType().Name, saveable.GenerateSaveData());
        }
        var resultPath = jsonFolder + "data.sav";
        var jsonData = JsonConvert.SerializeObject(saveDataDict, Formatting.Indented);
        if (!File.Exists(resultPath))
        {
            // 创建文件夹
            Directory.CreateDirectory(jsonFolder);
        }
        // 创建文件并写入数据
        File.WriteAllText(resultPath, jsonData);
    }


    // 加载数据
    public void Load()
    {
        var resultPath = jsonFolder + "data.sav";
        if (!File.Exists(resultPath)) return;
        // 反序列化数据
        var saveDataDict = JsonConvert.DeserializeObject<Dictionary<string, GameSaveData>>(File.ReadAllText(resultPath));
        foreach (var saveable in saveables)
        {
            saveable.RestoreGameData(saveDataDict[saveable.GetType().Name]);
        }

    }
}
使用样例

比如场景切换类中,需要保存当前所在的场景,以便下次继续游戏时直接进入到此场景中
所以需要先注册当前类(因为版本限制,需要自己写注册逻辑,应该是直接调用接口方法就可以注册的)

// 保存数据注册
ISaveable saveable = this;
SaveLoadManager.Instance.Register(this);

对了,别忘了要继承啊

public class TransitionManager : Singleton<TransitionManager>, ISaveable{}

接着来实现接口的保存和加载方法

// 存儲數據
public GameSaveData GenerateSaveData()
{
   GameSaveData gameSaveData = new GameSaveData();
   gameSaveData.curScene = (SceneName)Enum.Parse(typeof(SceneName), SceneManager.GetActiveScene().name);
   return gameSaveData;
}

// 加載數據
public void RestoreGameData(GameSaveData saveData)
{
   startScene = saveData.curScene;
   Transition(SceneName.Menu.ToString(), startScene.ToString());
}

就ok了

动态根据枚举名称获取枚举值

//SceneManager.GetActiveScene().name 获取当前激活场景名称
SceneName name =  (SceneName)Enum.Parse(typeof(SceneName), SceneManager.GetActiveScene().name);

添加音乐音效

如果是添加背景音乐,可以直接创建个gameObject添加AudioSource组件,将背景音乐拖入,并进行设置即可
如果是添加音效,如点击音,可以创建一个音效类进行统一管理,同样也要挂载到使用了AudioSource的gameObject上,使用audioSource.PlayOneShot(clip)来实现音效的播放

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

public class AudioManager : Singleton<AudioManager>
{
    private AudioSource audioSource;

    public AudioClip collectClip; //收集物品音效
    public AudioClip clickClip; //点击音效

    protected override void Awake()
    {
        base.Awake();
        audioSource = GetComponent<AudioSource>();
    }
    // 播放音效
    public void AudioPlayByClip(AudioClip clip)
    {
        // 单次播放
        audioSource.PlayOneShot(clip);
    }

    // 播放音效
    public void AudioPlayByTp(AudioClipTp audioClipTp)
    {
        if (audioClipTp == AudioClipTp.Collect)
        {
            audioSource.PlayOneShot(collectClip);
        }
        else if (audioClipTp == AudioClipTp.Click)
        {
            audioSource.PlayOneShot(clickClip);
        }
    }
}

打包注意

  • 打包前记得要清一下内存数据,Edit-Clear All PlayerPrefs

打包配置

打包设置里Player-Other Settings-Scripting Backend可以选择IL2CPP这个模式可以减小游戏大小并且增加反编译的难度

Q&A

MoveTool点击不了

突然MoveTool使用不了了,是有HandTool可以,其他都点击不了

unity 图片fillAmount跟随倒计时_json_05


把这里选择为第一个就行了= =

(float)(70 / 100) = 0

代码里写的(float)(70 / 100)结果是0?!

(float)(70 / 100)两个int计算结果也为int ,向下取整,70 / 100结果为0 ,再转为float也是0
应该写为(float)70 / 100

shaderGraph透明通道无效

Unity2020.1.2 连接了Alpha通道还是有背景色

unity 图片fillAmount跟随倒计时_游戏引擎_06

PBR Master的设置里修改

unity 图片fillAmount跟随倒计时_学习_07


顺便一提 设置双面的就是在设置面板勾选Tow Sided

文字模糊

这是50号字,你敢信

unity 图片fillAmount跟随倒计时_json_08

网上的解决办法是先调整文本框大小在调整缩放,再扩大字号

unity 图片fillAmount跟随倒计时_游戏引擎_09


是清晰很多,但不适用,因为需要自适应对话框大小,文本框大小不能写死(难道要代码算么)

协程未执行

private IEnumerable DialogueRoutine(Stack<string> data){}
//调用
StartCoroutine("DialogueRoutine", dialogueEmptyStack);
StartCoroutine((IEnumerator)DialogueRoutine(dialogueFinishStack));

协程一直不执行,还报错类型转换问题

协程名称写错啦
应该是IEnumerator