基于ScriptableObject设计游戏数据表

devtools/2024/10/8 22:43:15/

前言

本篇文章是针对之前对于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
#endifpublic 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.ppmy.cn/devtools/123068.html

相关文章

pycharm中使用anaconda创建多环境,无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

问题描述 用的IDE是&#xff1a; 使用anaconda创建了一个Python 3.9的环境 结果使用pip命令的时候&#xff0c;报错 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称 解决方案 为了不再增加系统变量&#xff0c;我们直接将变量添加在当前项目中你的Ter…

主流前端框架实际案例说明

为了更深入地理解不同前端框架的特点和适用场景&#xff0c;以下将通过几个具体案例分析&#xff0c;探讨在实际项目中选择框架的决策过程。 案例一&#xff1a;电商平台开发 项目背景 一个新兴电商平台希望快速上线&#xff0c;提供良好的用户体验和性能&#xff0c;同时需…

命名管道Linux

管道是 毫不相关的进程进程间通信::命名管道 管道 首先自己要用用户层缓冲区&#xff0c;还得把用户层缓冲区拷贝到管道里&#xff0c;&#xff08;从键盘里输入数据到用户层缓冲区里面&#xff09;&#xff0c;然后用户层缓冲区通过系统调用&#xff08;write&#xff09;写…

Mybatis-plus做了什么

Mybatis-plus做了什么 Mybatis回顾以前的方案Mybatis-plus 合集总览&#xff1a;Mybatis框架梳理 聊一下mybatis-plus。你是否有过疑问&#xff0c;Mybatis-plus中BaseMapper方法对应的SQL在哪里&#xff1f;它为啥会被越来越多人接受。在Mybatis已经足够灵活的情况下&…

SQL第12课——联结表

三点&#xff1a;什么是联结&#xff1f;为什么使用联结&#xff1f;如何编写使用联结的select语句 12.1 联结 SQL最强大的功能之一就是能在数据查询的执行中联结&#xff08;join)表。联结是利用SQL的select能执行的最重要的操作。 在使用联结前&#xff0c;需要了解关系表…

用 LoRA 微调 Stable Diffusion:拆开炼丹炉,动手实现你的第一次 AI 绘画

总得拆开炼丹炉看看是什么样的。这篇文章将带你从代码层面一步步实现 AI 文本生成图像&#xff08;Text-to-Image&#xff09;中的 LoRA 微调过程&#xff0c;你将&#xff1a; 了解 Trigger Words&#xff08;触发词&#xff09;到底是什么&#xff0c;以及它们如何影响生成结…

力扣59.螺旋矩阵||

题目链接&#xff1a;59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff…

【无人机设计与技术】自抗扰控制(ADRC)的建模与仿真研究

摘要 本文针对四旋翼无人机姿态控制系统进行了基于自抗扰控制(ADRC)的建模与仿真研究。通过MATLAB/Simulink仿真平台&#xff0c;实现了无人机的姿态控制模型&#xff0c;并采用自抗扰控制器(ADRC)对无人机的姿态进行控制。本文详细介绍了自抗扰控制器的设计方法和应用&#x…