数据偏好
有没有想过应该用数据结构Y还是Z, 来处理问题X ?本文涵盖了与这些困境有关的各种主题.
注解
[something]-time 操作. 这个术语来自于算法分析的 大O 表示法.
长话短说, 它描述了运行时长度的最坏的情况. 用外行的话来说:
“随着问题域的大小增加, 算法的运行时间长度…”
O(1): “…不会增加.”O(log n): “…以缓慢的速度增长.”O(n): “…以同样的速度增长.”- 等等.
想象一下, 如果必须在一帧内处理300万个数据点. 不可能使用线性时间算法来设计这个特性, 因为数据的绝对大小, 将使运行时间大大超出分配的时间. 相比之下, 使用常量时间算法可以毫无问题地处理该操作.
总的来说, 开发人员希望尽可能避免进行线性时间操作. 但是, 如果保持线性时间运算的规模很小, 并且如果不需要经常执行操作, 则这是能够接受的. 平衡这些需求, 并为工作选择正确的算法/数据结构, 是使程序员的技能有价值的一部分.
数组、字典、对象
Variant 类中。变体(Variant)本身也可以存储兼容变体的数据结构,例如 Array (数组)、 Dictionary (字典)、 Object (对象)。Vector<Variant>实现数组。引擎会将数组内容存储在一段连续的内存之中,也就是说,元素与元素之间是相邻的。 注解 PoolStringArray 其实就类似于Vector<String>。 因为是在内存中连续存储,所以执行各种操作的性能如下: - 迭代 :最快,非常适合循环。
-
- 插入、删除、移动 :与位置相关,一般较慢。
- 操作:元素的添加、删除、移动需要移动与之相邻的元素(腾出地方或者填充空缺)。
- 末尾 添加、删除很快。
- 任意位置 添加、删除较慢。
- 开头 添加、删除最慢。
- 开头 执行多次插入、删除操作,那么……
- 反转数组。
- 末尾 执行数组更改。
- 再把数组反转回来。
这样就只复制了两次数组(虽然比较慢,但还是常数时间),否则就得把平均大概一半的数组复制 N 遍(线性时间)。
- 取值、设值 :因为是 按位置 存取的,所以最快。例如你可以请求第 0 个、第 2 个、第 10 个等等的元素,但不能按照元素的值来请求。
-
- 查找 :最慢。根据值获取索引,也就是位置。
- 操作:必须遍历数组,一个个元素做比较,直到找到匹配的为止。 -
- 如果保持有序, 自定义搜索操作, 可以使其达到对数时间(相对较快). 不过, 外行用户对此会感到不舒服. 通过在每次编辑之后, 重新排序数组, 并编写一个感知顺序的搜索算法来完成.
OrderedHashMap<Variant, Variant>. 引擎存储一个键值对的小数组(初始化为2^3或8条记录). 当试图访问一个值时, 它提供一个键. 然后, 对键进行 哈希 处理, 即转换成一个数字.”哈希” 值用来计算进入数组的索引. 作为一个数组,OHM就可以在键映射到值的 “表” 中快速查找. 当HashMap变得过满时, 它会增加到2的下一个幂值(以 16条记录, 然后是32条, 等等), 并重新构建结构. 散列是为了减少键碰撞的机会. 如果发生了, 列表必须为考虑前一个位置的值, 重新计算另一个索引. 总之, 这导致以牺牲内存和一些较小的操作效率为代价, 对所有记录的常量时间访问.
- 散列每个键任意次数.
-
- 保持不断增长的表规模.
- 如大家所知,Dictionaries [字典]擅长的任务是Arrays[数组]所不擅长的. 其操作细节概述如下:
- 迭代 : 快速.
-
- 插入, 删除, 移动 : 最快.
- 操作: 散列给定的键. 执行1个加法操作来查找适当的值(数组开始+偏移量). 移动其中的两个(一个插入, 一个擦除). 映射必须进行一些维护, 以保留其功能:
- 更新记录的有序列表.
- 确定列表密度, 是否需要扩展列表容量.
- 字典会记住用户插入键的顺序. 这使它能够执行可靠的迭代.
- 取值, 设值 : 最快. 和 根据键 查找相同.
-
- 查找 : 最慢. 标识值的键.
- 操作: 必须遍历记录并比较该值, 直到找到匹配的为止.
- 请注意,Godot并未开箱即用地提供此功能(因为它们并非用于此任务).
position的属性?”的问题, 它可能会询问其 script 或 ClassDB. 您可以在 在 Godot 中应用面向对象原则 文章中, 找到有关什么是对象以及它们如何工作的更多信息. 几个 迭代循环和哈希表查找. 此外, 查询是线性时间操作, 依赖于对象的继承层次结构大小. 如果Object查询的类(当前类)什么都没有找到, 则该请求将一直推迟到下一个基类, 一直到原始Object类为止. 虽然这些都是单独的快速操作, 但它必须进行如此多的检查, 于是这一事实使得它们比查找数据的两种方法都要慢. 注解 当开发人员提到脚本API有多慢时, 所引用的正是这一系列查询. 与编译后的, 应用程序知道在哪里可以找到任何东西的,C++代码相比, 不可避免的是, 脚本API操作将花费更长的时间. 他们必须定位任何相关数据的来源, 然后才能尝试访问它. GDScript很慢的原因是, 它执行的每个操作都要经过这个系统. C#可以通过更优化的字节码, 以更快的速度处理一些内容. 但是, 如果C#脚本调用引擎类的内容, 或者脚本试图访问它的外部内容, 它会通过这个管道. NativeScript C++甚至更进一步, 默认将所有内容都保持在内部. 对外部结构的调用将通过脚本API进行. 在NativeScript C++中, 注册方法以将其公开给脚本API是一项手动任务. 至此, 外部非C++类将使用API来查找它们.Array或Dictionary, 为什么选择一个Object而不是其他两个选项?
- 控件 : 对象能够创建更复杂的结构. 可以在数据上分层抽象, 以确保外部API不会响应内部数据结构的更改. 更重要的是, 对象可以有信号, 允许响应式行为. 对象带来了创建更复杂结构的能力.
- 清晰 : 当涉及到脚本和引擎类为对象定义的数据时, 对象是一个可靠的数据源. 属性可能不包含期望的值, 但是无需担心这个属性是否首先存在.
- 便利 : 如果已经有了类似的数据结构, 之后从现有类扩展, 可以使构建数据结构的任务变得容易得多. 相比之下, 数组和字典不能满足所有的用例.
对象还让用户有机会创建更专门化的数据结构。有了它,一个人可以设计自己的列表、二叉搜索树、堆、散列树、图、不相交集,以及其他选择。
“为什么不在树结构中使用节点?” 有人可能会问. 节点类包含与自定义数据结构无关的内容. 因此在构建树结构时, 构造自己的节点类型是很有帮助的.
GDScript
C#
extends Objectclass_name TreeNodevar _parent : TreeNode = nullvar _children : = [] setgetfunc _notification(p_what): match p_what: NOTIFICATION_PREDELETE: # Destructor. for a_child in _children: a_child.free()
从这里开始, 然后就可以创建具有特定功能的结构, 只会受到他们想象力的限制.// Can decide whether to expose getters/setters for properties laterpublic class TreeNode : Object{ private TreeNode _parent = null; private object[] _children = new object[0]; public override void Notification(int what) { switch (what) { case NotificationPredelete: foreach (object child in _children) { TreeNode node = child as TreeNode; if (node != null) node.Free(); } break; default: break; } }}
枚举: 整数VS字符串
export关键字时才可使用后者). 那么问题就来了,”应该使用哪一种?” 简而言之,”你觉得哪个更舒服就选哪个.” 这是GDScript特有的特性, 而并非一般的Godot脚本;该语言将可用性置于性能之上. 在技术层面上, 整数比较(常量时间)比字符串比较(线性时间)更快. 如果你想保持其他语言的习惯, 那么应该使用整数. 打印 枚举值时, 使用整数的主要问题就出现了. 作为整数尝试打印MY_ENUM, 将会打印5之类的东西, 而不是像MyEnum这样的词. 要打印整数枚举, 必须编写一个字典来映射每个枚举对应的字符串值. 如果使用枚举的主要目的是打印值, 并且希望将它们作为相关概念组合在一起, 那么使用它们作为字符串是有意义的. 这样, 就不需要在打印上执行单独的数据结构.AnimatedTexture vs. AnimatedSprite vs. AnimationPlayer vs. AnimationTree
在什么情况下应该使用Godot的各种动画类?对于Godot的新用户来说, 可能不是马上清楚答案. AnimatedTexture 是引擎绘制一个动画循环, 而不是一个静态图像的纹理. 用户可以进行如下操作… - 它在纹理的每个部分移动的速率(每秒帧数).
- 纹理中包含的区域数(帧).
VisualServer 按规定的速度依次绘制区块. 好消息是, 这并不涉及引擎部分额外的逻辑. 坏消息是, 用户几乎没有控制权.
AnimatedTexture是一个 Resource, 与此处讨论的其他 Node 对象不同. 可以创建一个 Sprite 节点, 该节点使用AnimatedTexture作为其纹理. 或者(其他方法做不到的)可以将AnimatedTexture作为图块, 添加到一个 TileSet 中, 并将其与 TileMap 集成在一起, 以获得许多自动动画化的背景, 所有的渲染将在单个批处理内绘制调用.AnimatedSprite节点, 与 SpriteFrames 资源结合使用, 允许用户通过精灵表创建各种动画序列, 在动画之间切换, 并控制它们的速度, 区域偏移量, 和方向. 这使得它们非常适合控制基于二维的帧动画. AnimationPlayer 节点与AnimatedSprite关联.AnimationPlayer也是你的必备工具, 例如… - 剪纸动画 : 在运行时编辑精灵的变换.
- 二维网格动画 : 为精灵的纹理划分一个区域, 并将骨骼绑定在上面. 然后动画化该骨骼, 使骨骼按照彼此之间的关系, 成比例地拉伸和弯曲纹理.
- 综上所述. AnimationTree 效果很出色. 可以在 这里 找到关于使用 AnimationTree 的深入指南.
