基于ScriptableObject设计游戏数据表

news2024/12/22 10:03:20

前言

本篇文章是针对之前对于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.coloradmin.cn/o/2183633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一级建造师备考攻略及一建各科老师推荐(各科四大金刚)

吐血整理:真正的实战派一建名师推荐! 考过的同学一定都知道推荐的老师YYDS! 一级建造师各科老师推荐: 《法规》名师:王欣、王竹梅、陈印、关涛 其他老师:房超、蔡恒、刘丹、武海峰、陈洁、安国庆、桂林 《管理》名师:宿吉南、龙炎飞、李向国、朱俊文 其他老师:缴广才、陈晨…

跟《经济学人》学英文:2024年09月28日这期 The curse of the Michelin star

The curse of the Michelin star Restaurants awarded the honour are more likely to close, research finds 原文&#xff1a; The twelve new restaurants added to the New York Michelin Guide this month, serving up cuisine ranging from “haute French” to “eco…

9.数据结构与算法-单链表,循环链表和双向链表的比较////顺序表和链表的比较

单链表&#xff0c;循环链表和双向链表的时间效率比较 顺序表和链表的区别 存储密度

HarmonyOS Next应用开发——自定义组件的使用

自定义组件的使用 在ArkUI中&#xff0c;UI显示的内容均为组件&#xff0c;由框架直接提供的称为系统组件&#xff0c;由开发者定义的称为自定义组件。在进行 UI 界面开发时&#xff0c;通常不是简单的将系统组件进行组合使用&#xff0c;而是需要考虑代码可复用性、业务逻辑与…

达梦数据库开启归档模式

目录 一、什么是归档模式&#xff1f; 二、开启归档模式的步骤 1、创建归档目录 2、进入dm数据库bin目录 3、登录数据库 4、关闭数据库 5、启动数据库到Mount状态 6、增加本地归档日志文件 7、开启归档 8、启动数据库 9、验证是否开启成功 三、开启归档模式的优…

Lj视频下载器 1.1.37 简洁高效的视频下载工具

Lj视频下载器是一个功能强大的视频下载器&#xff0c;支持直接添加视频地址或 m3u8 资源地址&#xff0c;可以从网页中自动提取视频进行下载。支持多种视频格式&#xff0c;包括 m3u8&#xff0c;并能自动检测并移除广告片段。 大小&#xff1a;19M 百度网盘&#xff1a;https…

Linux CentsOS定时删除一个目录下(包含子目录)的改动时间大于12小时的文件

Shell脚本 文件目录如下图 ** 查找/ai/img/目录下的所有文件** find /ai/img/ -type f查找/ai/img/目录下的所有上次改动时间大于720分钟(12小时)的文件 12 小时&#xff0c;也就是 720 分钟。所以&#xff0c;我们可以使用 -mmin 720 来查找修改时间超过 720 分钟&#xff08;…

uniapp 微信小程序 微信支付

本章的内容我尽量描述的细致一些&#xff0c;哪里看不懂给我评论就可以&#xff0c;我看到进行回复 微信支付大致分为4步&#xff0c;具体看后端设计 1. 获取code 2. 根据code获取openid 3. 根据openid&#xff0c;以及部分订单相关数据&#xff0c;生成prepayId (预支付交易会…

免费 Oracle 各版本 离线帮助使用和介绍

文章目录 Oracle 各版本 离线帮助使用和介绍概要在线帮助下载离线文档包&#xff1a;解压离线文档&#xff1a;访问离线文档&#xff1a;导航使用&#xff1a;目录介绍Install and Upgrade&#xff08;安装和升级&#xff09;&#xff1a;Administration&#xff08;管理&#…

交通场景多目标检测系统源码分享

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

Qt界面优化——绘图API

文章目录 绘图核心API绘制各种形状绘制线段绘制矩形绘制圆形绘制文本设置画笔设置画刷 绘制图片 绘图核心API Qt的各种控件&#xff0c;本质上都是画出来的&#xff0c;这不过这些都是提前画好了&#xff0c;我们拿过来直接使用即可。 实际开发中&#xff0c;可能现有控件无法…

面了智谱大模型算法岗,效率贼高!

最近这一两周不少互联网公司都已经开始秋招提前批面试了。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友…

使用容器启动的zk无法暴露3888问题解决

1. 问题描述 zk配置如下&#xff1a; 我通过容器启动了一个zk&#xff0c;通过-p 参数暴露了2181和3888端口&#xff0c;容器启动脚本如下&#xff1a; #!/bin/shdocker rm -f myzookeeper1docker run -p 12181:2181 -p 13888:3888 --name myzookeeper1 --restart always …

C# 字符串(String)的应用说明一

一.字符串&#xff08;String&#xff09;的应用说明&#xff1a; 在 C# 中&#xff0c;更常见的做法是使用 string 关键字来声明一个字符串变量&#xff0c;也可以使用字符数组来表示字符串。string 关键字是 System.String 类的别名。 二.创建 String 对象的方法说明&#x…

c++ 使用 Graham 扫描的凸包(Convex Hull using Graham Scan)

先决条件&#xff1a; 如何检查两个给定的线段是否相交&#xff1f; c https://blog.csdn.net/hefeng_aspnet/article/details/141713655 java https://blog.csdn.net/hefeng_aspnet/article/details/141713762 python https://blog.csdn.net/hefeng_aspnet/article/details/…

Australis 相機率定軟體說明

概要 課堂中使用Australis這套軟體&#xff0c;順帶記錄操作過程 內容以老師口述及我測試的經過 照片為老師課堂提供之 說明 執行 Step1. 匯入照片 注意&#xff01;&#xff01;如果是Mac的作業系統&#xff0c;將資料夾移到Windows上的時候&#xff0c;建議創一個新的資料…

mysql---索引类型及索引方法使用

mysql索引类型 Normal、Full Text、Unique 在 MySQL 中&#xff0c;索引的类型主要有以下几种&#xff1a; Normal Index&#xff08;普通索引&#xff09;&#xff1a; 这是最基本的索引类型&#xff0c;没有唯一性要求。允许重复值&#xff0c;可以加速查询性能。用法&#…

产品经理的学习

初学 接需求 画原型 写文档 日常产出 流程图 举例购物的流程 结构图 一个应用的全部功能&#xff0c;用思维导图的方式去罗列出来 竞品分析文档 竞品分类 竞品选择 竞品采集 竞品文档书写 也可以做一个产品的产品结构图 需求文档 干系人 需求方 记录人 产品经理 其他项目干系人…

搭建企业级私有仓库harbor

华子目录 harbor简介实验环境准备下载软件包安装docker-cehosts解析 实验步骤配置https加密传输解压进入解压目录&#xff0c;修改文件配置启动harbor 测试客户端配置harbor本地加速器注意 通过docker compose管理harbor harbor简介 harbor是由wmware公司开源的企业级docker r…

LLM基础概念:Token

什么是token&#xff1f;为什么要限制token的输入&#xff1f;平时说的消耗token数指的是什么&#xff1f; token是用于自然语言处理的词的片段。 在自然语言处理模型中&#xff0c;限制token数量主要是出于计算效率和资源限制的考虑。每一个token都对应一个向量&#xff0c;…