基于ScriptableObject设计游戏数据表

news/2024/10/2 7:10:41 标签: unity, 游戏, 数据存储

前言

本篇文章是针对之前对于ScriptableObject概念讲解的实际应用之一,在游戏开发中,我们可以使用该类来设计编辑器时的可读写数据表或者运行时的只读数据表。本文将针对运行时的只读数据表的应用进行探索,并且结合自定义的本地持久化存储方式使得基于ScriptableObject开发的数据表能够在运行时进行读写。

代码

代码目录结构
  • Table
    • Base
    • Editor
    • Interface
    • Unit

Table则为本模块的根目录,存储各个游戏数据表的脚本,Base目录存储数据表和游戏表基类,Editor目录存储数据表的编辑器脚本,Interface目录存储数据表和数据单元接口,Unit目录存储数据单元。

Base目录 

BaseTable.cs

using System;
using UnityEngine;

/// <summary>
/// 基础表
/// </summary>
public abstract class BaseTable : ScriptableObject
{
    /// <summary>
    /// 表类型
    /// </summary>
    public abstract Type mType { get; }
}

GameTable.cs

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using UnityEngine;

/// <summary>
/// 游戏表
/// </summary>
/// <typeparam name="T0">表类型</typeparam>
/// <typeparam name="T1">表单元类型</typeparam>
public class GameTable<T0, T1> : BaseTable, ITableHandler<T0, T1>
where T0 : GameTable<T0, T1>
where T1 : ITableUnit
{
    [Tooltip("是否自动控制加载和保存")] public bool isAutoControl = true;
    [HideInInspector, SerializeField] protected T1[] units;

#if UNITY_EDITOR
#pragma warning disable CS0414
    [HideInInspector, SerializeField] bool isAutoSave = true;
#pragma warning restore CS0414
#endif

    public sealed override Type mType => typeof(T0);

    public ReadOnlyCollection<T1> mUnits => Array.AsReadOnly(wrapper.value);

    public int mCount => wrapper.value == null ? 0 : wrapper.value.Length;

    public event Action<T1> mModifiedCallback;

    protected string jsonPath;

    protected TempWrapper<T1[]> wrapper;

    /// <summary>
    /// 保存到本地
    /// </summary>
    public virtual void SaveLocally()
    {
        if (Application.isEditor) return;

        wrapper.UnWrapByBinary(ref units);
        string jsonStr = JsonUtility.ToJson(this);

        if (!string.IsNullOrEmpty(jsonStr))
        {
            string dirPath = Path.GetDirectoryName(jsonPath);
            if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
            if (!File.Exists(jsonPath)) File.Create(jsonPath).Dispose();

            using (FileStream fs = new FileStream(jsonPath, FileMode.OpenOrCreate, FileAccess.Write))
            {
                byte[] bytes = Encoding.UTF8.GetBytes(jsonStr);
                fs.Write(bytes, 0, bytes.Length);
                fs.Flush();
                fs.Close();
            }
        }
    }

    /// <summary>
    /// 从本地加载
    /// </summary>
    public virtual void LoadFromLoacl()
    {
        if (Application.isEditor) return;

        if (File.Exists(jsonPath))
        {
            using (TextReader tr = new StreamReader(jsonPath, Encoding.UTF8))
            {
                string jsonStr = tr.ReadToEnd();

                if (!string.IsNullOrEmpty(jsonStr))
                {
                    try
                    {
                        JsonUtility.FromJsonOverwrite(jsonStr, this);
                        int len = units.Length;
                        wrapper.value = new T1[len];
                        units.CopyTo(wrapper.value, 0);
                        InvokeModifiedEvents();
                    }
                    catch (Exception e)
                    {
                        LogUtility.Log(e.Message, LogType.Error);
                    }
                }
                tr.Close();
            }
        }
        else
        {
            string dirPath = Path.GetDirectoryName(jsonPath);
            if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
            if (!File.Exists(jsonPath)) File.Create(jsonPath).Dispose();
        }
    }

    public virtual void ShareUnitsWith(T1[] array)
    {
        int len = wrapper.value.Length;

        if (array == null || array.Length != len)
            array = new T1[len];

        for (int i = 0; i < len; i++)
        {
            array[i] = wrapper.value[i];
        }
    }

    public virtual void SetDefault()
    {
        T0 table = Resources.Load<T0>(GamePathUtility.GetTableResourcesPath<T0>());

        if (table != null)
        {
            int len = table.units.Length;
            wrapper.value = new T1[len];
            table.units.CopyTo(wrapper.value, 0);
            InvokeModifiedEvents();
        }
    }

    public virtual T1 Get(Func<T1, bool> logic)
    {
        if (logic == null) return default;

        int len = wrapper.value.Length;
        for (int i = 0; i < len; i++)
        {
            ref T1 unit = ref wrapper.value[i];
            if (logic(unit)) return unit;
        }

        return default;
    }

    public virtual T1 Get(int index)
    {
        int len = wrapper.value.Length;
        if (index < 0 || index >= len) return default;
        return wrapper.value[index];
    }

    public virtual void Set(Func<T1, T1> logic)
    {
        if (logic == null) return;

        int len = wrapper.value.Length;
        for (int i = 0; i < len; i++)
        {
            wrapper.value[i] = logic(wrapper.value[i]);
        }
        InvokeModifiedEvents();
    }

    void InvokeModifiedEvents()
    {
        if (mModifiedCallback != null)
        {
            int len = wrapper.value.Length;
            for (int i = 0; i < len; i++)
            {
                mModifiedCallback.Invoke(wrapper.value[i]);
            }
        }
    }

    void Awake()
    {
        jsonPath = Path.Combine(Application.dataPath, $"Json/{mType.Name}.json");
    }

    void OnEnable()
    {
        if (units == null) units = Array.Empty<T1>();
        if (wrapper == null) wrapper = TempWrapper<T1[]>.WrapByBinary(ref units);
        if (isAutoControl) LoadFromLoacl();
    }

    void OnDisable()
    {
        if (isAutoControl) SaveLocally();

        if (wrapper != null)
        {
            wrapper.Dispose();
            wrapper = null;
        }
    }
}
Interface目录 

ITableHandler.cs

using System;
using System.Collections.ObjectModel;

// 表处理接口
public interface ITableHandler<TTable, TUnit> where TTable : BaseTable where TUnit : ITableUnit
{
    /// <summary>
    /// 表单元合集的只读视图
    /// </summary>
    ReadOnlyCollection<TUnit> mUnits { get; }

    /// <summary>
    /// 表单元合集中元素个数
    /// </summary>
    int mCount { get; }

    /// <summary>
    /// 表单元合集更改回调
    /// </summary>
    event Action<TUnit> mModifiedCallback;

    /// <summary>
    /// 分享表单元合集给指定的数组变量
    /// </summary>
    /// <param name="array">指定的数组变量</param>
    void ShareUnitsWith(TUnit[] array);

    /// <summary>
    /// 设置为默认值
    /// </summary>
    void SetDefault();

    /// <summary>
    /// 获取表单元
    /// </summary>
    /// <param name="logic">获取逻辑</param>
    TUnit Get(Func<TUnit, bool> logic);

    /// <summary>
    /// 获取表单元
    /// </summary>
    /// <param name="index">索引</param>
    TUnit Get(int index);

    /// <summary>
    /// 修改表单元
    /// </summary>
    /// <param name="logic">修改逻辑</param>
    void Set(Func<TUnit, TUnit> logic);
}

ITableUnit.cs

// 表单元接口
public interface ITableUnit { }
Editor目录 

GameTableEditor.cs

using UnityEditor;
using UnityEngine;

// 游戏表编辑器
public class GameTableEditor : Editor
{
    protected SerializedProperty units, isAutoSave;
    const string tip = "Should be saved after modification. Everything will be saved when we leave the inspector unless you don't check 'Is Auto Save'. In runtime, everything will be loaded from local in 'OnEnable' and saved to local in 'OnDisable' unless you don't check 'Is Auto Control'.";

    protected void Init()
    {
        units = serializedObject.FindProperty("units");
        isAutoSave = serializedObject.FindProperty("isAutoSave");
    }

    protected void SaveGUI()
    {
        if (GUILayout.Button("Save")) Save();
        isAutoSave.boolValue = EditorGUILayout.Toggle(isAutoSave.displayName, isAutoSave.boolValue);
    }

    protected void TipGUI()
    {
        EditorGUILayout.HelpBox(tip, MessageType.Info);
    }

    protected virtual void Save() { }

    void OnDisable() { if (isAutoSave.boolValue) Save(); }
}
示例(鼠标样式表)

CursorStyleUIUnit.cs

using System;
using UnityEngine;
using UnityEngine.UI;

// 鼠标样式UI单元
[Serializable]
public class CursorStyleUIUnit
{
    [Tooltip("鼠标样式类型")] public CursorStyleType styleType;
    [Tooltip("Dropdown组件")] public Dropdown dropdown;
    [Tooltip("当前选项的Image组件")] public Image showImage;
    [Tooltip("Dropdown组件选项模板下自定义的Image组件")] public Image itemShowImage;
}

CursorStyleUnit.cs

using System;
using UnityEngine;

// 鼠标样式单元
[Serializable]
public struct CursorStyleUnit : ITableUnit
{
    [Tooltip("鼠标样式的属性名称")] public string key;
    [Tooltip("鼠标样式的属性值")] public string value;

    public CursorStyleUnit(string key, string value)
    {
        this.key = key;
        this.value = value;
    }
}

CursorStyleTable.cs

using UnityEngine;

// 鼠标样式单元存储表
[CreateAssetMenu(fileName = "Assets/Resources/Tables/CursorStyleTable", menuName = "Custom/Create CursorStyle Table", order = 1)]
public sealed class CursorStyleTable : GameTable<CursorStyleTable, CursorStyleUnit>
{
    [HideInInspector, SerializeField] CursorShape defaultShape;
    [HideInInspector, SerializeField] CursorColor defaultColor;
    [HideInInspector, SerializeField] int defaultSize;

    /// <summary>
    /// 默认鼠标形状
    /// </summary>
    public CursorShape mDefaultShape => defaultShape;

    /// <summary>
    /// 默认鼠标颜色
    /// </summary>
    public CursorColor mDefaultColor => defaultColor;

    /// <summary>
    /// 默认鼠标尺寸
    /// </summary>
    public int mDefaultSize => defaultSize;
}

CursorStyleTableEditor.cs

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

[CustomEditor(typeof(CursorStyleTable))]
public sealed class CursorStyleTableEditor : GameTableEditor
{
    SerializedProperty defaultShape, defaultColor, defaultSize;
    ReorderableList list;
    string[] styleTypes; // 样式类型合集
    Dictionary<int, Style> styles; // key表示该项在整个集合中的索引,value表示样式
    Style defaultShapeStyle, defaultColorStyle, defaultSizeStyle; // 样式默认值
    GUIContent defaultShapeContent, defaultColorContent, defaultSizeContent;
    string[] shapeDisplayNames, colorDisplayNames, sizeDisplayNames; // 样式默认值下拉菜单选项
    int _shapeIndex, _colorIndex, _sizeIndex; // 样式默认值所选菜单项索引
    bool isStylesDirty;

    int shapeIndex
    {
        get => _shapeIndex;
        set
        {
            if (_shapeIndex != value)
            {
                _shapeIndex = value;
                UpdateDefaultStyles(Array.FindIndex(styleTypes, t => t == CursorStyleConstant.SHAPE));
            }
        }
    }

    int colorIndex
    {
        get => _colorIndex;
        set
        {
            if (_colorIndex != value)
            {
                _colorIndex = value;
                UpdateDefaultStyles(Array.FindIndex(styleTypes, t => t == CursorStyleConstant.COLOR));
            }
        }
    }

    int sizeIndex
    {
        get => _sizeIndex;
        set
        {
            if (_sizeIndex != value)
            {
                _sizeIndex = value;
                UpdateDefaultStyles(Array.FindIndex(styleTypes, t => t == CursorStyleConstant.SIZE));
            }
        }
    }

    // 记录每种样式类型和值
    struct Style
    {
        public int styleTypeIndex; // 样式类型索引
        public string value; // 样式值

        public Style(int styleTypeIndex, string value)
        {
            this.styleTypeIndex = styleTypeIndex;
            this.value = value;
        }

        public bool CompareTo(ref Style other)
        {
            return styleTypeIndex == other.styleTypeIndex && value == other.value;
        }
    }

    void OnEnable()
    {
        Init();
        defaultShape = serializedObject.FindProperty("defaultShape");
        defaultColor = serializedObject.FindProperty("defaultColor");
        defaultSize = serializedObject.FindProperty("defaultSize");

        list = new ReorderableList(serializedObject, units, false, false, true, true)
        {
            drawElementCallback = DrawUnitCallback,
            onAddCallback = OnAddElement,
            onRemoveCallback = OnDelElement
        };

        styleTypes = new string[] { CursorStyleConstant.SHAPE, CursorStyleConstant.COLOR, CursorStyleConstant.SIZE };
        styles = new Dictionary<int, Style>();

        defaultShapeStyle.styleTypeIndex = Array.FindIndex(styleTypes, t => t == CursorStyleConstant.SHAPE);
        defaultShapeStyle.value = ((CursorShape)defaultShape.intValue).ToString();
        defaultColorStyle.styleTypeIndex = Array.FindIndex(styleTypes, t => t == CursorStyleConstant.COLOR);
        defaultColorStyle.value = ((CursorColor)defaultColor.intValue).ToString();
        defaultSizeStyle.styleTypeIndex = Array.FindIndex(styleTypes, t => t == CursorStyleConstant.SIZE);
        defaultSizeStyle.value = defaultSize.intValue.ToString();

        int len = units.arraySize;
        SerializedProperty element;
        for (int i = 0; i < len; i++)
        {
            element = units.GetArrayElementAtIndex(i);
            int styleTypeIndex = Array.IndexOf(styleTypes, element.FindPropertyRelative("key").stringValue);
            AddOrSetElement(i, new Style(styleTypeIndex, element.FindPropertyRelative("value").stringValue));
        }

        defaultShapeContent = new GUIContent(defaultShape.displayName, defaultShape.tooltip);
        defaultColorContent = new GUIContent(defaultColor.displayName, defaultColor.tooltip);
        defaultSizeContent = new GUIContent(defaultSize.displayName, defaultSize.tooltip);

        len = styleTypes.Length;
        for (int i = 0; i < len; i++)
        {
            UpdateDefaultDisplayNames(i);
        }

        string str = defaultShapeStyle.value;
        _shapeIndex = Array.FindIndex(shapeDisplayNames, s => s == str);
        str = defaultColorStyle.value;
        _colorIndex = Array.FindIndex(colorDisplayNames, s => s == str);
        str = defaultSizeStyle.value;
        _sizeIndex = Array.FindIndex(sizeDisplayNames, s => s == str);
    }

    void DrawUnitCallback(Rect rect, int index, bool isActive, bool isFocused)
    {
        if (index >= styles.Count) styles[index] = new Style();

        Style style = styles[index];
        rect.y += 2;
        style.styleTypeIndex = EditorGUI.Popup(new Rect(rect.x, rect.y, 80, EditorGUIUtility.singleLineHeight), style.styleTypeIndex, styleTypes);
        style.value = EditorGUI.TextField(new Rect(rect.x + 100, rect.y, rect.width - 100, EditorGUIUtility.singleLineHeight), style.value);
        UpdateStyle(ref style, index);
    }

    void OnAddElement(ReorderableList list)
    {
        ReorderableList.defaultBehaviours.DoAddButton(list);
        AddOrSetElement(list.count - 1, new Style(0, string.Empty));
    }

    void OnDelElement(ReorderableList list)
    {
        DelElement(list.index);
        ReorderableList.defaultBehaviours.DoRemoveButton(list);
    }

    void AddOrSetElement(int index, Style style)
    {
        if (style.styleTypeIndex < 0 || style.styleTypeIndex >= styleTypes.Length
        || string.IsNullOrEmpty(style.value) || index < 0 || index >= list.count) return;

        styles[index] = style;
        UpdateDefaultDisplayNames(style.styleTypeIndex);
    }

    void DelElement(int index)
    {
        Style style = styles[index];
        styles.Remove(index);
        UpdateDefaultDisplayNames(style.styleTypeIndex);
    }

    void UpdateDefaultDisplayNames(params int[] styleTypeIndexes)
    {
        if (styleTypeIndexes == null || styleTypeIndexes.Length == 0) return;

        int len = styleTypeIndexes.Length;
        var group = styles.GroupBy(kv => kv.Value.styleTypeIndex);
        string CONST_STR;
        IGrouping<int, KeyValuePair<int, Style>> temp;

        for (int i = 0; i < len; i++)
        {
            int index = styleTypeIndexes[i];
            if (index < 0 || index >= styleTypes.Length) continue;
            CONST_STR = styleTypes[index];

            switch (CONST_STR)
            {
                case CursorStyleConstant.SHAPE:
                    temp = group.Where(g => g.Key == index).FirstOrDefault();
                    if (temp != null) shapeDisplayNames = temp.Select(kv => kv.Value.value).ToArray();
                    else shapeDisplayNames = Array.Empty<string>();
                    break;
                case CursorStyleConstant.COLOR:
                    temp = group.Where(g => g.Key == index).FirstOrDefault();
                    if (temp != null) colorDisplayNames = temp.Select(kv => kv.Value.value).ToArray();
                    else colorDisplayNames = Array.Empty<string>();
                    break;
                case CursorStyleConstant.SIZE:
                    temp = group.Where(g => g.Key == index).FirstOrDefault();
                    if (temp != null) sizeDisplayNames = temp.Select(kv => kv.Value.value).ToArray();
                    else sizeDisplayNames = Array.Empty<string>();
                    break;
            }
        }
    }

    void UpdateDefaultStyles(params int[] styleTypeIndexes)
    {
        if (styleTypeIndexes == null || styleTypeIndexes.Length == 0) return;

        int len = styleTypeIndexes.Length;
        string CONST_STR;

        for (int i = 0; i < len; i++)
        {
            int index = styleTypeIndexes[i];
            if (index < 0 || index >= styleTypes.Length) continue;
            CONST_STR = styleTypes[index];

            switch (CONST_STR)
            {
                case CursorStyleConstant.SHAPE:
                    if (_shapeIndex < 0 || _shapeIndex >= shapeDisplayNames.Length)
                        defaultShapeStyle.value = CursorShape.None.ToString();
                    else defaultShapeStyle.value = shapeDisplayNames[_shapeIndex];
                    break;
                case CursorStyleConstant.COLOR:
                    if (_colorIndex < 0 || _colorIndex >= colorDisplayNames.Length)
                        defaultColorStyle.value = CursorColor.None.ToString();
                    else defaultColorStyle.value = colorDisplayNames[_colorIndex];
                    break;
                case CursorStyleConstant.SIZE:
                    if (_sizeIndex < 0 || _sizeIndex >= sizeDisplayNames.Length)
                        defaultSizeStyle.value = "0";
                    else defaultSizeStyle.value = sizeDisplayNames[_sizeIndex];
                    break;
            }
        }
    }

    void UpdateStyle(ref Style style, int index)
    {
        if (!styles[index].CompareTo(ref style))
        {
            styles[index] = style;
            isStylesDirty = true;
        }
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        base.OnInspectorGUI();

        EditorGUILayout.LabelField("鼠标样式单元合集", EditorStyles.boldLabel);
        list.DoLayoutList();

        EditorGUILayout.LabelField("鼠标样式默认值", EditorStyles.boldLabel);

        if (isStylesDirty)
        {
            isStylesDirty = false;
            for (int i = 0; i < styleTypes.Length; i++)
            {
                UpdateDefaultDisplayNames(i);
                UpdateDefaultStyles(i);
            }
        }

        EditorGUI.BeginDisabledGroup(shapeDisplayNames.Length == 0);
        shapeIndex = EditorGUILayout.Popup(defaultShapeContent, shapeIndex, shapeDisplayNames);
        EditorGUI.EndDisabledGroup();

        EditorGUI.BeginDisabledGroup(colorDisplayNames.Length == 0);
        colorIndex = EditorGUILayout.Popup(defaultColorContent, colorIndex, colorDisplayNames);
        EditorGUI.EndDisabledGroup();

        EditorGUI.BeginDisabledGroup(sizeDisplayNames.Length == 0);
        sizeIndex = EditorGUILayout.Popup(defaultSizeContent, sizeIndex, sizeDisplayNames);
        EditorGUI.EndDisabledGroup();

        SaveGUI();
        TipGUI();
        serializedObject.ApplyModifiedProperties();
    }

    protected override void Save()
    {
        List<CursorStyleUnit> reserve = new List<CursorStyleUnit>();
        int len = styles.Count;

        for (int i = 0; i < len; i++)
        {
            Style style = styles[i];
            if (!string.IsNullOrEmpty(style.value))
            {
                CursorStyleUnit v_unit = new CursorStyleUnit(styleTypes[style.styleTypeIndex], style.value);
                if (!reserve.Contains(v_unit)) reserve.Add(v_unit);
            }
        }

        units.ClearArray();
        styles.Clear();
        len = reserve.Count;
        CursorStyleUnit unit;
        SerializedProperty element;

        for (int i = 0; i < len; i++)
        {
            units.InsertArrayElementAtIndex(i);
            element = units.GetArrayElementAtIndex(i);
            unit = reserve[i];
            element.FindPropertyRelative("key").stringValue = unit.key;
            element.FindPropertyRelative("value").stringValue = unit.value;
            styles[i] = new Style(Array.FindIndex(styleTypes, t => t == unit.key), unit.value);
        }

        for (int i = 0; i < styleTypes.Length; i++)
        {
            UpdateDefaultDisplayNames(i);
            UpdateDefaultStyles(i);
        }

        if (Enum.TryParse(defaultShapeStyle.value, out CursorShape shape))
            defaultShape.intValue = (int)shape;
        if (Enum.TryParse(defaultColorStyle.value, out CursorColor color))
            defaultColor.intValue = (int)color;
        defaultSize.intValue = Convert.ToInt32(defaultSizeStyle.value);

        serializedObject.ApplyModifiedProperties();
    }
}

界面展示

分析

BaseTable作为所有表格的抽象基类并继承自ScriptableObject,用于后续扩展。ITableHandler声明表格的公开属性和行为。ITableUnit声明数据单元的公开属性和行为,作为暂留接口用于后续扩展,所有数据单元需要实现该接口。GameTable继承自BaseTable,并实现了ITableHandler接口,作为游戏数据表的基类,实现通用属性和方法,向具体游戏表类开放重写方法。GameTableEditor作为游戏数据表编辑器脚本的基类,实现通用逻辑。


示例中CursorStyleUIUnit作为鼠标样式的UI单元,负责定义UI界面上与表格数据相对应的UI组件。CursorStyleUnit作为鼠标样式的数据单元,负责定义每一项表格数据。CursorStyleTable则是定义鼠标样式表的具体逻辑。CursorStyleTableEditor用于定义鼠标样式表在编辑器Inspector面板中的GUI界面。


GameTable中isAutoControl字段用于启用运行时自动进行本地持久化管理的服务,在OnEnable方法中从本地持久化文件中加载内容,在OnDisable方法中将缓存内容保存至本地持久化文件中。isAutoSave字段用于启用编辑器时表格自动保存修改到资产文件的服务,若不勾选,每次在Inspector面板中进行修改后需要手动点击Save按钮进行保存,勾选后会自动保存。提供了指示表类型、表单元只读视图、表单元个数和修改回调等属性,以及本地持久化管理、表单元共享、表单元获取和设置以及重置为默认值等方法。


对表格进行设计后,我们可以使用表格管理器来统一管理所有表格,基于ScriptableObject的特性,我们可以为每个表格创建资产文件,通过加载资产文件即可获取表格实例。


TempWrapper称为字段临时缓存包装器,具体请看系列文章中与此相关的内容。

版本改进

......

系列文章

字段临时缓存包装器

如果这篇文章对你有帮助,请给作者点个赞吧!


http://www.niftyadmin.cn/n/5688133.html

相关文章

CSS | 面试题:你知道几种移动端适配方案?

目录 一、自适应和响应式 二、为什么要做移动端适配&#xff1f; 三、当前流行的几种适配方案 (1) 方案一&#xff1a;百分比设置&#xff08;不推荐&#xff09; (2) 方案二&#xff1a;rem 动态设置 font-size px 与 rem 的单位换算 手动换算 less/scss函数 webpac…

Android Studio :The emulator process for AVD was killed。

问题描述&#xff1a; 创建虚拟器&#xff0c;点击运行后提示&#xff1a;<font style"color:rgb(34, 34, 38);">The emulator process for AVD was killed</font> 分析原因&#xff1a; 虚拟机安装路径中包含中文字符导致乱码 > Android Studio …

Unity Asset Store的默认下载位置及更改下载路径的方法

修改Unity Asset Store的默认下载路径 Unity Asset Store默认下载位置 Unity Asset Store里下载资源&#xff0c;默认是下载到C盘里的&#xff0c;如果你不想做C盘战士的话&#xff0c;记得将下载的资源转移到其他盘。 Unity商城默认下载路径是C:\用户\用户名&#xff08;一般…

付费计量系统通用功能(5)

11.6 Class 6: Distribution function配电 Capability of distributing electrical energy from the transmission grid to the customer’s installation. 分配电能从输电线路到用户装置 Receives the transmitted electrical energy from the transmission g…

药品识别与分类系统源码分享

药品识别与分类检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer V…

Vue3 组件中使用 SCSS 变量

在 JavaScript 中不能直接使用 SCSS 变量。但是可以通过一些间接的方法来实现类似的效果。 一、使用 sass-extract 使用 sass-extract 库来提取 SCSS 变量并生成 JSON 文件&#xff0c;然后在 JavaScript 中读取这个 JSON 文件来获取变量值。 1. 安装 sass-extract npm ins…

植物叶片病害检测数据集 5100张 29类 带标注 voc yolo

植物叶片病害检测数据集 5100张 29类 带标注 voc yolo 植物叶片病害检测数据集 名称 植物叶片病害检测数据集 (Plant Leaf Disease Detection Dataset) 规模 图像数量&#xff1a;5154张图像。类别&#xff1a;29种病害类型。分类名: (图片张数&#xff0c;标注个数) Tomato…

在WPF中实现多语言切换的四种方式

在WPF中有多种方式可以实现多语言&#xff0c;这里提供几种常用的方式。 一、使用XML实现多语言切换 使用XML实现多语言的思路就是使用XML作为绑定的数据源。主要用到XmlDataProvider类. 使用XmlDataProvider.Source属性指定XML文件的路径或通过XmlDataProvider.Document指定…