From 78e0b09c7af4376dc32361fbca1130d1825ae8d1 Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Tue, 14 Mar 2023 11:22:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 91 ++++++---- source/src/Core.ts | 17 +- source/src/Debug/Debug.ts | 24 ++- source/src/Math/Flags.ts | 62 ++++--- source/src/Math/Rectangle.ts | 64 ++++--- source/src/Physics/Collision.ts | 83 ++++++--- source/src/Physics/SpatialHash.ts | 282 ++++++++++++++++++------------ source/src/Utils/GlobalManager.ts | 27 +-- 8 files changed, 406 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 4c1adb65..90a0303a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ecs-framework 的目标是成为功能强大的框架。它为您构建游戏提供了坚实的基础。它包括的许多功能包括: - 完整的场景/实体/组件系统 -- SpatialHash用于超快速的广相物理学查找。您永远不会看到它,因为它在幕后起作用,但是您仍然会喜欢它,因为它可以通过射线广播或重叠检查迅速找到您附近的所有事物。 +- SpatialHash是一种空间散列数据结构,用于加速2D物理引擎的碰撞检测,它能够将物体分割为多个小区域并快速查询每个区域内包含的物体,从而大幅度提高碰撞检测的效率。 - AABB,圆和多边形碰撞/触发检测 - 高效的协程,可在多个帧或动画定时中分解大型任务(Core.startCoroutine) - 通过Astar和广度优先搜索提供寻路支持,以查找图块地图或您自己的自定义格式 ( 参见 https://github.com/esengine/ecs-astar ) @@ -24,51 +24,83 @@ ecs-framework 的目标是成为功能强大的框架。它为您构建游戏提 ## Scene/Entity/Component -框架的大部分围绕着实体组件系统(ECS)。ECS与您可能使用过的任何其他ECS均不同,所以我为您再以下详细介绍。 +Scene表示游戏场景,是所有实体和组件的容器;Entity表示游戏场景中的实体,是组件的容器;Component表示游戏实体中的组件,包含实体的具体行为逻辑。 ### Scene -ECS的根源。可以将场景视为游戏的不同部分,在适当的时间调用它们的方法。您也可以使用场景通过findEntity和findEntitiesByTag方法定位实体。 +这是一个ECS(Entity-Component-System)框架的场景类,用于管理游戏中的实体和处理器。它具有以下特点: -场景可以包含一种称为场景组件的特殊类型的组件。 SceneComponent通过add / get / removeSceneComponent方法进行管理。可以将场景组件视为简化组件。它包含少量可重写的生命周期方法(onEnabled / onDisabled / update / onRemovedFromScene)。当您需要一个位于场景级别但不需要实体容器的对象时,可以使用这些对象。 +- 维护了一个实体列表和一个实体处理器列表,以便在游戏中轻松添加、更新和删除实体。 +- 具有实体、组件和处理器的基本结构,可帮助您组织代码并实现分离的关注点。 +- 可以添加和删除场景组件,这是一个特殊类型的组件,可用于实现场景范围的逻辑。 +- 通过使用实体处理器,可以轻松地处理实体的更新和渲染。 +- 可以搜索场景中的实体、组件和处理器,并根据需要添加或删除它们。 +- 可以为实体分配唯一的标识符,以便在处理实体时轻松地跟踪它们。 ### Entity -将实体添加到场景中/从场景中删除,并由场景进行管理。 您可以子类化Entity,也可以只创建一个Entity实例,然后向其中添加任何必需的组件(通过addComponent,然后通过getComponent检索)。 在实体的最基本层次上,可以将其视为组件的容器。 实体具有一系列在整个生命周期中的不同时间被场景调用的方法。 +Entity 是ECS中的一个基础概念,它代表了游戏中的实体。每个 Entity 都有唯一的 ID 和名称,可以在一个 Scene 中被创建、添加、删除和管理。 -实体生命周期方法: -- onAddedToScene:在将所有未决的实体更改提交后将实体添加到场景中时调用 -- onRemovedFromScene:当实体从场景中移除时调用 -- update:只要启用了实体,就会每帧调用 +在一个 Entity 中,可以添加多个 Component 来实现不同的功能。例如,Transform Component 用来表示实体的位置、旋转和缩放等变换属性,其他自定义的 Component 则可以实现各种具体的游戏逻辑。 -实体上的一些关键/重要属性如下: +在 Entity 中,可以通过以下方法来管理 Component: -- updateOrder:控制实体的顺序。 这会影响在每个实体上调用更新的顺序以及标签列表的顺序。 -- tag:随便使用它。 以后可以使用它在场景中查询具有特定标签(Scene.findEntitiesByTag)的所有实体。 -- updateInterval:指定应多久调用一次此Entities更新方法。 1表示每帧,2表示每两帧,依此类推 +- createComponent(componentType: new (...args) => T): T 用来创建并返回一个指定类型的 Component 实例。 +- addComponent(component: T): T 用来添加一个已有的 Component 实例。 +- getComponent(type: new (...args) => T): T 用来获取指定类型的 Component 实例。 +- getComponentInScene(type: new (...args) => T): T 用来获取指定类型的 Component 实例,但会在整个场景中搜索。 +- tryGetComponent(type: new (...args) => T, outComponent: Ref): boolean 用来尝试获取指定类型的 Component 实例,并将结果存储在传入的 outComponent 参数中。 +- hasComponent(type: new (...args) => T): boolean 用来检查 Entity 是否有指定类型的 Component 实例。 +- getOrCreateComponent(type: new (...args) => T): T 如果 Entity 没有指定类型的 Component 实例,将会创建一个并返回。 +此外,还可以通过 removeComponent() 方法来移除一个指定的 Component 实例,或者 removeAllComponents() 方法来移除 Entity 中的所有 Component 实例。 + +最后,在 Entity 中还可以通过 Tween 类来实现各种动画效果,例如 tweenPositionTo()、tweenScaleTo() 和 tweenRotationDegreesTo() 等方法。 + +以下是Entity类中一些关键和重要的属性: + +- scene:实体所属的场景对象。 +- name:实体的名称。 +- id:实体的唯一标识符。 +- transform:实体的变换组件。 +- components:实体的组件列表。 +- updateInterval:实体更新间隔,用于控制实体的更新频率。 +- componentBits:用于标记实体拥有哪些组件。 +这些属性是Entity类中非常重要的,它们可以用来操作和控制实体的行为和状态。其中,transform和components属性是实体最为关键的组成部分,前者用于操作实体的位置、旋转和缩放等变换信息,后者用于添加、删除、查询和更新实体的组件信息。通过这些属性,我们可以非常方便地构建出自己的游戏对象,并实现一些基本的功能,如移动、碰撞、动画、音效等。 ### Component -组件添加到实体并由实体管理。 它们构成了您游戏的重点,并且基本上是可重用的代码块,这些代码决定了实体的行为方式。 +这是一个用于构建实体组件系统的基本组件类,所有其他组件都应该从这个抽象类派生。每个组件都可以与一个实体相关联,可以通过实体来访问其它组件。 -组件生命周期方法: +重要属性: -- initialize:在创建组件并分配Entity字段但在onAddedToEntity之前调用 -- onAddedToEntity:在将所有未决组件更改提交后将组件添加到实体时调用 -- onRemovedFromEntity:当组件从其实体中移除时调用。 在这里进行所有清理。 -- onEntityPositionChanged:在实体位置更改时调用。 这使组件可以知道它们是由于父实体移动而移动的。 -- update:只要启用了实体和组件并且组件实现IUpdatable,就会每帧调用 -- onEnabled:在启用父实体或组件时调用 -- onDisabled:在禁用父实体或组件时调用 +- id: 组件的唯一标识符。 +- entity: 附加此组件的实体。 +重要方法: + +- addComponent(component: T): T:将指定的组件添加到此组件所在的实体中。 +- getComponent(type: new (...args: any[]) => T): T:获取指定类型的组件。 +- getComponents(typeName: any, componentList?: any[]): any[]:获取指定类型的组件数组。 +- hasComponent(type: new (...args: any[]) => Component): boolean:判断实体是否包含指定类型的组件。 +- removeComponent(component?: Component): void:从此组件所在的实体中删除指定的组件,如果未指定组件,则删除此组件本身。 +此外,组件还具有一些生命周期方法,例如 initialize()、onAddedToEntity()、onRemovedFromEntity()、onEntityTransformChanged()、onEnabled()、onDisabled() 等,这些方法可以根据需要在派生类中进行重写,以便在实体上添加或移除组件时执行相应的操作。 ## Debug -Debug类提供日志记录。 Insist类提供各种断言条件。 您可以在整个代码中自由使用它们。 +这是一个静态类 Debug,它包含了一些方法用于打印调试信息。其中包含的方法如下: + +- warnIf(condition: boolean, format: string, ...args: any[]): 如果 condition 为 true,则打印警告信息。 +- warn(format: string, ...args: any[]): 打印警告信息。 +- error(format: string, ...args: any[]): 打印错误信息。 +- log(type: LogType, format: string, ...args: any[]): 打印指定类型的信息,可选的类型有 error、warn、log、info 和 trace。 +该类的优点是它提供了一种统一的调试信息输出方式,可以帮助开发者更方便地输出调试信息,以便在调试时更快地定位问题。缺点是它的功能比较单一,只能输出调试信息,不能对调试信息进行更加复杂的处理。 ## Flags -您是否喜欢将大量数据打包为单个number的功能,但讨厌处理该数据的语法? Flags类可以为您提供帮助。 它包括用于处理number的辅助方法,以检查是否设置了位以及设置/取消设置了它们。 处理Collider.physicsLayer非常方便。 +这是一个静态工具类 Flags,提供了一些位标志操作的方法: -### 示例地址 - -#### [laya-demo](https://github.com/esengine/ecs-laya-demo) -#### [egret-demo](https://github.com/esengine/ecs-egret-demo) +- isFlagSet:检查位标志是否已在数值中设置(该标志未移位) +- isUnshiftedFlagSet:检查位标志是否在数值中设置(该标志已移位) +- setFlagExclusive:设置数值标志位,移除所有已经设置的标志 +- setFlag:设置标志位 +- unsetFlag:取消标志位 +- invertFlags:反转数值集合位 +- binaryStringRepresentation:打印 number 的二进制表示,方便调试 number 标志。 ### 如何参与项目 #### Node.js版本 @@ -79,9 +111,6 @@ v10.20.1 3. 打包成js: `gulp build` > 如遇到gulp未找到则先执行 `npm install gulp -g` -### 渲染集成框架 -#### [cocos-framework](https://github.com/esengine/cocos-framework) - ## 扩展库 #### [基于ecs-framework开发的astar/BreadthFirst/Dijkstra/GOAP目标导向计划 路径寻找库](https://github.com/esengine/ecs-astar) diff --git a/source/src/Core.ts b/source/src/Core.ts index e2416c9d..2baadc47 100644 --- a/source/src/Core.ts +++ b/source/src/Core.ts @@ -115,17 +115,18 @@ module es { * 删除全局管理器对象 * @param manager */ - public static unregisterGlobalManager(manager: es.GlobalManager) { + public static unregisterGlobalManager(manager: GlobalManager) { new es.List(this._instance._globalManagers).remove(manager); manager.enabled = false; } /** - * 获取类型为T的全局管理器 - * @param type + * 获取指定类型的全局管理器实例 + * @param type 管理器类型的构造函数 + * @returns 指定类型的全局管理器实例,如果找不到则返回 null */ - public static getGlobalManager(type: new (...args) => T): T { - for (let i = 0, s = Core._instance._globalManagers.length; i < s; ++ i) { + public static getGlobalManager(type: new (...args) => T): T { + for (let i = 0, s = Core._instance._globalManagers.length; i < s; ++i) { let manager = Core._instance._globalManagers[i]; if (manager instanceof type) return manager; @@ -144,9 +145,9 @@ module es { } /** - * 启动一个coroutine。Coroutine可以将number延时几秒或延时到其他startCoroutine.Yielding + * 启动一个coroutine。Coroutine可以将number延时几秒或延时到其他startCoroutine.Yielding * null将使coroutine在下一帧被执行。 - * @param enumerator + * @param enumerator */ public static startCoroutine(enumerator): ICoroutine { return this._instance._coroutineManager.startCoroutine(enumerator); @@ -166,7 +167,7 @@ module es { public startDebugDraw() { // 如果debug标志未开启,则直接返回 if (!this.debug) return; - + // 计算帧率和内存使用情况 this._frameCounter++; // 帧计数器递增 this._frameCounterElapsedTime += Time.deltaTime; // 帧计数器累加时间 diff --git a/source/src/Debug/Debug.ts b/source/src/Debug/Debug.ts index 31826c55..68ae0f07 100644 --- a/source/src/Debug/Debug.ts +++ b/source/src/Debug/Debug.ts @@ -8,21 +8,43 @@ module es { } export class Debug { + /** + * 如果条件为true,则在控制台中以警告方式打印消息。 + * @param condition 是否应该打印消息的条件 + * @param format 要打印的消息格式 + * @param args 与消息格式相对应的参数列表 + */ public static warnIf(condition: boolean, format: string, ...args: any[]) { if (condition) this.log(LogType.warn, format, args); } + /** + * 在控制台中以警告方式打印消息。 + * @param format 要打印的消息格式 + * @param args 与消息格式相对应的参数列表 + */ public static warn(format: string, ...args: any[]) { this.log(LogType.warn, format, args); } + /** + * 在控制台中以错误方式打印消息。 + * @param format 要打印的消息格式 + * @param args 与消息格式相对应的参数列表 + */ public static error(format: string, ...args: any[]) { this.log(LogType.error, format, args); } + /** + * 在控制台中以标准日志方式打印消息。 + * @param type 要打印的日志类型 + * @param format 要打印的消息格式 + * @param args 与消息格式相对应的参数列表 + */ public static log(type: LogType, format: string, ...args: any[]) { - switch(type) { + switch (type) { case LogType.error: console.error(`${type}: ${StringUtils.format(format, args)}`); break; diff --git a/source/src/Math/Flags.ts b/source/src/Math/Flags.ts index 0144a2c5..205093d8 100644 --- a/source/src/Math/Flags.ts +++ b/source/src/Math/Flags.ts @@ -1,74 +1,78 @@ module es { /** - * 帮助处理位掩码的实用程序类 - * 除了isFlagSet之外,所有方法都期望flag参数是一个非移位的标志 - * 允许您使用普通的(0、1、2、3等)来设置/取消您的标记 + * 一个用于操作二进制标志(也称为位字段) */ export class Flags { /** - * 检查位标志是否已在数值中设置 - * 检查期望标志是否已经移位 - * @param self - * @param flag + * 检查指定二进制数字中是否已设置了指定标志位 + * @param self 二进制数字 + * @param flag 标志位,应该为2的幂 + * @returns 如果设置了指定的标志位,则返回true,否则返回false */ public static isFlagSet(self: number, flag: number): boolean { - return (self & flag) != 0; + return (self & flag) !== 0; } /** - * 检查位标志是否在数值中设置 - * @param self - * @param flag + * 检查指定二进制数字中是否已设置未移位的指定标志位 + * @param self 二进制数字 + * @param flag 标志位,不应移位(应为2的幂) + * @returns 如果设置了指定的标志位,则返回true,否则返回false */ public static isUnshiftedFlagSet(self: number, flag: number): boolean { flag = 1 << flag; - return (self & flag) != 0; + return (self & flag) !== 0; } /** - * 设置数值标志位,移除所有已经设置的标志 - * @param self - * @param flag + * 将指定的标志位设置为二进制数字的唯一标志 + * @param self 二进制数字 + * @param flag 标志位,应该为2的幂 */ public static setFlagExclusive(self: Ref, flag: number) { self.value = 1 << flag; } /** - * 设置标志位 - * @param self - * @param flag + * 将指定的标志位设置为二进制数字 + * @param self 二进制数字的引用 + * @param flag 标志位,应该为2的幂 */ public static setFlag(self: Ref, flag: number) { - self.value = (self.value | 1 << flag); + self.value |= 1 << flag; } /** - * 取消标志位 - * @param self - * @param flag + * 将指定的标志位从二进制数字中取消设置 + * @param self 二进制数字的引用 + * @param flag 标志位,应该为2的幂 */ public static unsetFlag(self: Ref, flag: number) { flag = 1 << flag; - self.value = (self.value & (~flag)); + self.value &= ~flag; } /** - * 反转数值集合位 - * @param self + * 反转二进制数字中的所有位(将1变为0,将0变为1) + * @param self 二进制数字的引用 */ public static invertFlags(self: Ref) { self.value = ~self.value; } /** - * 打印 number 的二进制表示。 方便调试 number 标志 + * 返回二进制数字的字符串表示形式(以二进制形式) + * @param self 二进制数字 + * @param leftPadWidth 返回的字符串的最小宽度(在左侧填充0) + * @returns 二进制数字的字符串表示形式 */ - public static binaryStringRepresentation(self: number, - leftPadWidth: number = 10) { + public static binaryStringRepresentation( + self: number, + leftPadWidth = 10 + ): string { let str = self.toString(2); while (str.length < (leftPadWidth || 2)) { - str = '0' + str; + str = "0" + str; } return str; } diff --git a/source/src/Math/Rectangle.ts b/source/src/Math/Rectangle.ts index 73ee18ca..4279541d 100644 --- a/source/src/Math/Rectangle.ts +++ b/source/src/Math/Rectangle.ts @@ -98,7 +98,7 @@ module es { this.height = value.y; } - /** + /** * 位于这个矩形中心的一个点 * 如果 "宽度 "或 "高度 "是奇数,则中心点将向下舍入 */ @@ -163,7 +163,7 @@ module es { /** * 获取指定边缘的位置 - * @param edge + * @param edge */ public getSide(edge: Edge) { switch (edge) { @@ -214,28 +214,34 @@ module es { } public rayIntersects(ray: Ray2D): { intersected: boolean; distance: number } { - const res = { intersected: false, distance: 0 }; + // 存储相交点和相交距离 + const res = {intersected: false, distance: 0}; let maxValue = Infinity; - + + // 计算射线与矩形的相交距离 if (Math.abs(ray.direction.x) < 1E-06) { + // 如果射线方向的x分量很小,说明它是垂直的,那么它就不会相交 if (ray.start.x < this.x || ray.start.x > this.x + this.width) { return res; } } else { + // 计算射线与x边界的交点,以及在矩形上面和下面的交点 const num11 = 1 / ray.direction.x; let num8 = (this.x - ray.start.x) * num11; let num7 = (this.x + this.width - ray.start.x) * num11; if (num8 > num7) { [num7, num8] = [num8, num7]; } - + + // 将最远的相交距离更新为上下两个交点中更远的那个 res.distance = Math.max(num8, res.distance); maxValue = Math.min(num7, maxValue); if (res.distance > maxValue) { return res; } } - + + // 计算射线与y边界的交点,以及在矩形左边和右边的交点 if (Math.abs(ray.direction.y) < 1e-06) { if (ray.start.y < this.y || ray.start.y > this.y + this.height) { return res; @@ -247,14 +253,16 @@ module es { if (num6 > num5) { [num5, num6] = [num6, num5]; } - + + // 将最远的相交距离更新为左右两个交点中更远的那个 res.distance = Math.max(num6, res.distance); maxValue = Math.min(num5, maxValue); if (res.distance > maxValue) { return res; } } - + + // 如果相交了,将标志设为真,并返回相交点 res.intersected = true; return res; } @@ -265,7 +273,7 @@ module es { */ public containsRect(value: Rectangle) { return ((((this.x <= value.x) && (value.x < (this.x + this.width))) && - (this.y <= value.y)) && + (this.y <= value.y)) && (value.y < (this.y + this.height))); } @@ -358,8 +366,8 @@ module es { /** * 创建一个新的RectangleF,该RectangleF包含两个其他矩形的重叠区域 - * @param value1 - * @param value2 + * @param value1 + * @param value2 * @returns 将两个矩形的重叠区域作为输出参数 */ public static intersect(value1: Rectangle, value2: Rectangle) { @@ -386,8 +394,8 @@ module es { /** * 创建一个完全包含两个其他矩形的新矩形 - * @param value1 - * @param value2 + * @param value1 + * @param value2 */ public static union(value1: Rectangle, value2: Rectangle) { let x = Math.min(value1.x, value2.x); @@ -399,8 +407,8 @@ module es { /** * 在矩形重叠的地方创建一个新的矩形 - * @param value1 - * @param value2 + * @param value1 + * @param value2 */ public static overlap(value1: Rectangle, value2: Rectangle): Rectangle { let x = Math.max(value1.x, value2.x, 0); @@ -411,7 +419,7 @@ module es { } public calculateBounds(parentPosition: Vector2, position: Vector2, origin: Vector2, scale: Vector2, - rotation: number, width: number, height: number) { + rotation: number, width: number, height: number) { if (rotation == 0) { this.x = Math.trunc(parentPosition.x + position.x - origin.x * scale.x); this.y = Math.trunc(parentPosition.y + position.y - origin.y * scale.y); @@ -456,10 +464,10 @@ module es { /** * 返回一个横跨当前矩形和提供的三角形位置的矩形 - * @param deltaX - * @param deltaY + * @param deltaX + * @param deltaY */ - public getSweptBroadphaseBounds(deltaX: number, deltaY: number){ + public getSweptBroadphaseBounds(deltaX: number, deltaY: number) { let broadphasebox = Rectangle.empty; broadphasebox.x = deltaX > 0 ? this.x : this.x + deltaX; @@ -471,13 +479,13 @@ module es { } /** - * 如果发生碰撞,返回true + * 如果发生碰撞,返回true * moveX和moveY将返回b1为避免碰撞而必须移动的移动量 - * @param other - * @param moveX - * @param moveY + * @param other + * @param moveX + * @param moveY */ - public collisionCheck(other: Rectangle, moveX: Ref, moveY: Ref){ + public collisionCheck(other: Rectangle, moveX: Ref, moveY: Ref) { moveX.value = moveY.value = 0; let l = other.x - (this.x + this.width); @@ -504,8 +512,8 @@ module es { /** * 计算两个矩形之间有符号的交点深度 - * @param rectA - * @param rectB + * @param rectA + * @param rectB * @returns 两个相交的矩形之间的重叠量。 * 这些深度值可以是负值,取决于矩形/相交的哪些边。 * 这允许调用者确定正确的推送对象的方向,以解决碰撞问题。 @@ -541,7 +549,7 @@ module es { /** * 比较当前实例是否等于指定的矩形 - * @param other + * @param other */ public equals(other: Rectangle) { return this === other; @@ -550,7 +558,7 @@ module es { /** * 获取这个矩形的哈希码 */ - public getHashCode(): number{ + public getHashCode(): number { return (Math.trunc(this.x) ^ Math.trunc(this.y) ^ Math.trunc(this.width) ^ Math.trunc(this.height)); } diff --git a/source/src/Physics/Collision.ts b/source/src/Physics/Collision.ts index 405740ff..e6280974 100644 --- a/source/src/Physics/Collision.ts +++ b/source/src/Physics/Collision.ts @@ -125,46 +125,75 @@ module es { return false; } - public static rectToLine(rect: Rectangle, lineFrom: Vector2, lineTo: Vector2) { + /** + * 检查矩形和线段之间是否相交 + * @param rect - 要检查的矩形 + * @param lineFrom - 线段起点 + * @param lineTo - 线段终点 + * @returns 如果相交返回 true,否则返回 false + */ + public static rectToLine(rect: Rectangle, lineFrom: Vector2, lineTo: Vector2): boolean { + // 获取起点和终点所在矩形的位置 const fromSector = this.getSector(rect.x, rect.y, rect.width, rect.height, lineFrom); const toSector = this.getSector(rect.x, rect.y, rect.width, rect.height, lineTo); + // 起点或终点位于矩形内部 if (fromSector == PointSectors.center || toSector == PointSectors.center) { return true; - } else if ((fromSector & toSector) != 0) { + } + + // 起点和终点都在矩形外部的同一区域 + if ((fromSector & toSector) != 0) { return false; - } else { - const both = fromSector | toSector; - // 线对边进行检查 - let edgeFrom: Vector2; - let edgeTo: Vector2; + } - if ((both & PointSectors.top) != 0) { - edgeFrom = new Vector2(rect.x, rect.y); - edgeTo = new Vector2(rect.x + rect.width, rect.y); - if (this.lineToLine(edgeFrom, edgeTo, lineFrom, lineTo)) - return true; + // 到这里说明起点和终点分别在矩形的两个不同区域,需要检查线段是否与矩形的边相交 + + // 枚举起点和终点所在区域 + const both = fromSector | toSector; + + // 逐条检查矩形的四条边是否与线段相交 + if ((both & PointSectors.top) != 0) { + if (this.lineToLine( + new Vector2(rect.x, rect.y), + new Vector2(rect.x + rect.width, rect.y), + lineFrom, + lineTo + )) { + return true; } + } - if ((both & PointSectors.bottom) != 0) { - edgeFrom = new Vector2(rect.x, rect.y + rect.height); - edgeTo = new Vector2(rect.x + rect.width, rect.y + rect.height); - if (this.lineToLine(edgeFrom, edgeTo, lineFrom, lineTo)) - return true; + if ((both & PointSectors.bottom) != 0) { + if (this.lineToLine( + new Vector2(rect.x, rect.y + rect.height), + new Vector2(rect.x + rect.width, rect.y + rect.height), + lineFrom, + lineTo + )) { + return true; } + } - if ((both & PointSectors.left) != 0) { - edgeFrom = new Vector2(rect.x, rect.y); - edgeTo = new Vector2(rect.x, rect.y + rect.height); - if (this.lineToLine(edgeFrom, edgeTo, lineFrom, lineTo)) - return true; + if ((both & PointSectors.left) != 0) { + if (this.lineToLine( + new Vector2(rect.x, rect.y), + new Vector2(rect.x, rect.y + rect.height), + lineFrom, + lineTo + )) { + return true; } + } - if ((both & PointSectors.right) != 0) { - edgeFrom = new Vector2(rect.x + rect.width, rect.y); - edgeTo = new Vector2(rect.x + rect.width, rect.y + rect.height); - if (this.lineToLine(edgeFrom, edgeTo, lineFrom, lineTo)) - return true; + if ((both & PointSectors.right) != 0) { + if (this.lineToLine( + new Vector2(rect.x + rect.width, rect.y), + new Vector2(rect.x + rect.width, rect.y + rect.height), + lineFrom, + lineTo + )) { + return true; } } diff --git a/source/src/Physics/SpatialHash.ts b/source/src/Physics/SpatialHash.ts index 16071d89..fd2c150e 100644 --- a/source/src/Physics/SpatialHash.ts +++ b/source/src/Physics/SpatialHash.ts @@ -35,49 +35,56 @@ module es { } /** - * 将对象添加到SpatialHash - * @param collider + * 注册一个碰撞器 + * @param collider 碰撞器 */ - public register(collider: Collider) { - let bounds = collider.bounds.clone(); + public register(collider: Collider): void { + // 克隆碰撞器的 bounds 属性 + const bounds = collider.bounds.clone(); + // 存储克隆后的 bounds 属性到 registeredPhysicsBounds 属性中 collider.registeredPhysicsBounds = bounds; - let p1 = this.cellCoords(bounds.x, bounds.y); - let p2 = this.cellCoords(bounds.right, bounds.bottom); + // 获取碰撞器所在的网格坐标 + const p1 = this.cellCoords(bounds.x, bounds.y); + const p2 = this.cellCoords(bounds.right, bounds.bottom); - // 更新边界以跟踪网格大小 + // 更新网格边界,以确保其覆盖所有碰撞器 if (!this.gridBounds.contains(p1.x, p1.y)) { this.gridBounds = RectangleExt.union(this.gridBounds, p1); } - if (!this.gridBounds.contains(p2.x, p2.y)) { this.gridBounds = RectangleExt.union(this.gridBounds, p2); } + // 将碰撞器添加到所在的所有单元格中 for (let x = p1.x; x <= p2.x; x++) { for (let y = p1.y; y <= p2.y; y++) { - // 如果没有单元格,我们需要创建它 - let c: Collider[] = this.cellAtPosition(x, y, true); - c.push(collider); + // 如果该单元格不存在,创建一个新的单元格 + const cell: Collider[] = this.cellAtPosition(x, y, /* createIfNotExists = */ true); + cell.push(collider); } } } /** - * 从SpatialHash中删除对象 - * @param collider + * 从空间哈希中移除一个碰撞器 + * @param collider 碰撞器 */ - public remove(collider: Collider) { - let bounds = collider.registeredPhysicsBounds.clone(); - let p1 = this.cellCoords(bounds.x, bounds.y); - let p2 = this.cellCoords(bounds.right, bounds.bottom); + public remove(collider: Collider): void { + // 克隆碰撞器的 registeredPhysicsBounds 属性 + const bounds = collider.registeredPhysicsBounds.clone(); + // 获取碰撞器所在的网格坐标 + const p1 = this.cellCoords(bounds.x, bounds.y); + const p2 = this.cellCoords(bounds.right, bounds.bottom); + // 从所有单元格中移除该碰撞器 for (let x = p1.x; x <= p2.x; x++) { for (let y = p1.y; y <= p2.y; y++) { - // 单元格应该始终存在,因为这个碰撞器应该在所有查询的单元格中 - let cell = this.cellAtPosition(x, y); + // 单元格应该始终存在,因为该碰撞器应该在所有查询的单元格中 + const cell = this.cellAtPosition(x, y); Insist.isNotNull(cell, `从不存在碰撞器的单元格中移除碰撞器: [${collider}]`); - if (cell != null) + if (cell != null) { new es.List(cell).remove(collider); + } } } } @@ -95,32 +102,38 @@ module es { } /** - * 返回边框与单元格相交的所有对象 - * @param bounds - * @param excludeCollider - * @param layerMask + * 执行基于 AABB 的广域相交检测并返回碰撞器列表 + * @param bounds 边界矩形 + * @param excludeCollider 排除的碰撞器 + * @param layerMask 碰撞层掩码 + * @returns 碰撞器列表 */ public aabbBroadphase(bounds: Rectangle, excludeCollider: Collider, layerMask: number): Collider[] { this._tempHashSet.clear(); + // 获取边界矩形所在的网格单元格 const p1 = this.cellCoords(bounds.x, bounds.y); const p2 = this.cellCoords(bounds.right, bounds.bottom); + // 对所有相交的单元格中的碰撞器执行检测 for (let x = p1.x; x <= p2.x; x++) { for (let y = p1.y; y <= p2.y; y++) { const cell = this.cellAtPosition(x, y); - if (!cell) + if (!cell) { continue; + } - // 当cell不为空。循环并取回所有碰撞器 + // 如果单元格不为空,循环并取回所有碰撞器 if (cell.length > 0) { for (let i = 0; i < cell.length; i++) { const collider = cell[i]; - - // 如果它是自身或者如果它不匹配我们的层掩码 跳过这个碰撞器 - if (collider == excludeCollider || !Flags.isFlagSet(layerMask, collider.physicsLayer.value)) + + // 如果它是自身或者如果它不匹配我们的层掩码跳过这个碰撞器 + if (collider === excludeCollider || !Flags.isFlagSet(layerMask, collider.physicsLayer.value)) { continue; - + } + + // 检查碰撞器的 bounds 是否与边界矩形相交 if (bounds.intersects(collider.bounds)) { this._tempHashSet.add(collider); } @@ -129,66 +142,65 @@ module es { } } + // 返回所有相交的碰撞器列表 return Array.from(this._tempHashSet); } /** - * 通过空间散列投掷一条线,并将该线碰到的任何碰撞器填入碰撞数组 - * https://github.com/francisengelmann/fast_voxel_traversal/blob/master/main.cpp - * http://www.cse.yorku.ca/~amana/research/grid.pdf - * @param start - * @param end - * @param hits - * @param layerMask + * 执行基于线段的射线检测并返回所有命中的碰撞器 + * @param start 射线起点 + * @param end 射线终点 + * @param hits 射线命中结果 + * @param layerMask 碰撞层掩码 + * @param ignoredColliders 忽略的碰撞器 + * @returns 命中的碰撞器数量 */ - public linecast(start: Vector2, end: Vector2, hits: RaycastHit[], layerMask: number, ignoredColliders: Set) { - let ray = new Ray2D(start, end); + public linecast(start: Vector2, end: Vector2, hits: RaycastHit[], layerMask: number, ignoredColliders: Set): number { + // 创建一个射线 + const ray = new Ray2D(start, end); + // 使用射线解析器初始化线段命中结果 this._raycastParser.start(ray, hits, layerMask, ignoredColliders); - // 获取我们的起始/结束位置,与我们的网格在同一空间内 + // 获取起点和终点所在的网格单元格 let currentCell = this.cellCoords(start.x, start.y); - let lastCell = this.cellCoords(end.x, end.y); + const lastCell = this.cellCoords(end.x, end.y); - // 我们向什么方向递增单元格检查? + // 计算射线在 x 和 y 方向上的步长 let stepX = Math.sign(ray.direction.x); let stepY = Math.sign(ray.direction.y); + if (currentCell.x === lastCell.x) { + stepX = 0; + } + if (currentCell.y === lastCell.y) { + stepY = 0; + } - // 我们要确保,如果我们在同一条线上或同一排上,就不会踩到不必要的方向上 - if (currentCell.x == lastCell.x) stepX = 0; - if (currentCell.y == lastCell.y) stepY = 0; - - // 计算单元格的边界。 - // 当步长为正数时,下一个单元格在这个单元格之后,意味着我们要加1。 - let xStep = stepX < 0 ? 0 : stepX; - let yStep = stepY < 0 ? 0 : stepY; + // 计算 x 和 y 方向上的网格单元格步长 + const xStep = stepX < 0 ? 0 : stepX; + const yStep = stepY < 0 ? 0 : stepY; let nextBoundaryX = (currentCell.x + xStep) * this._cellSize; let nextBoundaryY = (currentCell.y + yStep) * this._cellSize; - // 确定射线穿过第一个垂直体素边界时的t值,y/水平也是如此。 - // 这两个值的最小值将表明我们可以沿着射线移动多少,并且仍然保持在当前的体素中,对于接近垂直/水平的射线可能是无限的。 - let tMaxX = ray.direction.x != 0 ? (nextBoundaryX - ray.start.x) / ray.direction.x : Number.MAX_VALUE; - let tMaxY = ray.direction.y != 0 ? (nextBoundaryY - ray.start.y) / ray.direction.y : Number.MAX_VALUE; + // 计算 t 值的最大值和步长 + let tMaxX = ray.direction.x !== 0 ? (nextBoundaryX - ray.start.x) / ray.direction.x : Number.MAX_VALUE; + let tMaxY = ray.direction.y !== 0 ? (nextBoundaryY - ray.start.y) / ray.direction.y : Number.MAX_VALUE; + const tDeltaX = ray.direction.x !== 0 ? this._cellSize / (ray.direction.x * stepX) : Number.MAX_VALUE; + const tDeltaY = ray.direction.y !== 0 ? this._cellSize / (ray.direction.y * stepY) : Number.MAX_VALUE; - // 我们要走多远才能从一个单元格的边界穿过一个单元格 - let tDeltaX = ray.direction.x != 0 ? this._cellSize / (ray.direction.x * stepX) : Number.MAX_VALUE; - let tDeltaY = ray.direction.y != 0 ? this._cellSize / (ray.direction.y * stepY) : Number.MAX_VALUE; - - // 开始遍历并返回交叉单元格。 + // 检查射线起点所在的单元格是否与射线相交 let cell = this.cellAtPosition(currentCell.x, currentCell.y); - - if (cell != null && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)) { + if (cell !== null && this._raycastParser.checkRayIntersection(currentCell.x, currentCell.y, cell)) { this._raycastParser.reset(); return this._raycastParser.hitCounter; } - while (currentCell.x != lastCell.x || currentCell.y != lastCell.y) { + // 在所有相交的单元格中沿着射线前进并检查碰撞器 + while (currentCell.x !== lastCell.x || currentCell.y !== lastCell.y) { if (tMaxX < tMaxY) { currentCell.x = MathHelper.toInt(MathHelper.approach(currentCell.x, lastCell.x, Math.abs(stepX))); - tMaxX += tDeltaX; } else { currentCell.y = MathHelper.toInt(MathHelper.approach(currentCell.y, lastCell.y, Math.abs(stepY))); - tMaxY += tDeltaY; } @@ -199,36 +211,43 @@ module es { } } - // 复位 + // 重置射线解析器并返回命中的碰撞器数量 this._raycastParser.reset(); return this._raycastParser.hitCounter; } /** - * 获取所有在指定矩形范围内的碰撞器 - * @param rect - * @param results - * @param layerMask + * 执行矩形重叠检测并返回所有命中的碰撞器 + * @param rect 矩形 + * @param results 碰撞器命中结果 + * @param layerMask 碰撞层掩码 + * @returns 命中的碰撞器数量 */ - public overlapRectangle(rect: Rectangle, results: Collider[], layerMask: number) { + public overlapRectangle(rect: Rectangle, results: Collider[], layerMask: number): number { + // 更新重叠检测框的位置和大小 this._overlapTestBox.updateBox(rect.width, rect.height); this._overlapTestBox.position = rect.location; let resultCounter = 0; - let potentials = this.aabbBroadphase(rect, null, layerMask); + // 获取潜在的相交碰撞器 + const potentials = this.aabbBroadphase(rect, null, layerMask); - for (let i = 0; i < potentials.length; i ++) { + // 遍历所有潜在的碰撞器并检查它们是否与矩形相交 + for (let i = 0; i < potentials.length; i++) { const collider = potentials[i]; if (collider instanceof BoxCollider) { + // 如果是 BoxCollider,直接将其添加到命中结果中 results[resultCounter] = collider; resultCounter++; } else if (collider instanceof CircleCollider) { + // 如果是 CircleCollider,使用 rectToCircle 函数检查矩形与圆是否相交 if (Collisions.rectToCircle(rect, collider.bounds.center, collider.bounds.width * 0.5)) { results[resultCounter] = collider; resultCounter++; } } else if (collider instanceof PolygonCollider) { + // 如果是 PolygonCollider,使用 Polygon.shape.overlaps 函数检查矩形与多边形是否相交 if (collider.shape.overlaps(this._overlapTestBox)) { results[resultCounter] = collider; resultCounter++; @@ -237,42 +256,53 @@ module es { throw new Error("overlapRectangle对这个类型没有实现!"); } - if (resultCounter == results.length) + if (resultCounter === results.length) { return resultCounter; + } } return resultCounter; } + /** - * 获取所有落在指定圆圈内的碰撞器 - * @param circleCenter - * @param radius - * @param results - * @param layerMask + * 执行圆形重叠检测并返回所有命中的碰撞器 + * @param circleCenter 圆心坐标 + * @param radius 圆形半径 + * @param results 碰撞器命中结果 + * @param layerMask 碰撞层掩码 + * @returns 命中的碰撞器数量 */ - public overlapCircle(circleCenter: Vector2, radius: number, results: Collider[], layerMask): number { + public overlapCircle(circleCenter: Vector2, radius: number, results: Collider[], layerMask: number): number { + // 计算包含圆形的最小矩形框 const bounds = new Rectangle(circleCenter.x - radius, circleCenter.y - radius, radius * 2, radius * 2); + // 更新重叠检测圆的位置和半径 this._overlapTestCircle.radius = radius; this._overlapTestCircle.position = circleCenter; let resultCounter = 0; + // 获取潜在的相交碰撞器 const potentials = this.aabbBroadphase(bounds, null, layerMask); - if (potentials.length > 0) - for (let i = 0; i < potentials.length; i ++) { + + // 遍历所有潜在的碰撞器并检查它们是否与圆相交 + if (potentials.length > 0) { + for (let i = 0; i < potentials.length; i++) { const collider = potentials[i]; if (collider instanceof BoxCollider) { + // 如果是 BoxCollider,使用 BoxCollider.shape.overlaps 函数检查矩形与圆是否相交 if (collider.shape.overlaps(this._overlapTestCircle)) { results[resultCounter] = collider; resultCounter++; } } else if (collider instanceof CircleCollider) { + // 如果是 CircleCollider,使用 CircleCollider.shape.overlaps 函数检查圆与圆是否相交 if (collider.shape.overlaps(this._overlapTestCircle)) { results[resultCounter] = collider; resultCounter++; } } else if (collider instanceof PolygonCollider) { + // 如果是 PolygonCollider,使用 PolygonCollider.shape.overlaps 函数检查多边形与圆是否相交 if (collider.shape.overlaps(this._overlapTestCircle)) { results[resultCounter] = collider; resultCounter++; @@ -281,70 +311,101 @@ module es { throw new Error("对这个对撞机类型的overlapCircle没有实现!"); } - // 如果我们所有的结果数据有了则返回 - if (resultCounter === results.length) + if (resultCounter === results.length) { return resultCounter; + } } + } return resultCounter; } /** - * 获取单元格的x,y值作为世界空间的x,y值 - * @param x - * @param y + * 将给定的 x 和 y 坐标转换为单元格坐标 + * @param x X 坐标 + * @param y Y 坐标 + * @returns 转换后的单元格坐标 */ public cellCoords(x: number, y: number): Vector2 { + // 使用 inverseCellSize 计算出单元格的 x 和 y 坐标 return new Vector2(Math.floor(x * this._inverseCellSize), Math.floor(y * this._inverseCellSize)); } /** - * 获取世界空间x,y值的单元格。 - * 如果单元格为空且createCellIfEmpty为true,则会创建一个新的单元格 - * @param x - * @param y - * @param createCellIfEmpty + * 返回一个包含特定位置处的所有碰撞器的数组 + * 如果此位置上没有单元格并且createCellIfEmpty参数为true,则会创建一个新的单元格 + * @param x 单元格 x 坐标 + * @param y 单元格 y 坐标 + * @param createCellIfEmpty 如果该位置上没有单元格是否创建一个新单元格,默认为false + * @returns 该位置上的所有碰撞器 */ public cellAtPosition(x: number, y: number, createCellIfEmpty: boolean = false): Collider[] { + // 获取指定位置的单元格 let cell: Collider[] = this._cellDict.tryGetValue(x, y); + + // 如果不存在此位置的单元格,并且需要创建,则创建并返回空单元格 if (!cell) { if (createCellIfEmpty) { cell = []; this._cellDict.add(x, y, cell); } } + return cell; } } + /** + * 数字字典 + */ export class NumberDictionary { + // 存储数据的 Map 对象 public _store: Map = new Map(); + /** + * 将指定的列表添加到以给定 x 和 y 为键的字典条目中 + * @param x 字典的 x 坐标 + * @param y 字典的 y 坐标 + * @param list 要添加到字典的列表 + */ public add(x: number, y: number, list: T[]) { this._store.set(this.getKey(x, y), list); } /** - * 使用蛮力方法从字典存储列表中移除碰撞器 - * @param obj + * 从字典中删除给定的对象 + * @param obj 要删除的对象 */ public remove(obj: T) { + // 遍历 Map 中的所有值,从值中查找并删除给定的对象 this._store.forEach(list => { let index = list.indexOf(obj); list.splice(index, 1); }) } + /** + * 尝试从字典中检索指定键的值 + * @param x 字典的 x 坐标 + * @param y 字典的 y 坐标 + * @returns 指定键的值,如果不存在则返回 null + */ public tryGetValue(x: number, y: number): T[] { return this._store.get(this.getKey(x, y)); } + /** + * 根据给定的 x 和 y 坐标返回一个唯一的字符串键 + * @param x 字典的 x 坐标 + * @param y 字典的 y 坐标 + * @returns 唯一的字符串键 + */ public getKey(x: number, y: number) { return `${x}_${y}`; } /** - * 清除字典数据 + * 清空字典 */ public clear() { this._store.clear(); @@ -378,46 +439,51 @@ module es { } /** - * 如果hits数组被填充,返回true。单元格不能为空! - * @param cellX - * @param cellY - * @param cell + * 对射线检测到的碰撞器进行进一步的处理,将结果存储在传递的碰撞数组中。 + * @param cellX 当前单元格的x坐标 + * @param cellY 当前单元格的y坐标 + * @param cell 该单元格中的碰撞器列表 + * @returns 如果当前单元格有任何碰撞器与射线相交,则返回true */ public checkRayIntersection(cellX: number, cellY: number, cell: Collider[]): boolean { for (let i = 0; i < cell.length; i++) { const potential = cell[i]; - // 管理我们已经处理过的碰撞器 + // 如果该碰撞器已经处理过,则跳过它 if (this._checkedColliders.indexOf(potential) != -1) continue; + // 将该碰撞器标记为已处理 this._checkedColliders.push(potential); - // 只有当我们被设置为这样做时才会点击触发器 + + // 如果该碰撞器是触发器且当前不允许触发器响应射线检测,则跳过它 if (potential.isTrigger && !Physics.raycastsHitTriggers) continue; - // 确保碰撞器在图层蒙版上 + // 确保碰撞器的图层与所提供的图层掩码相匹配 if (!Flags.isFlagSet(this._layerMask, potential.physicsLayer.value)) continue; + // 如果设置了要忽略的碰撞器并且该碰撞器是被忽略的,则跳过它 if (this._ignoredColliders && this._ignoredColliders.has(potential)) { continue; } - // TODO: rayIntersects的性能够吗?需要测试它。Collisions.rectToLine可能更快 - // TODO: 如果边界检查返回更多数据,我们就不需要为BoxCollider检查做任何事情 - // 在做形状测试之前先做一个边界检查 + // TODO: Collisions.rectToLine方法可能会更快一些,因为它没有涉及到浮点数除法和平方根计算,而且更简单 + // 但是,rayIntersects方法也很快,并且在实际情况下可能更适合用于特定的应用程序 + // 先进行一个边界检查 const colliderBounds = potential.bounds; const res = colliderBounds.rayIntersects(this._ray); - if (res.intersected && res.distance <= 1) { + if (res.intersected && res.distance <= 1) { // 只有当该碰撞器与射线相交且交点在射线长度范围内才进一步进行形状检测 let tempHit = new Out(this._tempHit); + + // 调用形状的方法,检查该碰撞器是否与射线相交,并将结果保存在tempHit中 if (potential.shape.collidesWithLine(this._ray.start, this._ray.end, tempHit)) { - // 检查一下,我们应该排除这些射线,射线cast是否在碰撞器中开始 + // 如果碰撞器包含射线起点,而且不允许射线起点在碰撞器中启动检测,那么跳过该碰撞器 if (!Physics.raycastsStartInColliders && potential.shape.containsPoint(this._ray.start)) continue; - // TODO: 确保碰撞点在当前单元格中,如果它没有保存它以供以后计算 - + // 将碰撞信息添加到列表中 tempHit.value.collider = potential; this._cellHits.push(tempHit.value); } diff --git a/source/src/Utils/GlobalManager.ts b/source/src/Utils/GlobalManager.ts index f4799309..f50bc6bf 100644 --- a/source/src/Utils/GlobalManager.ts +++ b/source/src/Utils/GlobalManager.ts @@ -1,55 +1,58 @@ module es { + /** + * 全局管理器的基类。所有全局管理器都应该从此类继承。 + */ export class GlobalManager { + /** + * 表示管理器是否启用 + */ public _enabled: boolean; /** - * 如果true则启用了GlobalManager。 - * 状态的改变会导致调用OnEnabled/OnDisable + * 获取或设置管理器是否启用 */ public get enabled() { return this._enabled; } - /** - * 如果true则启用了GlobalManager。 - * 状态的改变会导致调用OnEnabled/OnDisable - * @param value - */ public set enabled(value: boolean) { this.setEnabled(value); } /** - * 启用/禁用这个GlobalManager - * @param isEnabled + * 设置管理器是否启用 + * @param isEnabled 如果为true,则启用管理器;否则禁用管理器 */ public setEnabled(isEnabled: boolean) { if (this._enabled != isEnabled) { this._enabled = isEnabled; if (this._enabled) { + // 如果启用了管理器,则调用onEnabled方法 this.onEnabled(); } else { + // 如果禁用了管理器,则调用onDisabled方法 this.onDisabled(); } } } /** - * 此GlobalManager启用时调用 + * 在启用管理器时调用的回调方法 */ public onEnabled() { } /** - * 此GlobalManager禁用时调用 + * 在禁用管理器时调用的回调方法 */ public onDisabled() { } /** - * 在frame .update之前调用每一帧 + * 更新管理器状态的方法 */ public update() { } } + }