├── .fleet └── settings.json ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── collision.md ├── coroutine.md ├── create_entity_component.md ├── emitter.md ├── getting_start.md ├── physics.md ├── render.md ├── scene_component.md ├── system.md └── time.md ├── source ├── .idea │ ├── .gitignore │ ├── misc.xml │ ├── modules.xml │ ├── source.iml │ └── vcs.xml ├── .vscode │ └── tasks.json ├── .wing │ └── settings.json ├── bin │ ├── framework.d.ts │ ├── framework.js │ └── framework.min.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── src │ ├── Core.ts │ ├── Debug │ │ ├── Console │ │ │ └── DebugConsole.ts │ │ ├── Debug.ts │ │ ├── DebugDefaults.ts │ │ └── Insist.ts │ ├── ECS │ │ ├── Component.ts │ │ ├── ComponentType.ts │ │ ├── Components │ │ │ ├── IUpdatable.ts │ │ │ ├── Physics │ │ │ │ ├── ArcadeRigidbody.ts │ │ │ │ ├── CharacterController.ts │ │ │ │ ├── Colliders │ │ │ │ │ ├── BoxCollider.ts │ │ │ │ │ ├── CircleCollider.ts │ │ │ │ │ ├── Collider.ts │ │ │ │ │ ├── PolygonCollider.ts │ │ │ │ │ └── SectorCollider.ts │ │ │ │ ├── ITriggerListener.ts │ │ │ │ ├── Mover.ts │ │ │ │ └── ProjectileMover.ts │ │ │ └── SceneComponent.ts │ │ ├── CoreEvents.ts │ │ ├── Entity.ts │ │ ├── Scene.ts │ │ ├── SceneTransition.ts │ │ ├── Systems │ │ │ ├── DelayedIteratingSystem.ts │ │ │ ├── EntityProcessingSystem.ts │ │ │ ├── EntitySystem.ts │ │ │ ├── IntervalIteratingSystem.ts │ │ │ ├── IntervalSystem.ts │ │ │ ├── PassiveSystem.ts │ │ │ └── ProcessingSystem.ts │ │ ├── Transform.ts │ │ └── Utils │ │ │ ├── Bits.ts │ │ │ ├── ComponentList.ts │ │ │ ├── ComponentTypeFactory.ts │ │ │ ├── ComponentTypeManager.ts │ │ │ ├── EntityList.ts │ │ │ ├── EntityProcessorList.ts │ │ │ ├── HashHelper.ts │ │ │ ├── IdentifierPool.ts │ │ │ ├── Matcher.ts │ │ │ ├── StringUtils.ts │ │ │ ├── Time.ts │ │ │ └── TimeUtils.ts │ ├── Math │ │ ├── Bezier.ts │ │ ├── BezierSpline.ts │ │ ├── Flags.ts │ │ ├── MathHelper.ts │ │ ├── Matrix.ts │ │ ├── Matrix2D.ts │ │ ├── MatrixHelper.ts │ │ ├── Rectangle.ts │ │ ├── SubpixelFloat.ts │ │ ├── SubpixelVector2.ts │ │ └── Vector2.ts │ ├── Physics │ │ ├── ColliderTriggerHelper.ts │ │ ├── Collision.ts │ │ ├── Physics.ts │ │ ├── Ray2D.ts │ │ ├── RaycastHit.ts │ │ ├── Shapes │ │ │ ├── Box.ts │ │ │ ├── Circle.ts │ │ │ ├── CollisionResult.ts │ │ │ ├── Line.ts │ │ │ ├── Polygon.ts │ │ │ ├── Projection.ts │ │ │ ├── RealtimeCollisions.ts │ │ │ ├── Sector.ts │ │ │ ├── Shape.ts │ │ │ └── ShapeCollisions │ │ │ │ ├── ShapeCollisionSector.ts │ │ │ │ ├── ShapeCollisionsBox.ts │ │ │ │ ├── ShapeCollisionsCircle.ts │ │ │ │ ├── ShapeCollisionsLine.ts │ │ │ │ ├── ShapeCollisionsPoint.ts │ │ │ │ └── ShapeCollisionsPolygon.ts │ │ └── SpatialHash.ts │ ├── Tween │ │ ├── AbstractTweenable.ts │ │ ├── Easing │ │ │ ├── EaseType.ts │ │ │ ├── Easing.ts │ │ │ └── Lerps.ts │ │ ├── Interfaces │ │ │ ├── ITween.ts │ │ │ ├── ITweenControl.ts │ │ │ ├── ITweenTarget.ts │ │ │ └── ITweenable.ts │ │ ├── PropertyTweens.ts │ │ ├── TransformSpringTween.ts │ │ ├── TransformVector2Tween.ts │ │ ├── Tween.ts │ │ ├── TweenManager.ts │ │ └── Tweens.ts │ └── Utils │ │ ├── Analysis │ │ └── Stopwatch.ts │ │ ├── AnimCurve.ts │ │ ├── Collections │ │ ├── Bag.ts │ │ ├── HashMap.ts │ │ ├── ImmutableBag.ts │ │ ├── LinkList.ts │ │ ├── ListPool.ts │ │ ├── Pair.ts │ │ ├── PairSet.ts │ │ └── Pool.ts │ │ ├── Coroutines │ │ ├── Coroutine.ts │ │ └── CoroutineManager.ts │ │ ├── DynamticAtlas │ │ └── MaxRectsBinPack.ts │ │ ├── Emitter.ts │ │ ├── Enum.ts │ │ ├── EqualityComparer.ts │ │ ├── Extensions │ │ ├── ArrayUtils.ts │ │ ├── Base64Utils.ts │ │ ├── EdgeExt.ts │ │ ├── NumberExtension.ts │ │ ├── RandomUtils.ts │ │ ├── RectangleExt.ts │ │ ├── TextureUtils.ts │ │ ├── TypeUtils.ts │ │ └── Vector2Ext.ts │ │ ├── GlobalManager.ts │ │ ├── Hash.ts │ │ ├── IComparer.ts │ │ ├── IEqualityComparable.ts │ │ ├── IEqualityComparer.ts │ │ ├── IEquatable.ts │ │ ├── Linq │ │ ├── enumerable.ts │ │ ├── helpers.ts │ │ └── list.ts │ │ ├── Observable.ts │ │ ├── Out.ts │ │ ├── PolygonLight │ │ ├── EndPoint.ts │ │ ├── Segment.ts │ │ └── VisibilityComputer.ts │ │ ├── Ref.ts │ │ ├── Screen.ts │ │ ├── SubpixelNumber.ts │ │ ├── Timers │ │ ├── ITimer.ts │ │ ├── Timer.ts │ │ └── TimerManager.ts │ │ ├── Triangulator.ts │ │ ├── UUID.ts │ │ └── prelog.ts └── tsconfig.json └── sponsor ├── alipay.jpg └── wechatpay.png /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolchains": [], 3 | "backend.maxHeapSizeMb": 896 4 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: esengine # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://github.com/esengine/ecs-framework/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/ecs-framework/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [8.x] 20 | 21 | defaults: 22 | run: 23 | shell: cmd 24 | working-directory: ./source 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: Install Dependencies 32 | run: npm install 33 | - run: npm run build --if-present 34 | - run: gulp build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /source/node_modules 2 | /demo/bin-debug 3 | /demo/bin-release 4 | /.idea 5 | /.vscode 6 | /demo_wxgame 7 | /demo/.wing 8 | /demo/.idea 9 | /demo/.vscode 10 | /source/docs 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ecs-framework 的目标是成为功能强大的框架。它为您构建游戏提供了坚实的基础。它包括的许多功能包括: 2 | 3 | - 完整的场景/实体/组件系统 4 | - SpatialHash是一种空间散列数据结构,用于加速2D物理引擎的碰撞检测,它能够将物体分割为多个小区域并快速查询每个区域内包含的物体,从而大幅度提高碰撞检测的效率。 5 | - AABB,圆和多边形碰撞/触发检测 6 | - 高效的协程,可在多个帧或动画定时中分解大型任务(Core.startCoroutine) 7 | - 通过Astar和广度优先搜索提供寻路支持,以查找图块地图或您自己的自定义格式 ( 参见 https://github.com/esengine/ecs-astar ) 8 | - tween系统。任何number / Vector / 矩形/字段或属性都可以tween。 9 | - 针对核心事件的优化的事件发射器(发射器类),您也可以将其添加到自己的任何类中 10 | - 延迟和重复任务的调度程序(核心调度方法) 11 | 12 | # 快速开始 13 | - [运行框架](docs/getting_start.md) 14 | - [创建实体与组件](docs/create_entity_component.md) 15 | - [创建系统](docs/system.md) 16 | - [全局时间Time](docs/time.md) 17 | - [协程Coroutine](docs/coroutine.md) 18 | - [Physics](docs/physics.md) 19 | - [Emitter](docs/emitter.md) 20 | - [Collision](docs/collision.md) 21 | 22 | ## 交流群 23 | 点击链接加入群聊【ecs游戏框架交流】:https://jq.qq.com/?_wv=1027&k=29w1Nud6 24 | 25 | 26 | ## Scene/Entity/Component 27 | Scene表示游戏场景,是所有实体和组件的容器;Entity表示游戏场景中的实体,是组件的容器;Component表示游戏实体中的组件,包含实体的具体行为逻辑。 28 | 29 | ### Scene 30 | 这是一个ECS(Entity-Component-System)框架的场景类,用于管理游戏中的实体和处理器。它具有以下特点: 31 | 32 | - 维护了一个实体列表和一个实体处理器列表,以便在游戏中轻松添加、更新和删除实体。 33 | - 具有实体、组件和处理器的基本结构,可帮助您组织代码并实现分离的关注点。 34 | - 可以添加和删除场景组件,这是一个特殊类型的组件,可用于实现场景范围的逻辑。 35 | - 通过使用实体处理器,可以轻松地处理实体的更新和渲染。 36 | - 可以搜索场景中的实体、组件和处理器,并根据需要添加或删除它们。 37 | - 可以为实体分配唯一的标识符,以便在处理实体时轻松地跟踪它们。 38 | 39 | ### Entity 40 | Entity 是ECS中的一个基础概念,它代表了游戏中的实体。每个 Entity 都有唯一的 ID 和名称,可以在一个 Scene 中被创建、添加、删除和管理。 41 | 42 | 在一个 Entity 中,可以添加多个 Component 来实现不同的功能。例如,Transform Component 用来表示实体的位置、旋转和缩放等变换属性,其他自定义的 Component 则可以实现各种具体的游戏逻辑。 43 | 44 | 在 Entity 中,可以通过以下方法来管理 Component: 45 | 46 | - createComponent(componentType: new (...args) => T): T 用来创建并返回一个指定类型的 Component 实例。 47 | - addComponent(component: T): T 用来添加一个已有的 Component 实例。 48 | - getComponent(type: new (...args) => T): T 用来获取指定类型的 Component 实例。 49 | - getComponentInScene(type: new (...args) => T): T 用来获取指定类型的 Component 实例,但会在整个场景中搜索。 50 | - tryGetComponent(type: new (...args) => T, outComponent: Ref): boolean 用来尝试获取指定类型的 Component 实例,并将结果存储在传入的 outComponent 参数中。 51 | - hasComponent(type: new (...args) => T): boolean 用来检查 Entity 是否有指定类型的 Component 实例。 52 | - getOrCreateComponent(type: new (...args) => T): T 如果 Entity 没有指定类型的 Component 实例,将会创建一个并返回。 53 | 此外,还可以通过 removeComponent() 方法来移除一个指定的 Component 实例,或者 removeAllComponents() 方法来移除 Entity 中的所有 Component 实例。 54 | 55 | 最后,在 Entity 中还可以通过 Tween 类来实现各种动画效果,例如 tweenPositionTo()、tweenScaleTo() 和 tweenRotationDegreesTo() 等方法。 56 | 57 | 以下是Entity类中一些关键和重要的属性: 58 | 59 | - scene:实体所属的场景对象。 60 | - name:实体的名称。 61 | - id:实体的唯一标识符。 62 | - transform:实体的变换组件。 63 | - components:实体的组件列表。 64 | - updateInterval:实体更新间隔,用于控制实体的更新频率。 65 | - componentBits:用于标记实体拥有哪些组件。 66 | 这些属性是Entity类中非常重要的,它们可以用来操作和控制实体的行为和状态。其中,transform和components属性是实体最为关键的组成部分,前者用于操作实体的位置、旋转和缩放等变换信息,后者用于添加、删除、查询和更新实体的组件信息。通过这些属性,我们可以非常方便地构建出自己的游戏对象,并实现一些基本的功能,如移动、碰撞、动画、音效等。 67 | 68 | ### Component 69 | 这是一个用于构建实体组件系统的基本组件类,所有其他组件都应该从这个抽象类派生。每个组件都可以与一个实体相关联,可以通过实体来访问其它组件。 70 | 71 | 重要属性: 72 | 73 | - id: 组件的唯一标识符。 74 | - entity: 附加此组件的实体。 75 | 重要方法: 76 | 77 | - addComponent(component: T): T:将指定的组件添加到此组件所在的实体中。 78 | - getComponent(type: new (...args: any[]) => T): T:获取指定类型的组件。 79 | - getComponents(typeName: any, componentList?: any[]): any[]:获取指定类型的组件数组。 80 | - hasComponent(type: new (...args: any[]) => Component): boolean:判断实体是否包含指定类型的组件。 81 | - removeComponent(component?: Component): void:从此组件所在的实体中删除指定的组件,如果未指定组件,则删除此组件本身。 82 | 此外,组件还具有一些生命周期方法,例如 initialize()、onAddedToEntity()、onRemovedFromEntity()、onEntityTransformChanged()、onEnabled()、onDisabled() 等,这些方法可以根据需要在派生类中进行重写,以便在实体上添加或移除组件时执行相应的操作。 83 | 84 | 85 | ## Debug 86 | 这是一个静态类 Debug,它包含了一些方法用于打印调试信息。其中包含的方法如下: 87 | 88 | - warnIf(condition: boolean, format: string, ...args: any[]): 如果 condition 为 true,则打印警告信息。 89 | - warn(format: string, ...args: any[]): 打印警告信息。 90 | - error(format: string, ...args: any[]): 打印错误信息。 91 | - log(type: LogType, format: string, ...args: any[]): 打印指定类型的信息,可选的类型有 error、warn、log、info 和 trace。 92 | 该类的优点是它提供了一种统一的调试信息输出方式,可以帮助开发者更方便地输出调试信息,以便在调试时更快地定位问题。缺点是它的功能比较单一,只能输出调试信息,不能对调试信息进行更加复杂的处理。 93 | 94 | ## Flags 95 | 这是一个静态工具类 Flags,提供了一些位标志操作的方法: 96 | 97 | - isFlagSet:检查位标志是否已在数值中设置(该标志未移位) 98 | - isUnshiftedFlagSet:检查位标志是否在数值中设置(该标志已移位) 99 | - setFlagExclusive:设置数值标志位,移除所有已经设置的标志 100 | - setFlag:设置标志位 101 | - unsetFlag:取消标志位 102 | - invertFlags:反转数值集合位 103 | - binaryStringRepresentation:打印 number 的二进制表示,方便调试 number 标志。 104 | 105 | ### 如何参与项目 106 | #### Node.js版本 107 | v10.20.1 108 | #### 操作步骤 109 | 1. 进入source目录 110 | 2. 安装package包: `npm install` 111 | 3. 打包成js: `gulp build` 112 | > 如遇到gulp未找到则先执行 `npm install gulp -g` 113 | 114 | ## 扩展库 115 | 116 | #### [基于ecs-framework开发的astar/BreadthFirst/Dijkstra/GOAP目标导向计划 路径寻找库](https://github.com/esengine/ecs-astar) 117 | #### [基于ecs-framework开发的AI(BehaviourTree、UtilityAI)系统](https://github.com/esengine/BehaviourTree-ai) 118 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /docs/collision.md: -------------------------------------------------------------------------------- 1 | # Collision 2 | 碰撞检测在大多数游戏中都很常见。框架内使用了一些更先进的碰撞/重叠检查方法,如Minkowski、分离轴定理和古老的三角法 3 | 4 | ## 线与线相交 [lineToLine] 5 | - 返回是否相交 6 | ```typescript 7 | const a1 = new es.Vector2(0, 0); 8 | const a2 = new es.Vector2(100, 100); 9 | const b1 = new es.Vector2(-100, 0); 10 | const b2 = new es.Vector2(100, 200); 11 | const result = es.Collisions.lineToLine(a1, a2, b1, b2); 12 | ``` 13 | - 返回是否相交并获得相交的点 14 | ```typescript 15 | const a1 = new es.Vector2(0, 0); 16 | const a2 = new es.Vector2(100, 100); 17 | const b1 = new es.Vector2(-100, 0); 18 | const b2 = new es.Vector2(100, 200); 19 | // 相交的点坐标 20 | const intersection = new es.Vector2(); 21 | const result = es.Collisions.lineToLineIntersection(a1, a2, b1, b2, intersection); 22 | ``` 23 | 24 | ## 圆和圆相交 [circleToCircle] 25 | ```typescript 26 | const center1 = new es.Vector2(0, 0); 27 | const radius1 = 50; 28 | const center2 = new es.Vector2(30, 30); 29 | const radius2 = 50; 30 | const result = es.Collisions.circleToCircle(center1, radius1, center2, radius2); 31 | ``` 32 | 33 | ## 圆和线相交 [circleToLine] 34 | ```typescript 35 | const center1 = new es.Vector2(0, 0); 36 | const radius1 = 50; 37 | const a1 = new es.Vector2(0, 0); 38 | const a2 = new es.Vector2(100, 100); 39 | const result = es.Collisions.circleToLine(center1, radius1, a1, a2); 40 | ``` 41 | 42 | ## 点是否在圆内 [circleToPoint] 43 | ```typescript 44 | const center1 = new es.Vector2(0, 0); 45 | const radius1 = 50; 46 | const point = new es.Vector2(0, 0); 47 | const result = es.Collisions.circleToPoint(center1, radius1, point); 48 | ``` 49 | 50 | ## 圆是否和矩形相交 [rectToCircle] 51 | ```typescript 52 | const rect = new es.Rectangle(0, 0, 100, 100); 53 | const center = new es.Vector2(30, 30); 54 | const radius = 50; 55 | const result = es.Collisions.rectToCircle(rect, center, radius); 56 | ``` 57 | 58 | ## 矩形与线是否相交 [rectToLine] 59 | ```typescript 60 | const rect = new es.Rectangle(0, 0, 100, 100); 61 | const a1 = new es.Vector2(0, 0); 62 | const a2 = new es.Vector2(100, 100); 63 | const result = es.Collisions.rectToLine(rect, a1, a2); 64 | ``` 65 | 66 | ## 点是否在矩形内 [rectToPoint] 67 | ```typescript 68 | const point = new es.Vector2(100, 100); 69 | const result = es.Collisions.rectToPoint(0, 0, 100, 100, point); 70 | ``` -------------------------------------------------------------------------------- /docs/coroutine.md: -------------------------------------------------------------------------------- 1 | # Coroutine 2 | ## 协程介绍 3 | 框架的协程系统是基于js的一个简单而强大的迭代器。这一点你不必关注太多,我们直接进入一个简单的例子来看看协程到底能干什么。首先,我们来看一下这段简单的代码 4 | 5 | ### 倒计时器 6 | 这是一个简单的脚本组件,只做了倒计时,并且在到达0的时候log一个信息。 7 | ```typescript 8 | export class AComponent extends es.Component implements es.IUpdatable { 9 | public timer = 3; 10 | update() { 11 | this.timer -= es.Time.deltaTime; 12 | if(this.timer <= 0) { 13 | console.Log("Timer has finished!"); 14 | } 15 | } 16 | } 17 | ``` 18 | 19 | 还不错,代码简短实用,但问题是,如果我们需要复杂的脚本组件(像一个角色或者敌人的类),拥有多个计时器呢?刚开始的时候,我们的代码也许会是这样的: 20 | 21 | ```typescript 22 | export class AComponent extends es.Component implements es.IUpdatable 23 | { 24 | public firstTimer = 3; 25 | public secondTimer = 2; 26 | public thirdTimer = 1; 27 | update() { 28 | this.firstTimer -= es.Time.deltaTime; 29 | if(this.firstTimer <= 0) 30 | console.Log("First timer has finished!"); 31 | this.secondTimer -= es.Time.deltaTime; 32 | if(this.secondTimer <= 0) 33 | console.Log("Second timer has finished!"); 34 | this.thirdTimer -= es.Time.deltaTime; 35 | if(this.thirdTimer <= 0) 36 | console.Log("Third timer has finished!"); 37 | } 38 | } 39 | 40 | ``` 41 | 42 | 尽管不是太糟糕,但是我个人不是很喜欢自己的代码中充斥着这些计时器变量,它们看上去很乱,而且当我需要重新开始计时的时候还得记得去重置它们(这活我经常忘记做)。 43 | 44 | 45 | 46 | 如果我只用一个for循环来做这些,看上去是否会好很多? 47 | 48 | ```typescript 49 | for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime) { 50 | //Just do nothing... 51 | } 52 | console.Log("This happens after 5 seconds!"); 53 | ``` 54 | 55 | 现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。 56 | 57 | 58 | 59 | 好的,你可能现在明白我的意思:协程可以做的正是这一点! 60 | 61 | ## 码入你的协程! 62 | 63 | 现在,这里提供了上面例子运用协程的版本!我建议你从这里开始跟着我来写一个简单的脚本组件,这样你可以在你自己的程序中看到它是如何工作的。 64 | 65 | ```typescript 66 | export class AComponent extends es.Component implements es.IUpdatable 67 | { 68 | onAddedToEntity() { 69 | es.Core.startCoroutine(this.countdown()); 70 | } 71 | 72 | *countdown() { 73 | for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime) 74 | yield null; 75 | console.Log("This message appears after 3 seconds!"); 76 | } 77 | } 78 | 79 | ``` 80 | 81 | 这看上去有点不一样,没关系,接下来我会解释这里到底发生了什么。 82 | 83 | ```typescript 84 | es.Core.startCoroutine(this.countdown()); 85 | ``` 86 | 87 | 这一行用来开始我们的countdown程序,注意,我并没有给它传入参数,但是这个方法调用了它自己(这是通过传递countdown的yield返回值来实现的)。 88 | 89 | ### Yield 90 | 91 | 为了能在连续的多帧中(在这个例子中,3秒钟等同于很多帧)调用该方法,框架必须通过某种方式来存储这个方法的状态,这是通过迭代器中使用yield语句得到的返回值,当你`yield`一个方法时,你相当于说了,**现在停止这个方法,然后在下一帧中从这里重新开始!**。 92 | 93 | > 注意:用0或者null来yield的意思是告诉协程等待下一帧,直到继续执行为止。当然,同样的你可以继续yield其他协程,我会在下一个教程中讲到这些。 94 | 95 | ## 一些例子 96 | 97 | 协程在刚开始接触的时候是非常难以理解的,无论是新手还是经验丰富的程序员我都见过他们对于协程语句一筹莫展的时候。因此我认为通过例子来理解它是最好的方法,这里有一些简单的协程例子: 98 | 99 | ### 多次输出Hello 100 | 101 | 记住,yield是 **停止执行方法,并且在下一帧从这里重新开始**,这意味着你可以这样做: 102 | 103 | ```typescript 104 | //这将打招呼 5 次,每帧一次,持续 5 帧 105 | *sayHelloFiveTimes() { 106 | yield null; 107 | console.Log("Hello"); 108 | yield null; 109 | console.Log("Hello"); 110 | yield null; 111 | console.Log("Hello"); 112 | yield null; 113 | console.Log("Hello"); 114 | yield null; 115 | console.Log("Hello"); 116 | } 117 | //这将做与上述功能完全相同的事情! 118 | *sayHello5Times() { 119 | for(let i = 0; i < 5; i++) { 120 | console.Log("Hello"); 121 | yield null; 122 | } 123 | } 124 | ``` 125 | 126 | ### 每一帧输出“Hello”,无限循环。。。 127 | 128 | 通过在一个while循环中使用yield,你可以得到一个无限循环的协程,这几乎就跟一个update()循环等同 129 | 130 | ```typescript 131 | //一旦启动,这将一直运行直到手动停止 132 | *sayHelloEveryFrame(){ 133 | while(true) { 134 | console.Log("Hello"); 135 | yield null; 136 | } 137 | } 138 | ``` 139 | 140 | ### 计时 141 | 不过跟update()不一样的是,你可以在协程中做一些更有趣的事 142 | 143 | ```typescript 144 | *countSeconds(){ 145 | let seconds = 0; 146 | while(true) 147 | { 148 | // 1秒后执行下一帧 149 | yield 1; 150 | seconds++; 151 | console.Log("自协程启动以来已经过去了" + seconds + "秒钟."); 152 | } 153 | } 154 | ``` 155 | 156 | 这个方法突出了协程一个非常酷的地方:方法的状态被存储了,这使得方法中定义的这些变量都会保存它们的值,即使是在不同的帧中。还记得这个教程开始时那些烦人的计时器变量吗?通过协程,我们再也不需要担心它们了,只需要把变量直接放到方法里面! 157 | 158 | ### 开始和终止协程 159 | 160 | 之前,我们已经学过了通过 es.Core.startCoroutine()方法来开始一个协程,就像这样: 161 | 162 | ```typescript 163 | const coroutine = es.Core.startCoroutine(this.countdown()); 164 | ``` 165 | 166 | 我们可以像这样停止协程 167 | 168 | ```typescript 169 | coroutine.stop(); 170 | ``` 171 | 172 | 或者你可以再迭代器内返回`yield "break"`方式中止协程 173 | 174 | ```typescript 175 | *countSeconds(){ 176 | let seconds = 0; 177 | while(true) 178 | { 179 | for(let timer = 0; timer < 1; timer += es.Time.deltaTime) 180 | yield null; 181 | seconds++; 182 | console.Log("自协程启动以来已经过去了" + seconds + "秒钟."); 183 | 184 | // 如果大于10秒,终止协程 185 | if (second > 10) 186 | yield "break"; 187 | } 188 | } 189 | ``` -------------------------------------------------------------------------------- /docs/create_entity_component.md: -------------------------------------------------------------------------------- 1 | # 创建实体 2 | 3 | 实体必须依赖于场景,不能单独存在。创建实体方法由场景提供。 4 | 5 | ## 方式一 6 | ```typescript 7 | // 通过全局快捷获取场景创建实体 8 | const playerEntity = es.Core.scene.createEntity("player"); 9 | ``` 10 | 11 | ## 方式二 12 | ```typescript 13 | export class MainScene extends es.Scene { 14 | onStart() { 15 | // 通过场景内创建 16 | const playerEntity = this.createEntity("player"); 17 | } 18 | } 19 | ``` 20 | 21 | ### Transform 22 | 23 | 框架中提供的实体不同于其他框架实体,它更偏向于游戏使用,实体内含有`Transform`属性。可用于快速访问位置,旋转,缩放等。如果需要应用于游戏引擎,请再组件重写`onTransformChanged`监听这些属性的变化。 24 | 25 | > 实体内包含对transform里位置、旋转、缩放的快捷tween方法。`tweenPositionTo`/`tweenLocalPositionTo`/`tweenScaleTo`/`tweenLocalScaleTo`/`tweenRotationDegreesTo`/`tweenLocalRotationDegreesTo` 26 | 27 | ### tag / setTag 28 | 29 | 实体还提供`tag`属性及`setTag`方法来快速设置实体的标记,可再场景中使用`findEntitiesWithTag`快速查询拥有该标记的实体或使用`findEntityWithTag`来查找第一个拥有该标记的实体,你可以把它当作组来使用。 30 | 31 | ### detachFromScene / attachToScene 32 | 当你不想实体与场景被销毁时一同被销毁。可先 `detachFromScene`,等待合适的时机再调用 `attachToScene` 放入新的场景。 33 | 34 | 35 | # 创建组件 36 | 37 | 组件一般配合实体使用。组件需要继承 `es.Component` 来标识为组件,如果想让组件拥有每帧更新能力则额外继承`es.IUpdatable` 接口。在实现的`update`方法当中进行更新逻辑。 38 | 39 | ```typescript 40 | // es.IUpdatable接口为可选接口,如果不需要更新能力则不必继承 41 | export class AComponent extends es.Component implements es.IUpdatable { 42 | update() { 43 | // 更新逻辑 44 | } 45 | } 46 | ``` 47 | 48 | ## 加入组件 49 | 50 | 组件必须挂载于实体上,不能单独存在,如果需要单独于场景的组件则参考 [es.SceneComponent](scene_component.md) 组件。 51 | 52 | - 方式一:将现有的AComponent加入实体 53 | ```typescript 54 | const aCom = playerEntity.addComponent(new AComponent()); 55 | ``` 56 | 57 | - 方式二:在实体上直接创建组件 58 | ```typescript 59 | const aCom = playerEntity.createComponent(AComponent); 60 | ``` 61 | 62 | ## 获取组件 63 | 64 | - 方式一: 根据类型获取找到满足条件的第一个组件 65 | ```typescript 66 | // 不能保证已经加入场景 67 | const aCom = playerEntity.getComponent(AComponent); 68 | ``` 69 | 70 | ```typescript 71 | // 保证已经加入场景 72 | const aCom = playerEntity.getComponentInScene(AComponent); 73 | ``` 74 | 75 | - 方式二: 尝试找到一个组件,返回是否找到组件标志,第二参数需要一个引用组件用于存储已找到的组件 76 | ```typescript 77 | const outCom = new Ref(); 78 | const find = playerEntity.tryGetComponent(AComponent, outCom); 79 | if (find) { 80 | const aCom = outCom.value; 81 | } 82 | ``` 83 | 84 | - 方式三:获取该类型的组件,如果未找到则创建一个并返回 85 | ```typescript 86 | const aCom = playerEntity.getOrCreateComponent(AComponent); 87 | ``` 88 | 89 | - 方式四:根据第二参数中的列表找到该类型的所有组件并返回 90 | ```typescript 91 | const findArray: Component[] = [ 92 | new AComponent(), 93 | new BComponent(), 94 | new CComponent() 95 | ]; 96 | // findArray可不传,则在实体上寻找满足第一个条件的所有组件 97 | const coms = playerEntity.getComponents(AComponent, findArray); 98 | ``` 99 | 100 | - 组件是否存在 101 | 102 | ```typescript 103 | const find = playerEntity.hasComponent(AComponent); 104 | ``` 105 | 106 | ## 移除组件 107 | 108 | - 方式一: 移除已实例组件 109 | ```typescript 110 | playerEntity.removeComponent(aCom); 111 | ``` 112 | 113 | - 方式二:移除满足类型的第一个组件 114 | ```typescript 115 | playerEntity.removeComponentForType(AComponent); 116 | ``` 117 | 118 | - 方式三: 移除所有组件 119 | ```typescript 120 | playerEntity.removeAllComponents(); 121 | ``` -------------------------------------------------------------------------------- /docs/emitter.md: -------------------------------------------------------------------------------- 1 | # Emitter 2 | Core提供了一个在某些关键时刻触发事件的发射器。 通过Core.emitter.addObserver和Core.emitter.removeObserver进行访问。 CoreEvents枚举定义了所有可用事件。 3 | 4 | 发射器类也可以在自己的类中使用。 您可以通过number,enum或任何结构键输入事件。 5 | 6 | ## 自定义事件发生器 7 | 8 | - string为key的事件发生器 9 | ```typescript 10 | export enum CustomEvent { 11 | enum1, 12 | enum2 13 | } 14 | 15 | export class MainScene extends es.Scene { 16 | // string为key的事件发生器 17 | private str_emitter = new es.Emitter(); 18 | // number为key的事件发生器 19 | private num_emitter = new es.Emitter(); 20 | // enum为key的事件发生器 21 | private custom_emitter = new es.Emitter(); 22 | 23 | onStart() { 24 | // 监听触发器 25 | this.str_emitter.addObserver("test", this.onStrEmit, this); 26 | 27 | // 触发监听器 28 | this.str_emitter.emit("test"); 29 | 30 | // 移除事件触发器 31 | this.str_emitter.removeObserver("test", this.onStrEmit); 32 | } 33 | 34 | // args为emit传入的参数。不传则为空 35 | onStrEmit(...args: any[]) { 36 | console.log("test"); 37 | } 38 | } 39 | ``` -------------------------------------------------------------------------------- /docs/getting_start.md: -------------------------------------------------------------------------------- 1 | # 如何开始 2 | 3 | ## 初始化框架 4 | 5 | ```typescript 6 | // 创建调试模式下的`Core`实例 7 | const core = es.Core.create(); 8 | 9 | // 创建非调试模式下的`Core`实例 10 | const core = es.Core.create(false); 11 | ``` 12 | 13 | ## 分发帧事件 14 | 15 | ```typescript 16 | // dt 是一个可选参数,如果传入了 es.Time.deltaTime或者不传入参数,则代表使用框架的内置的时间差来更新游戏状态; 17 | // 如果传入了游戏引擎自带的 deltaTime,则代表使用该值来更新游戏状态。 18 | // 在 es.Core.update 方法中,会根据 dt 的值来计算时间戳信息,并更新全局管理器和当前场景的状态 19 | es.Core.emitter.emit(es.CoreEvents.frameUpdated, dt); 20 | ``` 21 | 22 | > 尽可能使用引擎的dt,以免再游戏暂停继续时由于dt导致的跳帧问题 23 | 24 | > **您还需要一个默认的场景以使得游戏可以进行使用ecs框架以及物理类或tween系统** 25 | 26 | ## 创建场景类 27 | 28 | 场景类需要继承框架中的 `es.Scene` 29 | 30 | ```typescript 31 | /** 示例场景 */ 32 | export class MainScene extends Scene { 33 | constructor() { 34 | super(); 35 | } 36 | 37 | /** 38 | * 初始化场景,添加实体和组件 39 | * 40 | * 这个方法会在场景被创建时被调用。我们在这个方法中创建了一个实体, 41 | * 并向它添加了一个SpriteRender组件和一个TransformMove组件。 42 | */ 43 | public initialize() { 44 | // 创建一个实体 45 | let entity = this.createEntity("Player"); 46 | 47 | // 添加一个SpriteRender组件,用于显示实体的图像 48 | let spriteRender = entity.addComponent(new SpriteRender()); 49 | spriteRender.sprite = new es.Sprite(new es.Texture("player.png")); 50 | 51 | // 添加一个TransformMove组件,用于移动实体 52 | let transformMove = entity.addComponent(new TransformMove()); 53 | transformMove.speed = 50; 54 | } 55 | 56 | /** 57 | * 场景开始运行时执行的操作 58 | * 59 | * 这个方法会在场景开始运行时被调用。我们在这个方法中输出一条消息表示场景已经开始运行。 60 | */ 61 | public onStart() { 62 | console.log("MainScene has started!"); 63 | } 64 | 65 | /** 66 | * 场景被销毁时执行的操作 67 | * 68 | * 这个方法会在场景被销毁时被调用。我们在这个方法中输出一条消息表示场景已经被卸载。 69 | */ 70 | public unload() { 71 | console.log("MainScene has been unloaded!"); 72 | } 73 | } 74 | ``` 75 | 76 | 要想激活该场景需要通过核心类 `Core` 来设置当前 `MainScene` 为使用的场景 77 | 78 | ```typescript 79 | es.Core.scene = new MainScene(); 80 | ``` -------------------------------------------------------------------------------- /docs/physics.md: -------------------------------------------------------------------------------- 1 | ## 关于 Physics/Collision 2 | 框架中的物理不是一个真实的物理模拟。它只提供了游戏物理。您可以执行一些操作,如检测碰撞器、重叠检查、碰撞检查、扫描测试等。不是一个完整的刚体模拟。 3 | 4 | ### Colliders 物理系统的根本 5 | 没有Collider,在物理系统中什么也不会发生。 碰撞器存在于实体类中,有几种类型:BoxCollider,CircleCollider和PolygonCollider。 您可以像这样添加Collider:`entity.addComponent(new BoxCollider())`. 将碰撞器添加到Entity时,它们会自动添加到SpatialHash中。 6 | 7 | ### SpatialHash:你永远不会用到它,但它仍然对你很重要 8 | SpatialHash类是一个隐藏类,该类为您的游戏全局管理 `collider`。静态物理类是SpatialHash的公共包装器。 SpatialHash没有设置大小限制,用于快速进行碰撞/线投射/重叠检查。例如,如果你有一个英雄在世界各地移动,而不必检查每个对撞机(可能是数百个)是否发生碰撞,则只需向SpatialHash询问英雄附近的所有collider即可。这大大缩小了您的碰撞检查范围。 9 | 10 | SpatialHash有一个可配置的方面,它可以极大地影响其性能:单元大小。 SpatialHash将空间分成一个网格,选择适当的网格大小可以将可能发生的碰撞查询减到最少。默认情况下,网格大小为100像素。您可以通过在创建场景之前设置`Physics.SpatialHashCellSize`来更改此设置。选择比您的平均玩家/敌人人数稍大的尺寸通常效果最佳。 11 | 12 | ### Physics 类 13 | 物理类是物理的核心类。 您可以设置一些属性,例如前面提到的spatialHashCellSize,raycastsHitTriggers和raycastsStartInColliders。 14 | - linecast:从开始到结束投射一条线,并返回与layerMask相匹配的碰撞器的第一次命中 15 | - overlapRectangle:检查是否有任何collider在矩形区域内 16 | - overlapCircle:检查是否有任何collider在圆形区域内 17 | - boxcastBroadphase:返回边界与collider.bounds相交的所有碰撞器。 请注意,这是一个broadphase检查,因此它只检查边界,不执行单个碰撞器到碰撞器的检查! 18 | 19 | 会注意到上面提到的layerMask。 layerMask允许您确定与哪些碰撞器碰撞。 每个collider都可以设置其物理层,以便在查询物理系统时可以选择仅检索与传入的layerMask匹配的对撞机。 所有物理方法都接受默认为所有图层的图层蒙版参数。 使用此选项可以过滤冲突检查,并通过不执行不必要的冲突检查来使性能保持最佳状态。 20 | 21 | ### 使用物理系统 22 | 射线检测对于检查敌人的视线、探测实体的空间环境、快速移动的子弹等各种事情都非常有用。下面是一个从头到尾投射线条的示例,如果击中某个物体,它只会记录数据: 23 | ```ts 24 | const hit = es.Physics.linecast( start, end ); 25 | if( hit.collider != null ) 26 | console.log( `ray hit ${hit}, entity: {hit.collider.entity}`); 27 | ``` 28 | 29 | 我们使用了一些更先进的碰撞/重叠检查方法,如Minkowski和、分离轴定理和古老的三角法。这些都被包装在Collider类上的简单易用的方法中。让我们看一些例子。 30 | 31 | 第一个例子是处理碰撞的最简单方法。deltaMovement是您希望移动实体的量,通常是velocity*Time.deltaTime. collidesWithAny方法将检查所有碰撞并调整deltaMovement以解决任何碰撞。 32 | 33 | ```ts 34 | // 碰撞结果将包含一些非常有用的信息,例如被撞的collider,表面命中的法线和最小平移矢量(MTV)。 MTV可用于将碰撞实体直接移动到命中的碰撞器附近。 35 | let collisionResult = null; 36 | 37 | // 进行检查以查看entity.getComponent(Collider)(实体上的第一个碰撞器)是否与场景中的任何其他碰撞器发生碰撞。请注意,如果您有多个碰撞器,则可以获取并遍历它们,而不是仅检查第一个碰撞器。 38 | if( entity.getComponent(es.Collider).collidesWithAny( deltaMovement, collisionResult ) ) 39 | { 40 | // 记录CollisionResult。 您可能需要使用它来添加一些粒子效果或与您的游戏相关的任何其他内容。 41 | console.log( `collision result: ${collisionResult}` ); 42 | } 43 | 44 | // 将实体移到新位置。 已经调整了deltaMovement为我们解决冲突。 45 | entity.position = entity.position.add(deltaMovement); 46 | ``` 47 | 48 | 如果您需要对碰撞发生时的情况进行更多控制,则也可以手动检查是否与其他collider发生碰撞。 请注意,执行此操作时,deltaMovement不会为您调整。 解决冲突时,您需要考虑最小平移矢量。 49 | 50 | ```ts 51 | let collisionResult = null; 52 | 53 | // 进行检查以查看entity.getComponent是否与一些其他Collider发生碰撞 54 | if( entity.getComponent(es.Collider).collidesWith( someOtherCollider, deltaMovement, collisionResult ) ) 55 | { 56 | // 将实体移动到与命中Collider相邻的位置,然后记录CollisionResult 57 | entity.position = entity.position.add(deltaMovement.sub(collisionResult.minimumTranslationVector)); 58 | console.log( `collision result: ${collisionResult}` ); 59 | } 60 | ``` 61 | 我们可以使用前面提到的Physics.boxcastBroadphase方法或更具体地讲,将自身排除在查询之外的版本,使上述示例更进一步。 该方法将为我们提供场景中所有在我们附近的collider,然后我们可以使用这些对撞机进行实际的碰撞检查。 62 | 63 | ```ts 64 | // 在我们自身以外的位置获取可能与之重叠的任何东西 65 | let neighborColliders = es.Physics.boxcastBroadphaseExcludingSelf( entity.getComponent(es.Collider) ); 66 | 67 | // 遍历并检查每个对撞机是否重叠 68 | for( let collider of neighborColliders ) 69 | { 70 | if( entity.getComponent(es.Collider).overlaps( collider ) ) 71 | console.log( `我们正在重叠一个collider : ${collider}` ); 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/render.md: -------------------------------------------------------------------------------- 1 | ## 渲染框架 2 | 为了方便快速与引擎对接使用,框架内置了一套渲染框架,它仅仅只有接口,你需要实现框架所需要的内容才可以进行渲染。 -------------------------------------------------------------------------------- /docs/scene_component.md: -------------------------------------------------------------------------------- 1 | # scene_component 2 | 这是一个场景组件的基类,如果您需要一个不在实体上的组件则继承它 `es.SceneComponent`。场景组件默认包含`update`/`onEnabled`/`onDisabled`/`onRemovedFromScene`,你可以对他们进行重载。 3 | 4 | ```typescript 5 | export class ASceneComponent extends es.SceneComponent { 6 | /** 7 | * 在启用此SceneComponent时调用 8 | */ 9 | onEnabled() { 10 | 11 | } 12 | 13 | /** 14 | * 当禁用此SceneComponent时调用 15 | */ 16 | onDisabled() { 17 | 18 | } 19 | 20 | /** 21 | * 当该SceneComponent从场景中移除时调用 22 | */ 23 | onRemovedFromScene() { 24 | 25 | } 26 | 27 | update() { 28 | 29 | } 30 | } 31 | ``` 32 | 33 | - 场景组件需要添加至场景上, 通过场景中的 `addSceneComponent` 方法加入。 34 | 35 | ```typescript 36 | export class MainScene extends es.Scene { 37 | onStart() { 38 | const aSceneCom = this.addSceneComponent(new ASceneComponent()); 39 | } 40 | } 41 | ``` 42 | 43 | - 如果想要获取该场景组件则通过`getSceneComponent`方法获取 44 | 45 | ```typescript 46 | export class MainScene extends es.Scene { 47 | onStart() { 48 | const aSceneCom = this.getSceneComponent(ASceneComponent); 49 | } 50 | } 51 | ``` 52 | 53 | - 如果获取时发现没有可以自动创建则通过 `getOrCreateSceneComponent` 方法 54 | 55 | ```typescript 56 | export class MainScene extends es.Scene { 57 | onStart() { 58 | const aSceneCom = this.getOrCreateSceneComponent(ASceneComponent); 59 | } 60 | } 61 | ``` 62 | 63 | - 删除场景组件 64 | 65 | ```typescript 66 | export class MainScene extends es.Scene { 67 | onStart() { 68 | this.removeSceneComponent(aSceneCom); 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /docs/system.md: -------------------------------------------------------------------------------- 1 | # system 2 | 系统是ecs的核心。你的游戏逻辑应该在这里进行处理,所有的实体及组件也会在这里进行集中处理。用于处理实体的系统叫做 `es.EntityProcessingSystem`。 你需要继承他并实现`processEntity(entity: Entity)`方法。 3 | 4 | ```typescript 5 | export class ASystem extends es.EntityProcessingSystem { 6 | processEntity(entity: Entity){ 7 | 8 | } 9 | } 10 | ``` 11 | 12 | 系统也依赖于场景,如果想要系统被激活则需要使用场景中`addEntityProcessor`方法。系统被实例化需要传入一个`es.Matcher` 参数。 13 | 14 | ```typescript 15 | export class MainScene extends es.Scene { 16 | onStart() { 17 | this.addEntityProcessor(new ASystem(es.Matcher.empty().all(AComponent))); 18 | } 19 | } 20 | ``` 21 | 22 | ## Matcher 23 | matcher是系统的匹配器,用于匹配满足条件的实体传入系统进行处理。如果想要一个空的匹配器则直接 `es.Matcher.empty()` 24 | 25 | - all 26 | 同时拥有多个组件 27 | 28 | ```typescript 29 | es.Matcher.empty().all(AComponent, BComponent); 30 | ``` 31 | 32 | - one 33 | 拥有任意一个组件 34 | 35 | ```typescript 36 | es.Matcher.empty().one(AComponent, BComponent); 37 | ``` 38 | 39 | - exclude 40 | 拥有某些组件,并且不包含某些组件 41 | ```typescript 42 | // 不包含CComponent或者DComponent 43 | es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent, DComponent); 44 | 45 | // 不同时包含CComponent和DComponent 46 | es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent).exclude(DComponent); 47 | ``` 48 | 49 | ## 获取系统 50 | 51 | ```typescript 52 | export class MainScene extends es.Scene { 53 | onStart() { 54 | const aSystem = this.getEntityProcessor(ASystem); 55 | } 56 | } 57 | ``` 58 | 59 | ## 移除系统 60 | 61 | ```typescript 62 | export class MainScene extends es.Scene { 63 | onStart() { 64 | this.removeEntityProcessor(aSystem); 65 | } 66 | } 67 | ``` -------------------------------------------------------------------------------- /docs/time.md: -------------------------------------------------------------------------------- 1 | # Time 2 | 游戏中会经常使用到关于时间类。框架内提供了关于时间的多个属性 3 | 4 | ## 游戏运行的总时间 5 | `es.Time.totalTime` 6 | 7 | ## deltaTime的未缩放版本。不受时间尺度的影响 8 | `es.Time.unscaledDeltaTime` 9 | 10 | ## 前一帧到当前帧的时间增量(按时间刻度进行缩放) 11 | `es.Time.deltaTime` 12 | 13 | ## 时间刻度缩放 14 | `es.Time.timeScale` 15 | 16 | ## DeltaTime可以为的最大值 17 | `es.Time.maxDeltaTime` 默认为Number.MAX_VALUE 18 | 19 | ## 已传递的帧总数 20 | `es.Time.frameCount` 21 | 22 | ## 自场景加载以来的总时间 23 | `es.Time.timeSinceSceneLoad` -------------------------------------------------------------------------------- /source/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /source/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /source/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/.idea/source.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /source/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /source/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "gulp", 8 | "task": "build", 9 | "group": "build", 10 | "problemMatcher": [] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /source/.wing/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /source/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const gulp = require('gulp'); 3 | const { series, parallel } = require('gulp'); 4 | const minify = require('gulp-minify'); 5 | const inject = require('gulp-inject-string'); 6 | const ts = require('gulp-typescript'); 7 | const merge = require('merge2'); 8 | const tsProject = ts.createProject('tsconfig.json'); 9 | 10 | function buildJs() { 11 | return tsProject.src() 12 | .pipe(tsProject()) 13 | .js.pipe(inject.replace('var es;', '')) 14 | .pipe(inject.prepend('window.es = {};\n')) 15 | .pipe(inject.replace('var __extends =', 'window.__extends =')) 16 | .pipe(minify({ ext: { min: ".min.js" } })) 17 | .pipe(gulp.dest('./bin')); 18 | } 19 | 20 | function buildDts() { 21 | return tsProject.src() 22 | .pipe(tsProject()) 23 | // .dts.pipe(inject.append('import e = framework;')) 24 | .pipe(gulp.dest('./bin')); 25 | } 26 | 27 | exports.buildJs = buildJs; 28 | exports.buildDts = buildDts; 29 | exports.build = series(buildJs, buildDts); -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@esengine/egret-framework", 3 | "version": "1.0.1", 4 | "description": "用于egret 包含众多高性能方法以供使用", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "mocha --recursive --reporter tap --growl", 11 | "eslint": "eslint src --ext .ts" 12 | }, 13 | "author": "yhh", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/core": "^7.21.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "browserify": "^14.3.0", 19 | "gulp": "^4.0.2", 20 | "gulp-babel": "^8.0.0", 21 | "gulp-concat": "^2.6.1", 22 | "gulp-inject-string": "^1.1.2", 23 | "gulp-minify": "^3.1.0", 24 | "gulp-string-replace": "^1.1.2", 25 | "gulp-typescript": "^5.0.1", 26 | "gulp-uglify": "^3.0.2", 27 | "merge2": "^1.4.1", 28 | "tsify": "^3.0.1", 29 | "typedoc": "^0.23.26", 30 | "typescript": "^2.2.2", 31 | "vinyl-source-stream": "^1.1.0", 32 | "watchify": "^4.0.0" 33 | }, 34 | "publishConfig": { 35 | "registry": "https://npm.pkg.github.com/359807859@qq.com" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/esengine/ecs-framework.git" 40 | }, 41 | "dependencies": {} 42 | } 43 | -------------------------------------------------------------------------------- /source/src/Debug/Console/DebugConsole.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class DebugConsole { 3 | public static Instance: DebugConsole; 4 | } 5 | } -------------------------------------------------------------------------------- /source/src/Debug/Debug.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export enum LogType { 3 | error, 4 | warn, 5 | log, 6 | info, 7 | trace 8 | } 9 | 10 | export class Debug { 11 | /** 12 | * 如果条件为true,则在控制台中以警告方式打印消息。 13 | * @param condition 是否应该打印消息的条件 14 | * @param format 要打印的消息格式 15 | * @param args 与消息格式相对应的参数列表 16 | */ 17 | public static warnIf(condition: boolean, format: string, ...args: any[]) { 18 | if (condition) 19 | this.log(LogType.warn, format, args); 20 | } 21 | 22 | /** 23 | * 在控制台中以警告方式打印消息。 24 | * @param format 要打印的消息格式 25 | * @param args 与消息格式相对应的参数列表 26 | */ 27 | public static warn(format: string, ...args: any[]) { 28 | this.log(LogType.warn, format, args); 29 | } 30 | 31 | /** 32 | * 在控制台中以错误方式打印消息。 33 | * @param format 要打印的消息格式 34 | * @param args 与消息格式相对应的参数列表 35 | */ 36 | public static error(format: string, ...args: any[]) { 37 | this.log(LogType.error, format, args); 38 | } 39 | 40 | /** 41 | * 在控制台中以标准日志方式打印消息。 42 | * @param type 要打印的日志类型 43 | * @param format 要打印的消息格式 44 | * @param args 与消息格式相对应的参数列表 45 | */ 46 | public static log(type: LogType, format: string, ...args: any[]) { 47 | switch (type) { 48 | case LogType.error: 49 | console.error(`${type}: ${StringUtils.format(format, args)}`); 50 | break; 51 | case LogType.warn: 52 | console.warn(`${type}: ${StringUtils.format(format, args)}`); 53 | break; 54 | case LogType.log: 55 | console.log(`${type}: ${StringUtils.format(format, args)}`); 56 | break; 57 | case LogType.info: 58 | console.info(`${type}: ${StringUtils.format(format, args)}`); 59 | break; 60 | case LogType.trace: 61 | console.trace(`${type}: ${StringUtils.format(format, args)}`); 62 | break; 63 | default: 64 | throw new Error('argument out of range'); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /source/src/Debug/DebugDefaults.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 我们在这里存储了各种系统的默认颜色,如对撞机调试渲染、Debug.drawText等。 4 | * 命名方式尽可能采用CLASS-THING,以明确它的使用位置 5 | */ 6 | export class DebugDefault { 7 | public static debugText: number = 0xffffff; 8 | 9 | public static colliderBounds: number = 0xffffff * 0.3; 10 | public static colliderEdge: number = 0x8B0000; 11 | public static colliderPosition: number = 0xFFFF00; 12 | public static colliderCenter: number = 0xFF0000; 13 | 14 | public static renderableBounds: number = 0xFFFF00; 15 | public static renderableCenter: number = 0x9932CC; 16 | 17 | public static verletParticle: number = 0xDC345E; 18 | public static verletConstraintEdge: number = 0x433E36; 19 | } 20 | } -------------------------------------------------------------------------------- /source/src/Debug/Insist.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Insist { 3 | public static fail(message: string = null, ...args: any[]) { 4 | if (!console.assert) 5 | return; 6 | 7 | if (message == null) { 8 | console.assert(false); 9 | } else { 10 | console.assert(false, StringUtils.format(message, args)); 11 | } 12 | 13 | } 14 | 15 | public static isTrue(condition: boolean, message: string = null, ...args: any[]) { 16 | if (!condition) { 17 | if (message == null) { 18 | this.fail(); 19 | } else { 20 | this.fail(message, args); 21 | } 22 | } 23 | } 24 | 25 | public static isFalse(condition: boolean, message: string = null, ...args: any[]) { 26 | if (message == null) { 27 | this.isTrue(!condition); 28 | } else { 29 | this.isTrue(!condition, message, args); 30 | } 31 | } 32 | 33 | public static isNull(obj, message: string = null, ...args: any[]) { 34 | if (message == null) { 35 | this.isTrue(obj == null); 36 | } else { 37 | this.isTrue(obj == null, message, args); 38 | } 39 | } 40 | 41 | public static isNotNull(obj, message: string = null, ...args: any[]) { 42 | if (message == null) { 43 | this.isTrue(obj != null); 44 | } else { 45 | this.isTrue(obj != null, message, args); 46 | } 47 | } 48 | 49 | public static areEqual(first, second, message: string, ...args: any[]) { 50 | if (first != second) 51 | this.fail(message, args); 52 | } 53 | 54 | public static areNotEqual(first, second, message: string, ...args: any[]) { 55 | if (first == second) 56 | this.fail(message, args); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /source/src/ECS/Component.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 执行顺序 4 | * - onAddedToEntity 5 | * - OnEnabled 6 | * 7 | * 删除执行顺序 8 | * - onRemovedFromEntity 9 | */ 10 | export abstract class Component { 11 | public static _idGenerator: number = 0; 12 | /** 13 | * 此组件的唯一标识 14 | */ 15 | public readonly id: number; 16 | /** 17 | * 此组件附加的实体 18 | */ 19 | public entity: Entity; 20 | 21 | constructor() { 22 | this.id = Component._idGenerator++; 23 | } 24 | 25 | /** 26 | * 快速访问 this.entity.transform 27 | */ 28 | public get transform(): Transform { 29 | return this.entity.transform; 30 | } 31 | 32 | private _enabled: boolean = true; 33 | 34 | /** 35 | * 如果组件和实体都已启用,则为。当启用该组件时,将调用该组件的生命周期方法。状态的改变会导致调用onEnabled/onDisable。 36 | */ 37 | public get enabled() { 38 | return this.entity ? this.entity.enabled && this._enabled : this._enabled; 39 | } 40 | 41 | /** 42 | * 如果组件和实体都已启用,则为。当启用该组件时,将调用该组件的生命周期方法。状态的改变会导致调用onEnabled/onDisable。 43 | * @param value 44 | */ 45 | public set enabled(value: boolean) { 46 | this.setEnabled(value); 47 | } 48 | 49 | private _updateOrder = 0; 50 | 51 | /** 更新此实体上组件的顺序 */ 52 | public get updateOrder() { 53 | return this._updateOrder; 54 | } 55 | 56 | /** 更新此实体上组件的顺序 */ 57 | public set updateOrder(value: number) { 58 | this.setUpdateOrder(value); 59 | } 60 | 61 | /** 62 | * 当此组件已分配其实体,但尚未添加到实体的活动组件列表时调用。有用的东西,如物理组件,需要访问转换来修改碰撞体的属性。 63 | */ 64 | public initialize() { 65 | } 66 | 67 | /** 68 | * 在提交所有挂起的组件更改后,将该组件添加到场景时调用。此时,设置了实体字段和实体。场景也设定好了。 69 | */ 70 | public onAddedToEntity() { 71 | } 72 | 73 | /** 74 | * 当此组件从其实体中移除时调用。在这里做所有的清理工作。 75 | */ 76 | public onRemovedFromEntity() { 77 | } 78 | 79 | /** 80 | * 当实体的位置改变时调用。这允许组件知道它们由于父实体的移动而移动了。 81 | * @param comp 82 | */ 83 | public onEntityTransformChanged(comp: ComponentTransform) { 84 | } 85 | 86 | /** 87 | *当父实体或此组件启用时调用 88 | */ 89 | public onEnabled() { 90 | } 91 | 92 | /** 93 | * 禁用父实体或此组件时调用 94 | */ 95 | public onDisabled() { 96 | } 97 | 98 | public setEnabled(isEnabled: boolean) { 99 | if (this._enabled != isEnabled) { 100 | this._enabled = isEnabled; 101 | 102 | if (this._enabled) { 103 | this.onEnabled(); 104 | } else { 105 | this.onDisabled(); 106 | } 107 | } 108 | 109 | return this; 110 | } 111 | 112 | public setUpdateOrder(updateOrder: number) { 113 | if (this._updateOrder != updateOrder) { 114 | this._updateOrder = updateOrder; 115 | } 116 | 117 | return this; 118 | } 119 | 120 | /** 121 | * 添加组件 122 | * @param component 要添加的组件实例 123 | * @returns 返回添加的组件实例 124 | */ 125 | public addComponent(component: T): T { 126 | return this.entity.addComponent(component); 127 | } 128 | 129 | /** 130 | * 获取组件 131 | * @param type 组件类型 132 | * @returns 返回获取到的组件实例 133 | */ 134 | public getComponent(type: new (...args: any[]) => T): T { 135 | return this.entity.getComponent(type); 136 | } 137 | 138 | /** 139 | * 获取一组指定类型的组件 140 | * @param typeName 组件类型名 141 | * @param componentList 可选参数,存储组件实例的数组 142 | * @returns 返回指定类型的组件实例数组 143 | */ 144 | public getComponents(typeName: any, componentList?: any[]): any[] { 145 | return this.entity.getComponents(typeName, componentList); 146 | } 147 | 148 | /** 149 | * 判断实体是否包含指定类型的组件 150 | * @param type 组件类型 151 | * @returns 如果实体包含指定类型的组件,返回 true,否则返回 false。 152 | */ 153 | public hasComponent(type: new (...args: any[]) => Component): boolean { 154 | return this.entity.hasComponent(type); 155 | } 156 | 157 | /** 158 | * 删除组件 159 | * @param component 可选参数,要删除的组件实例。如果未指定该参数,则删除当前实例上的组件。 160 | */ 161 | public removeComponent(component?: Component): void { 162 | if (component) { 163 | this.entity.removeComponent(component); 164 | } else { 165 | this.entity.removeComponent(this); 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /source/src/ECS/ComponentType.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ComponentType { 3 | public static INDEX = 0; 4 | 5 | private index_ = 0; 6 | private type_: Class; 7 | 8 | constructor(type: Class, index?: number) { 9 | if (index !== undefined) { 10 | this.index_ = ComponentType.INDEX++; 11 | } else { 12 | this.index_ = index; 13 | } 14 | this.type_ = type; 15 | } 16 | 17 | public getName(): string { 18 | return getClassName(this.type_); 19 | } 20 | 21 | public getIndex(): number { 22 | return this.index_; 23 | } 24 | 25 | public toString(): string { 26 | return "ComponentType[" + getClassName(ComponentType) + "] (" + this.index_ + ")"; 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /source/src/ECS/Components/IUpdatable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 接口,当添加到一个Component时,只要Component和实体被启用,它就会在每个框架中调用更新方法。 4 | */ 5 | export interface IUpdatable { 6 | enabled: boolean; 7 | updateOrder: number; 8 | update(); 9 | } 10 | 11 | /** 12 | * 用于比较组件更新排序 13 | */ 14 | export class IUpdatableComparer implements IComparer { 15 | public compare(a: IUpdatable, b: IUpdatable) { 16 | return a.updateOrder - b.updateOrder; 17 | } 18 | } 19 | 20 | export var isIUpdatable = (props: any): props is IUpdatable => typeof (props as IUpdatable)['update'] !== 'undefined'; 21 | } -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/Colliders/BoxCollider.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | export class BoxCollider extends Collider { 4 | /** 5 | * 创建一个BoxCollider,并使用x/y组件作为局部Offset 6 | * @param x 7 | * @param y 8 | * @param width 9 | * @param height 10 | */ 11 | constructor(x: number = 0, y: number = 0, width: number = 1, height: number = 1) { 12 | super(); 13 | 14 | this._localOffset = new Vector2(x + width / 2, y + height / 2); 15 | 16 | this.shape = new Box(width, height); 17 | } 18 | 19 | public get width() { 20 | return (this.shape as Box).width; 21 | } 22 | 23 | public set width(value: number) { 24 | this.setWidth(value); 25 | } 26 | 27 | public get height() { 28 | return (this.shape as Box).height; 29 | } 30 | 31 | public set height(value: number) { 32 | this.setHeight(value); 33 | } 34 | 35 | /** 36 | * 设置BoxCollider的大小 37 | * @param width 38 | * @param height 39 | */ 40 | public setSize(width: number, height: number) { 41 | let box = this.shape as Box; 42 | if (width != box.width || height != box.height) { 43 | // 更新框,改变边界,如果我们需要更新物理系统中的边界 44 | box.updateBox(width, height); 45 | this._isPositionDirty = true; 46 | if (this.entity && this._isParentEntityAddedToScene) 47 | Physics.updateCollider(this); 48 | } 49 | 50 | return this; 51 | } 52 | 53 | /** 54 | * 设置BoxCollider的宽度 55 | * @param width 56 | */ 57 | public setWidth(width: number): BoxCollider { 58 | let box = this.shape as Box; 59 | if (width != box.width) { 60 | // 更新框,改变边界,如果我们需要更新物理系统中的边界 61 | box.updateBox(width, box.height); 62 | this._isPositionDirty = true; 63 | if (this.entity != null && this._isParentEntityAddedToScene) 64 | Physics.updateCollider(this); 65 | } 66 | 67 | return this; 68 | } 69 | 70 | /** 71 | * 设置BoxCollider的高度 72 | * @param height 73 | */ 74 | public setHeight(height: number) { 75 | let box = this.shape as Box; 76 | if (height != box.height) { 77 | // 更新框,改变边界,如果我们需要更新物理系统中的边界 78 | box.updateBox(box.width, height); 79 | this._isPositionDirty = true; 80 | if (this.entity && this._isParentEntityAddedToScene) 81 | Physics.updateCollider(this); 82 | } 83 | } 84 | 85 | public toString() { 86 | return `[BoxCollider: bounds: ${this.bounds}]`; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/Colliders/CircleCollider.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class CircleCollider extends Collider { 3 | /** 4 | * 创建一个具有半径的CircleCollider。 5 | * 请注意,当指定半径时,如果在实体上使用RenderableComponent,您将需要设置原点来对齐CircleCollider。 6 | * 例如,如果RenderableComponent有一个0,0的原点,并且创建了一个半径为1.5f * renderable.width的CircleCollider,你可以通过设置originNormalied为中心除以缩放尺寸来偏移原点 7 | * 8 | * @param radius 9 | */ 10 | constructor(radius: number = 1) { 11 | super(); 12 | 13 | this.shape = new Circle(radius); 14 | } 15 | 16 | public get radius(): number { 17 | return (this.shape as Circle).radius; 18 | } 19 | 20 | public set radius(value: number) { 21 | this.setRadius(value); 22 | } 23 | 24 | /** 25 | * 设置圆的半径 26 | * @param radius 27 | */ 28 | public setRadius(radius: number): CircleCollider { 29 | let circle = this.shape as Circle; 30 | if (radius != circle.radius) { 31 | circle.radius = radius; 32 | circle._originalRadius = radius; 33 | this._isPositionDirty = true; 34 | 35 | if (this.entity != null && this._isParentEntityAddedToScene) 36 | Physics.updateCollider(this); 37 | } 38 | 39 | return this; 40 | } 41 | 42 | public toString() { 43 | return `[CircleCollider: bounds: ${this.bounds}, radius: ${(this.shape as Circle).radius}]` 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/Colliders/PolygonCollider.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 多边形应该以顺时针方式定义 4 | */ 5 | export class PolygonCollider extends Collider { 6 | /** 7 | * 如果这些点没有居中,它们将以localOffset的差异为居中。 8 | * @param points 9 | */ 10 | constructor(points: Vector2[]) { 11 | super(); 12 | 13 | // 第一点和最后一点决不能相同。我们想要一个开放的多边形 14 | let isPolygonClosed = points[0] == points[points.length - 1]; 15 | 16 | // 最后一个移除 17 | if (isPolygonClosed) 18 | points = points.slice(0, points.length - 1); 19 | 20 | let center = Polygon.findPolygonCenter(points); 21 | this.setLocalOffset(center); 22 | Polygon.recenterPolygonVerts(points); 23 | this.shape = new Polygon(points); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/Colliders/SectorCollider.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 扇形碰撞器 4 | */ 5 | export class SectorCollider extends Collider { 6 | constructor( 7 | center: Vector2, 8 | radius: number, 9 | startAngle: number, 10 | endAngle: number 11 | ) { 12 | super(); 13 | this.shape = new Sector(center, radius, startAngle, endAngle); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/ITriggerListener.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 当添加到组件时,每当实体上的冲突器与另一个组件重叠/退出时,将调用这些方法。 4 | * ITriggerListener方法将在实现接口的触发器实体上的任何组件上调用。 5 | * 注意,这个接口只与Mover类一起工作 6 | */ 7 | export interface ITriggerListener { 8 | /** 9 | * 当碰撞器与触发碰撞器相交时调用。这是在触发碰撞器和触发碰撞器上调用的。 10 | * 移动必须由Mover/ProjectileMover方法处理,以使其自动工作。 11 | * @param other 12 | * @param local 13 | */ 14 | onTriggerEnter(other: Collider, local: Collider); 15 | 16 | /** 17 | * 当另一个碰撞器离开触发碰撞器时调用 18 | * @param other 19 | * @param local 20 | */ 21 | onTriggerExit(other: Collider, local: Collider); 22 | } 23 | 24 | export class TriggerListenerHelper { 25 | public static getITriggerListener(entity: Entity, components: ITriggerListener[]){ 26 | if (entity.components._components.length > 0) { 27 | for (let i = 0; i < entity.components._components.length; i ++) { 28 | const component = entity.components._components[i]; 29 | if (isITriggerListener(component)) { 30 | components.push(component); 31 | } 32 | } 33 | } 34 | 35 | for (let i in entity.components._componentsToAdd) { 36 | let component = entity.components._componentsToAdd[i]; 37 | if (isITriggerListener(component)) { 38 | components.push(component); 39 | } 40 | } 41 | 42 | return components; 43 | } 44 | } 45 | 46 | export var isITriggerListener = (props: any): props is ITriggerListener => typeof (props as ITriggerListener)['onTriggerEnter'] !== 'undefined'; 47 | } 48 | -------------------------------------------------------------------------------- /source/src/ECS/Components/Physics/ProjectileMover.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 移动时考虑到碰撞,只用于向任何ITriggerListeners报告。 4 | * 物体总是会全量移动,所以如果需要的话,由调用者在撞击时销毁它。 5 | */ 6 | export class ProjectileMover extends Component { 7 | private _tempTriggerList: ITriggerListener[] = []; 8 | private _collider: Collider; 9 | 10 | public onAddedToEntity() { 11 | let collider = null; 12 | for (let i = 0; i < this.entity.components.buffer.length; i++) { 13 | let component = this.entity.components.buffer[i]; 14 | if (component instanceof Collider) { 15 | collider = component; 16 | break; 17 | } 18 | } 19 | this._collider = collider; 20 | Debug.warnIf(this._collider == null, "ProjectileMover没有Collider。ProjectilMover需要一个Collider!"); 21 | } 22 | 23 | /** 24 | * 在考虑到碰撞的情况下移动实体 25 | * @param motion 26 | */ 27 | public move(motion: Vector2): boolean { 28 | if (this._collider == null) 29 | return false; 30 | 31 | let didCollide = false; 32 | 33 | // 获取我们在新的位置上可能会碰撞到的任何东西 34 | this.entity.position = Vector2.add(this.entity.position, motion); 35 | 36 | // 获取任何可能在新位置发生碰撞的东西 37 | let neighbors = Physics.boxcastBroadphase(this._collider.bounds, this._collider.collidesWithLayers.value); 38 | if (neighbors.length > 0) 39 | for (let i = 0; i < neighbors.length; i ++) { 40 | const neighbor = neighbors[i]; 41 | if (this._collider.overlaps(neighbor) && neighbor.enabled){ 42 | didCollide = true; 43 | this.notifyTriggerListeners(this._collider, neighbor); 44 | } 45 | } 46 | 47 | return didCollide; 48 | } 49 | 50 | private notifyTriggerListeners(self: Collider, other: Collider) { 51 | // 通知我们重叠的碰撞器实体上的任何侦听器 52 | TriggerListenerHelper.getITriggerListener(other.entity, this._tempTriggerList); 53 | for (let i = 0; i < this._tempTriggerList.length; i++) { 54 | this._tempTriggerList[i].onTriggerEnter(self, other); 55 | } 56 | this._tempTriggerList.length = 0; 57 | 58 | // 通知此实体上的任何侦听器 59 | TriggerListenerHelper.getITriggerListener(this.entity, this._tempTriggerList); 60 | for (let i = 0; i < this._tempTriggerList.length; i++) { 61 | this._tempTriggerList[i].onTriggerEnter(other, self); 62 | } 63 | this._tempTriggerList.length = 0; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /source/src/ECS/Components/SceneComponent.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class SceneComponent implements IComparer { 3 | /** 4 | * 这个场景组件被附加到的场景 5 | */ 6 | public scene: Scene; 7 | 8 | /** 9 | * 如果启用了SceneComponent,则为true。状态的改变会导致调用onEnabled/onDisable。 10 | */ 11 | public get enabled() { 12 | return this._enabled; 13 | } 14 | 15 | /** 16 | * 如果启用了SceneComponent,则为true。状态的改变会导致调用onEnabled/onDisable。 17 | * @param value 18 | */ 19 | public set enabled(value: boolean) { 20 | this.setEnabled(value); 21 | } 22 | 23 | /** 24 | * 更新此场景中SceneComponents的顺序 25 | */ 26 | public updateOrder: number = 0; 27 | 28 | public _enabled: boolean = true; 29 | 30 | /** 31 | * 在启用此SceneComponent时调用 32 | */ 33 | public onEnabled() { 34 | } 35 | 36 | /** 37 | * 当禁用此SceneComponent时调用 38 | */ 39 | public onDisabled() { 40 | } 41 | 42 | /** 43 | * 当该SceneComponent从场景中移除时调用 44 | */ 45 | public onRemovedFromScene() { 46 | } 47 | 48 | /** 49 | * 在实体更新之前每一帧调用 50 | */ 51 | public update() { 52 | } 53 | 54 | /** 55 | * 启用/禁用这个SceneComponent 56 | * @param isEnabled 57 | */ 58 | public setEnabled(isEnabled: boolean): SceneComponent { 59 | if (this._enabled != isEnabled) { 60 | this._enabled = isEnabled; 61 | 62 | if (this._enabled) { 63 | this.onEnabled(); 64 | } else { 65 | this.onDisabled(); 66 | } 67 | } 68 | 69 | return this; 70 | } 71 | 72 | /** 73 | * 设置SceneComponent的updateOrder并触发某种SceneComponent 74 | * @param updateOrder 75 | */ 76 | public setUpdateOrder(updateOrder: number) { 77 | if (this.updateOrder != updateOrder) { 78 | this.updateOrder = updateOrder; 79 | } 80 | 81 | return this; 82 | } 83 | 84 | public compare(other: SceneComponent): number { 85 | return this.updateOrder - other.updateOrder; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /source/src/ECS/CoreEvents.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export enum CoreEvents { 3 | /** 4 | * 当场景发生变化时触发 5 | */ 6 | sceneChanged, 7 | /** 8 | * 每帧更新事件 9 | */ 10 | frameUpdated, 11 | /** 12 | * 当渲染发生时触发 13 | */ 14 | renderChanged, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/src/ECS/SceneTransition.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * SceneTransition用于从一个场景过渡到另一个场景,或在一个场景内进行效果转换。 4 | * 如果sceneLoadAction为null,框架将执行场景内过渡,而不是加载新的场景中间过渡。 5 | */ 6 | export abstract class SceneTransition { 7 | /** 该函数应返回新加载的场景 */ 8 | protected sceneLoadAction: () => Scene; 9 | 10 | /** 11 | * 在loadNextScene执行时调用 12 | * 这在进行场景间转换时非常有用,这样您就可以知道何时可以重新设置相机或重置任何实体 13 | */ 14 | public onScreenObscured: Function; 15 | 16 | /** 17 | * 转换完成后调用,以便调用其他工作,例如启动另一个场景转换 18 | */ 19 | public onTransitionCompleted: Function; 20 | 21 | /** 22 | * 指示此转换是否将加载新场景的标志 23 | */ 24 | public _loadsNewScene: boolean = false; 25 | 26 | private _hasPreviousSceneRender: boolean = false; 27 | 28 | public get hasPreviousSceneRender() { 29 | if (!this._hasPreviousSceneRender) { 30 | this._hasPreviousSceneRender = true; 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | /** 38 | * 将此用于两部分过渡。例如,褪色会先褪色为黑色,然后当_isNewSceneLoaded变为true时会褪色。 39 | * 对于场景内转换,应在中点将isNewSceneLoaded设置为true,就像加载了新场景一样 40 | */ 41 | public _isNewSceneLoaded: boolean = false; 42 | 43 | protected constructor(sceneLoadAction: () => Scene) { 44 | this.sceneLoadAction = sceneLoadAction; 45 | this._loadsNewScene = sceneLoadAction != null; 46 | } 47 | 48 | protected * LoadNextScene() { 49 | // 如果我们有渲染界面,可以在这让玩家知道屏幕是模糊的(正在加载) 50 | if (this.onScreenObscured != null) 51 | this.onScreenObscured(); 52 | 53 | // 如果我们不加载一个新场景,我们只需设置标志 54 | if (!this._loadsNewScene) { 55 | this._isNewSceneLoaded = true; 56 | yield "break"; 57 | } 58 | 59 | Core.scene = this.sceneLoadAction(); 60 | this._isNewSceneLoaded = true; 61 | 62 | while (!this._isNewSceneLoaded) 63 | yield null; 64 | } 65 | 66 | /** 67 | * 在前一个场景出现第一次(也是唯一一次)后调用。 68 | * 此时,可以在生成一帧后加载新场景(因此第一次渲染调用发生在场景加载之前) 69 | */ 70 | public * onBeginTransition(): any { 71 | yield null; 72 | yield Core.startCoroutine(this.LoadNextScene()); 73 | 74 | this.transitionComplete(); 75 | } 76 | 77 | /** 78 | * 在渲染场景之前调用 79 | */ 80 | public preRender() { } 81 | 82 | /** 83 | * 在这里进行所有渲染 84 | */ 85 | public render() { } 86 | 87 | /** 88 | * 当过渡完成且新场景已设置时,将调用此函数 89 | */ 90 | protected transitionComplete() { 91 | Core.Instance._sceneTransition = null; 92 | 93 | if (this.onTransitionCompleted != null) 94 | this.onTransitionCompleted(); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /source/src/ECS/Systems/DelayedIteratingSystem.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | /** 4 | * 这个类是一个实体系统的基类,其可以被子类继承并在子类中实现具体的实体处理逻辑。 5 | * 该类提供了实体的添加、删除、更新等基本操作,并支持设置系统的更新时序、检查系统是否需要处理实体、获取系统的场景等方法 6 | */ 7 | export abstract class DelayedIteratingSystem extends EntitySystem { 8 | private delay = 0; 9 | private running = false; 10 | private acc = 0; 11 | 12 | constructor(matcher: Matcher) { 13 | super(matcher); 14 | } 15 | 16 | protected process(entities: Entity[]) { 17 | const processed = entities.length; 18 | 19 | if (processed === 0) { 20 | this.stop(); 21 | return; 22 | } 23 | 24 | this.delay = Number.MAX_VALUE; 25 | 26 | for (let i = 0; i < processed; i++) { 27 | const entity = entities[i]; 28 | this.processDelta(entity, this.acc); 29 | const remaining = this.getRemainingDelay(entity); 30 | 31 | if (remaining <= 0) { 32 | this.processExpired(entity); 33 | } else { 34 | this.offerDelay(remaining); 35 | } 36 | } 37 | 38 | this.acc = 0; 39 | } 40 | 41 | protected checkProcessing() { 42 | if (this.running) { 43 | this.acc += Time.deltaTime; 44 | return this.acc >= this.delay; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * 只有当提供的延迟比系统当前计划执行的时间短时,才会重新启动系统。 52 | * 如果系统已经停止(不运行),那么提供的延迟将被用来重新启动系统,无论其值如何 53 | * 如果系统已经在倒计时,并且提供的延迟大于剩余时间,系统将忽略它。 54 | * 如果提供的延迟时间短于剩余时间,系统将重新启动,以提供的延迟时间运行。 55 | * @param offeredDelay 提供的延迟时间,单位为秒 56 | */ 57 | public offerDelay(offeredDelay: number) { 58 | if (!this.running) { 59 | this.running = true; 60 | this.delay = offeredDelay; 61 | } else { 62 | this.delay = Math.min(this.delay, offeredDelay); 63 | } 64 | } 65 | 66 | /** 67 | * 获取系统被命令处理实体后的初始延迟 68 | */ 69 | public getInitialTimeDelay() { 70 | return this.delay; 71 | } 72 | 73 | /** 74 | * 获取系统计划运行前的时间 75 | * 如果系统没有运行,则返回零 76 | */ 77 | public getRemainingTimeUntilProcessing(): number { 78 | if (this.running) { 79 | return this.delay - this.acc; 80 | } 81 | 82 | return 0; 83 | } 84 | 85 | /** 86 | * 检查系统是否正在倒计时处理 87 | */ 88 | public isRunning(): boolean { 89 | return this.running; 90 | } 91 | 92 | /** 93 | * 停止系统运行,中止当前倒计时 94 | */ 95 | public stop() { 96 | this.running = false; 97 | this.acc = 0; 98 | } 99 | 100 | /** 101 | * 处理给定实体的延迟时间的一部分,抽象出累积的 Delta 值。 102 | * @param entity 要处理的实体 103 | * @param accumulatedDelta 本系统最后一次执行后的累积 delta 时间 104 | */ 105 | protected abstract processDelta(entity: Entity, accumulatedDelta: number); 106 | 107 | /** 108 | * 处理已到期的实体。 109 | * @param entity 要处理的实体 110 | */ 111 | protected abstract processExpired(entity: Entity); 112 | 113 | /** 114 | * 获取给定实体剩余的延迟时间。 115 | * @param entity 要检查的实体 116 | * @returns 剩余的延迟时间(以秒为单位) 117 | */ 118 | protected abstract getRemainingDelay(entity: Entity): number; 119 | } 120 | } -------------------------------------------------------------------------------- /source/src/ECS/Systems/EntityProcessingSystem.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | /** 4 | * 定义一个处理实体的抽象类,继承自 EntitySystem 类。 5 | * 子类需要实现 processEntity 方法,用于实现具体的实体处理逻辑。 6 | */ 7 | export abstract class EntityProcessingSystem extends EntitySystem { 8 | /** 9 | * 是否启用系统,默认为启用。 10 | */ 11 | public enabled: boolean = true; 12 | 13 | /** 14 | * 构造函数,初始化实体匹配器。 15 | * @param matcher 实体匹配器 16 | */ 17 | constructor(matcher: Matcher) { 18 | super(matcher); 19 | } 20 | 21 | /** 22 | * 处理单个实体,由子类实现。 23 | * @param entity 待处理的实体 24 | */ 25 | public abstract processEntity(entity: Entity): void; 26 | 27 | /** 28 | * 在晚于 update 的时间更新实体,由子类实现。 29 | * @param entity 待处理的实体 30 | */ 31 | public lateProcessEntity(entity: Entity): void { 32 | // do nothing 33 | } 34 | 35 | /** 36 | * 遍历系统的所有实体,逐个进行实体处理。 37 | * @param entities 实体数组 38 | */ 39 | protected process(entities: Entity[]) { 40 | // 如果实体数组为空,则直接返回 41 | if (entities.length === 0) { 42 | return; 43 | } 44 | 45 | // 遍历实体数组,逐个进行实体处理 46 | for (let i = 0, len = entities.length; i < len; i++) { 47 | const entity = entities[i]; 48 | this.processEntity(entity); 49 | } 50 | } 51 | 52 | /** 53 | * 在晚于 update 的时间更新实体。 54 | * @param entities 实体数组 55 | */ 56 | protected lateProcess(entities: Entity[]) { 57 | // 如果实体数组为空,则直接返回 58 | if (entities.length === 0) { 59 | return; 60 | } 61 | 62 | // 遍历实体数组,逐个进行实体处理 63 | for (let i = 0, len = entities.length; i < len; i++) { 64 | const entity = entities[i]; 65 | this.lateProcessEntity(entity); 66 | } 67 | } 68 | 69 | /** 70 | * 判断系统是否需要进行实体处理。 71 | * 如果启用了系统,则需要进行实体处理,返回 true; 72 | * 否则不需要进行实体处理,返回 false。 73 | */ 74 | protected checkProcessing(): boolean { 75 | return this.enabled; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /source/src/ECS/Systems/EntitySystem.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | /** 4 | * 实体系统的基类,用于处理一组实体。 5 | */ 6 | export abstract class EntitySystem { 7 | private _entities: Entity[] = []; 8 | private _updateOrder: number = 0; 9 | private _startTime = 0; 10 | private _endTime = 0; 11 | private _useTime = 0; 12 | 13 | /** 获取系统在当前帧所消耗的时间 仅在debug模式下生效 */ 14 | public get useTime() { 15 | return this._useTime; 16 | } 17 | 18 | /** 19 | * 获取系统的更新时序 20 | */ 21 | public get updateOrder() { 22 | return this._updateOrder; 23 | } 24 | 25 | public set updateOrder(value: number) { 26 | this.setUpdateOrder(value); 27 | } 28 | 29 | constructor(matcher?: Matcher) { 30 | this._matcher = matcher ? matcher : Matcher.empty(); 31 | this.initialize(); 32 | } 33 | 34 | private _scene: Scene; 35 | 36 | /** 37 | * 这个系统所属的场景 38 | */ 39 | public get scene() { 40 | return this._scene; 41 | } 42 | 43 | public set scene(value: Scene) { 44 | this._scene = value; 45 | this._entities = []; 46 | } 47 | 48 | private _matcher: Matcher; 49 | 50 | public get matcher() { 51 | return this._matcher; 52 | } 53 | 54 | /** 55 | * 设置更新时序 56 | * @param order 更新时序 57 | */ 58 | public setUpdateOrder(order: number) { 59 | this._updateOrder = order; 60 | this.scene.entityProcessors.setDirty(); 61 | } 62 | 63 | public initialize() { 64 | 65 | } 66 | 67 | public onChanged(entity: Entity) { 68 | let contains = !!this._entities.find(e => e.id == entity.id); 69 | let interest = this._matcher.isInterestedEntity(entity); 70 | 71 | if (interest && !contains) 72 | this.add(entity); 73 | else if (!interest && contains) 74 | this.remove(entity); 75 | } 76 | 77 | public add(entity: Entity) { 78 | if (!this._entities.find(e => e.id == entity.id)) 79 | this._entities.push(entity); 80 | this.onAdded(entity); 81 | } 82 | 83 | public onAdded(entity: Entity) { 84 | } 85 | 86 | public remove(entity: Entity) { 87 | new es.List(this._entities).remove(entity); 88 | this.onRemoved(entity); 89 | } 90 | 91 | public onRemoved(entity: Entity) { 92 | } 93 | 94 | public update() { 95 | if (this.checkProcessing()) { 96 | this.begin(); 97 | this.process(this._entities); 98 | } 99 | } 100 | 101 | public lateUpdate() { 102 | if (this.checkProcessing()) { 103 | this.lateProcess(this._entities); 104 | this.end(); 105 | } 106 | } 107 | 108 | /** 109 | * 在系统处理开始前调用 110 | * 在下一个系统开始处理或新的处理回合开始之前(以先到者为准),使用此方法创建的任何实体都不会激活 111 | */ 112 | protected begin() { 113 | if (!Core.Instance.debug) 114 | return; 115 | 116 | this._startTime = Date.now(); 117 | } 118 | 119 | protected process(entities: Entity[]) { 120 | } 121 | 122 | protected lateProcess(entities: Entity[]) { 123 | } 124 | 125 | /** 126 | * 系统处理完毕后调用 127 | */ 128 | protected end() { 129 | if (!Core.Instance.debug) 130 | return; 131 | 132 | this._endTime = Date.now(); 133 | this._useTime = this._endTime - this._startTime; 134 | } 135 | 136 | /** 137 | * 系统是否需要处理 138 | * 139 | * 在启用系统时有用,但仅偶尔需要处理 140 | * 这只影响处理,不影响事件或订阅列表 141 | * @returns 如果系统应该处理,则为true,如果不处理则为false。 142 | */ 143 | protected checkProcessing() { 144 | return true; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /source/src/ECS/Systems/IntervalIteratingSystem.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | /** 4 | * 定时遍历处理实体的系统,用于按指定的时间间隔遍历并处理感兴趣的实体。 5 | */ 6 | export abstract class IntervalIteratingSystem extends IntervalSystem { 7 | 8 | constructor(matcher: Matcher, interval: number) { 9 | super(matcher, interval); 10 | } 11 | 12 | /** 13 | * 处理本系统感兴趣的实体 14 | * @param entity 15 | */ 16 | public abstract processEntity(entity: Entity); 17 | 18 | /** 19 | * 遍历处理实体。 20 | * @param entities 本系统感兴趣的实体列表 21 | */ 22 | protected process(entities: Entity[]) { 23 | entities.forEach(entity => this.processEntity(entity)); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /source/src/ECS/Systems/IntervalSystem.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 定义一个按时间间隔处理的抽象类,继承自 EntitySystem 类。 4 | * 子类需要实现 process 方法,用于实现具体的处理逻辑。 5 | */ 6 | export abstract class IntervalSystem extends EntitySystem { 7 | /** 8 | * 累积增量以跟踪间隔 9 | */ 10 | private acc: number = 0; 11 | 12 | /** 13 | * 更新之间需要等待多长时间 14 | */ 15 | private readonly interval: number; 16 | 17 | /** 18 | * 时间间隔的余数,用于计算下一次需要等待的时间 19 | */ 20 | private intervalRemainder: number = 0; 21 | 22 | /** 23 | * 构造函数,初始化时间间隔。 24 | * @param matcher 实体匹配器 25 | * @param interval 时间间隔 26 | */ 27 | constructor(matcher: Matcher, interval: number) { 28 | super(matcher); 29 | this.interval = interval; 30 | } 31 | 32 | /** 33 | * 判断是否需要进行处理。 34 | * 如果需要进行处理,则更新累积增量和时间间隔余数,返回 true; 35 | * 否则返回 false。 36 | */ 37 | protected checkProcessing(): boolean { 38 | // 更新累积增量 39 | this.acc += Time.deltaTime; 40 | 41 | // 如果累积增量超过时间间隔,则进行处理 42 | if (this.acc >= this.interval) { 43 | // 更新时间间隔余数 44 | this.intervalRemainder = this.acc - this.interval; 45 | // 重置累积增量 46 | this.acc = 0; 47 | // 返回 true,表示需要进行处理 48 | return true; 49 | } 50 | 51 | // 返回 false,表示不需要进行处理 52 | return false; 53 | } 54 | 55 | /** 56 | * 获取本系统上次处理后的实际 delta 值。 57 | * 实际 delta 值等于时间间隔加上时间间隔余数。 58 | */ 59 | protected getIntervalDelta(): number { 60 | return this.interval + this.intervalRemainder; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /source/src/ECS/Systems/PassiveSystem.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 定义一个被动的实体系统,继承自 EntitySystem 类。 4 | * 被动的实体系统不会对实体进行任何修改,只会被动地接收实体的变化事件。 5 | */ 6 | export abstract class PassiveSystem extends EntitySystem { 7 | /** 8 | * 当实体发生变化时,不进行任何操作。 9 | * @param entity 发生变化的实体 10 | */ 11 | public onChanged(entity: Entity) { } 12 | 13 | /** 14 | * 不进行任何处理,只进行开始和结束计时。 15 | * @param entities 实体数组,未被使用 16 | */ 17 | protected process(entities: Entity[]) { 18 | // 调用 begin 和 end 方法,开始和结束计时 19 | this.begin(); 20 | this.end(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/src/ECS/Systems/ProcessingSystem.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 定义一个处理实体的抽象类,继承自 EntitySystem 类。 4 | * 子类需要实现 processSystem 方法,用于实现具体的处理逻辑。 5 | */ 6 | export abstract class ProcessingSystem extends EntitySystem { 7 | /** 8 | * 当实体发生变化时,不进行任何操作。 9 | * @param entity 发生变化的实体 10 | */ 11 | public onChanged(entity: Entity) { } 12 | 13 | /** 14 | * 处理实体,每帧调用 processSystem 方法进行处理。 15 | * @param entities 实体数组,未被使用 16 | */ 17 | protected process(entities: Entity[]) { 18 | // 调用 begin 和 end 方法,开始和结束计时 19 | this.begin(); 20 | // 调用子类实现的 processSystem 方法进行实体处理 21 | this.processSystem(); 22 | this.end(); 23 | } 24 | 25 | /** 26 | * 处理实体的具体方法,由子类实现。 27 | */ 28 | public abstract processSystem(): void; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /source/src/ECS/Utils/Bits.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 位操作类,用于操作一个位数组。 4 | */ 5 | export class Bits { 6 | private _bit: { [index: number]: number } = {}; 7 | 8 | /** 9 | * 设置指定位置的位值。 10 | * @param index 位置索引 11 | * @param value 位值(0 或 1) 12 | */ 13 | public set(index: number, value: number) { 14 | this._bit[index] = value; 15 | } 16 | 17 | /** 18 | * 获取指定位置的位值。 19 | * @param index 位置索引 20 | * @returns 位值(0 或 1) 21 | */ 22 | public get(index: number): number { 23 | let v = this._bit[index]; 24 | return v == null ? 0 : v; 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /source/src/ECS/Utils/ComponentTypeFactory.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 组件类型工厂,用于生成和管理组件类型。 4 | * 维护了一个类型映射表,将组件类型与其唯一索引相对应,以便在运行时高效地检查实体是否包含特定的组件类型。 5 | */ 6 | export class ComponentTypeFactory { 7 | /** 组件类型与其唯一索引的映射表 */ 8 | private componentTypes: Record = {}; 9 | /** 组件类型列表,按索引访问组件类型 */ 10 | public readonly types: Bag = new Bag(); 11 | /** 当前组件类型的计数器 */ 12 | private componentTypeCount = 0; 13 | 14 | /** 15 | * 获取给定组件类型的唯一索引。 16 | * 如果该组件类型尚未存在于类型映射表中,则创建一个新的组件类型,并将其添加到映射表和类型列表中。 17 | * @param c 要查找或创建的组件类型 18 | * @returns 组件类型的唯一索引 19 | */ 20 | public getIndexFor(c: new (...args: any[]) => any): number { 21 | return this.getTypeFor(c).getIndex(); 22 | } 23 | 24 | /** 25 | * 获取给定组件类型的ComponentType对象。 26 | * 如果该组件类型尚未存在于类型映射表中,则创建一个新的ComponentType对象,并将其添加到映射表和类型列表中。 27 | * @param c 要查找或创建的组件类型 28 | * @returns 组件类型的ComponentType对象 29 | */ 30 | public getTypeFor(c: new (...args: any[]) => any): ComponentType { 31 | // 如果给定的组件类型是一个已有的索引,则直接返回对应的ComponentType对象 32 | if (typeof c === "number") { 33 | return this.types.get(c); 34 | } 35 | 36 | // 获取给定组件类型对应的类名 37 | const className = getClassName(c); 38 | 39 | // 如果类型映射表中不存在该组件类型,则创建一个新的ComponentType对象 40 | if (!this.componentTypes[className]) { 41 | const index = this.componentTypeCount++; 42 | const type = new ComponentType(c, index); 43 | this.componentTypes[className] = type; 44 | this.types.set(index, type); 45 | } 46 | 47 | // 返回对应的ComponentType对象 48 | return this.componentTypes[className]; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /source/src/ECS/Utils/ComponentTypeManager.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 组件类型管理器,维护了一个组件类型和它们对应的位掩码之间的映射关系。 4 | * 用于实现实体匹配器中组件类型的比较操作,以确定实体是否符合给定的匹配器条件。 5 | */ 6 | export class ComponentTypeManager { 7 | /** 存储组件类型和它们对应的位掩码的Map */ 8 | private static _componentTypesMask: Map = new Map(); 9 | 10 | /** 11 | * 将给定的组件类型添加到组件类型列表中,并分配一个唯一的位掩码。 12 | * @param type 要添加的组件类型 13 | */ 14 | public static add(type) { 15 | if (!this._componentTypesMask.has(type)) { 16 | this._componentTypesMask.set(type, this._componentTypesMask.size); 17 | } 18 | } 19 | 20 | /** 21 | * 获取给定组件类型的位掩码。 22 | * 如果该组件类型还没有分配位掩码,则将其添加到列表中,并分配一个唯一的位掩码。 23 | * @param type 要获取位掩码的组件类型 24 | * @returns 组件类型的位掩码 25 | */ 26 | public static getIndexFor(type) { 27 | let v = -1; 28 | if (!this._componentTypesMask.has(type)) { 29 | this.add(type); 30 | v = this._componentTypesMask.get(type); 31 | } else { 32 | v = this._componentTypesMask.get(type); 33 | } 34 | 35 | return v; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/src/ECS/Utils/HashHelper.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class HashHelpers { 3 | // 哈希冲突阈值,超过此值将使用另一种哈希算法 4 | public static readonly hashCollisionThreshold: number = 100; 5 | // 哈希值用于计算哈希表索引的质数 6 | public static readonly hashPrime: number = 101; 7 | // 一组预定义的质数,用于计算哈希表容量 8 | public static readonly primes = [ 9 | 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 10 | 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 11 | 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, 12 | 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 13 | 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 14 | ]; 15 | // 可分配的最大数组长度,用于避免 OutOfMemoryException 16 | public static readonly maxPrimeArrayLength = 0x7FEFFFFD; 17 | 18 | /** 19 | * 判断一个数是否为质数 20 | * @param candidate 要判断的数 21 | * @returns 是否为质数 22 | */ 23 | public static isPrime(candidate: number): boolean { 24 | if ((candidate & 1) !== 0) { // 位运算判断奇偶性 25 | let limit = Math.sqrt(candidate); 26 | for (let divisor = 3; divisor <= limit; divisor += 2) { // 奇数因子判断 27 | if ((candidate % divisor) === 0) { 28 | return false; 29 | } 30 | } 31 | return true; 32 | } 33 | return (candidate === 2); // 2是质数 34 | } 35 | 36 | /** 37 | * 获取大于等于指定值的最小质数 38 | * @param min 指定值 39 | * @returns 大于等于指定值的最小质数 40 | */ 41 | public static getPrime(min: number): number { 42 | if (min < 0) { 43 | throw new Error("参数错误 min 不能小于0"); 44 | } 45 | 46 | for (let i = 0; i < this.primes.length; i++) { 47 | let prime = this.primes[i]; 48 | if (prime >= min) { 49 | return prime; 50 | } 51 | } 52 | 53 | // 在预定义的质数列表之外,需要计算最小的质数 54 | for (let i = (min | 1); i < Number.MAX_VALUE; i += 2) { // 从 min 向上计算奇数 55 | if (this.isPrime(i) && ((i - 1) % this.hashPrime !== 0)) { // i是质数且不是hashPrime的倍数 56 | return i; 57 | } 58 | } 59 | return min; 60 | } 61 | 62 | /** 63 | * 扩展哈希表容量 64 | * @param oldSize 原哈希表容量 65 | * @returns 扩展后的哈希表容量 66 | */ 67 | public static expandPrime(oldSize: number): number { 68 | let newSize = 2 * oldSize; 69 | 70 | // 在遇到容量溢出之前,允许哈希特表增长到最大可能的大小 71 | // 请注意,即使当_items.Length溢出时,这项检查也会起作用 72 | if (newSize > this.maxPrimeArrayLength && this.maxPrimeArrayLength > oldSize) { 73 | return this.maxPrimeArrayLength; 74 | } 75 | 76 | return this.getPrime(newSize); 77 | } 78 | 79 | /** 80 | * 计算字符串的哈希值 81 | * @param str 要计算哈希值的字符串 82 | * @returns 哈希值 83 | */ 84 | public static getHashCode(str: string): number { 85 | let hash = 0; 86 | if (str.length === 0) { 87 | return hash; 88 | } 89 | for (let i = 0; i < str.length; i++) { 90 | let char = str.charCodeAt(i); 91 | hash = ((hash << 5) - hash) + char; // 采用 FNV-1a 哈希算法 92 | hash = hash & hash; // 将hash值转换为32位整数 93 | } 94 | return hash; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /source/src/ECS/Utils/IdentifierPool.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class IdentifierPool { 3 | private ids: Bag; 4 | private nextAvailableId_ = 0; 5 | 6 | constructor() { 7 | this.ids = new Bag(); 8 | } 9 | 10 | public checkOut(): number { 11 | if (this.ids.size() > 0) { 12 | return this.ids.removeLast(); 13 | } 14 | 15 | return this.nextAvailableId_++; 16 | } 17 | 18 | public checkIn(id: number): void { 19 | this.ids.add(id); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /source/src/ECS/Utils/Matcher.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 定义一个实体匹配器类。 4 | */ 5 | export class Matcher { 6 | protected allSet: (new (...args: any[]) => Component)[] = []; 7 | protected exclusionSet: (new (...args: any[]) => Component)[] = []; 8 | protected oneSet: (new (...args: any[]) => Component)[] = []; 9 | 10 | public static empty() { 11 | return new Matcher(); 12 | } 13 | 14 | public getAllSet() { 15 | return this.allSet; 16 | } 17 | 18 | public getExclusionSet() { 19 | return this.exclusionSet; 20 | } 21 | 22 | public getOneSet() { 23 | return this.oneSet; 24 | } 25 | 26 | public isInterestedEntity(e: Entity) { 27 | return this.isInterested(e.componentBits); 28 | } 29 | 30 | public isInterested(components: Bits) { 31 | if (this.allSet.length !== 0) { 32 | for (let i = 0; i < this.allSet.length; i++) { 33 | const type = this.allSet[i]; 34 | if (!components.get(ComponentTypeManager.getIndexFor(type))) { 35 | return false; 36 | } 37 | } 38 | } 39 | 40 | if (this.exclusionSet.length !== 0) { 41 | for (let i = 0; i < this.exclusionSet.length; i++) { 42 | const type = this.exclusionSet[i]; 43 | if (components.get(ComponentTypeManager.getIndexFor(type))) { 44 | return false; 45 | } 46 | } 47 | } 48 | 49 | if (this.oneSet.length !== 0) { 50 | for (let i = 0; i < this.oneSet.length; i++) { 51 | const type = this.oneSet[i]; 52 | if (components.get(ComponentTypeManager.getIndexFor(type))) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * 添加所有包含的组件类型。 64 | * @param types 所有包含的组件类型列表 65 | */ 66 | public all(...types: (new (...args: any[]) => Component)[]): Matcher { 67 | this.allSet.push(...types); 68 | return this; 69 | } 70 | 71 | /** 72 | * 添加排除包含的组件类型。 73 | * @param types 排除包含的组件类型列表 74 | */ 75 | public exclude(...types: (new (...args: any[]) => Component)[]): Matcher { 76 | this.exclusionSet.push(...types); 77 | return this; 78 | } 79 | 80 | /** 81 | * 添加至少包含其中之一的组件类型。 82 | * @param types 至少包含其中之一的组件类型列表 83 | */ 84 | public one(...types: (new (...args: any[]) => Component)[]): Matcher { 85 | this.oneSet.push(...types); 86 | return this; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/src/ECS/Utils/Time.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 时间管理器,用于管理游戏中的时间相关属性 4 | */ 5 | export class Time { 6 | /** 游戏运行的总时间,单位为秒 */ 7 | public static totalTime: number = 0; 8 | 9 | /** deltaTime 的未缩放版本,不受时间尺度的影响 */ 10 | public static unscaledDeltaTime: number = 0; 11 | 12 | /** 前一帧到当前帧的时间增量,按时间刻度进行缩放 */ 13 | public static deltaTime: number = 0; 14 | 15 | /** 时间刻度缩放,可以加快或减慢游戏时间 */ 16 | public static timeScale = 1; 17 | 18 | /** DeltaTime 可以为的最大值,避免游戏出现卡顿情况 */ 19 | public static maxDeltaTime = Number.MAX_VALUE; 20 | 21 | /** 已传递的帧总数 */ 22 | public static frameCount = 0; 23 | 24 | /** 自场景加载以来的总时间,单位为秒 */ 25 | public static timeSinceSceneLoad: number = 0; 26 | 27 | /** 上一次记录的时间,用于计算两次调用 update 之间的时间差 */ 28 | private static _lastTime = -1; 29 | 30 | /** 31 | * 更新时间管理器 32 | * @param currentTime 当前时间 33 | * @param useEngineTime 是否使用引擎时间 34 | */ 35 | public static update(currentTime: number, useEngineTime: boolean) { 36 | let dt = 0; 37 | 38 | if (useEngineTime) { 39 | dt = currentTime; 40 | } else { 41 | // 如果当前时间为 -1,则表示使用系统时间 42 | if (currentTime === -1) { 43 | currentTime = Date.now(); 44 | } 45 | 46 | // 如果上一次记录的时间为 -1,则表示当前为第一次调用 update 47 | if (this._lastTime === -1) { 48 | this._lastTime = currentTime; 49 | } 50 | 51 | // 计算两次调用 update 之间的时间差,并将其转换为秒 52 | dt = (currentTime - this._lastTime) / 1000; 53 | } 54 | 55 | // 如果计算得到的时间差超过了最大时间步长,则将其限制为最大时间步长 56 | if (dt > this.maxDeltaTime) { 57 | dt = this.maxDeltaTime; 58 | } 59 | 60 | // 更新时间管理器的各个属性 61 | this.totalTime += dt; 62 | this.deltaTime = dt * this.timeScale; 63 | this.unscaledDeltaTime = dt; 64 | this.timeSinceSceneLoad += dt; 65 | this.frameCount++; 66 | 67 | // 记录当前时间,以备下一次调用 update 使用 68 | this._lastTime = currentTime; 69 | } 70 | 71 | 72 | public static sceneChanged() { 73 | this.timeSinceSceneLoad = 0; 74 | } 75 | 76 | /** 77 | * 检查指定时间间隔是否已过去 78 | * @param interval 指定时间间隔 79 | * @returns 是否已过去指定时间间隔 80 | */ 81 | public static checkEvery(interval: number): boolean { 82 | // 计算当前时刻所经过的完整时间间隔个数(向下取整) 83 | const passedIntervals = Math.floor(this.timeSinceSceneLoad / interval); 84 | 85 | // 计算上一帧到当前帧经过的时间所包含的时间间隔个数(向下取整) 86 | const deltaIntervals = Math.floor(this.deltaTime / interval); 87 | 88 | // 如果当前时刻所经过的时间间隔数比上一帧所经过的时间间隔数多,则说明时间间隔已过去 89 | return passedIntervals > deltaIntervals; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /source/src/Math/BezierSpline.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 提供了一系列立方贝塞尔点,并提供了帮助方法来访问贝塞尔 4 | */ 5 | export class BezierSpline { 6 | public _points: Vector2[] = []; 7 | public _curveCount: number = 0; 8 | 9 | /** 10 | * 在这个过程中,t被修改为在曲线段的范围内。 11 | * @param t 12 | */ 13 | public pointIndexAtTime(t: number): {time: number, range: number} { 14 | const res = {time: 0, range: 0}; 15 | if (t >= 1) { 16 | t = 1; 17 | res.range = this._points.length - 4; 18 | } else { 19 | t = MathHelper.clamp01(t) * this._curveCount; 20 | res.range = MathHelper.toInt(t); 21 | t -= res.range; 22 | res.range *= 3; 23 | } 24 | 25 | res.time = t; 26 | return res; 27 | } 28 | 29 | /** 30 | * 设置一个控制点,考虑到这是否是一个共享点,如果是,则适当调整 31 | * @param index 32 | * @param point 33 | */ 34 | public setControlPoint(index: number, point: Vector2) { 35 | if (index % 3 == 0) { 36 | const delta = point.sub(this._points[index]); 37 | if (index > 0) 38 | this._points[index - 1].addEqual(delta); 39 | 40 | if (index + 1 < this._points.length) 41 | this._points[index + 1].addEqual(delta); 42 | } 43 | 44 | this._points[index] = point; 45 | } 46 | 47 | /** 48 | * 得到时间t的贝塞尔曲线上的点 49 | * @param t 50 | */ 51 | public getPointAtTime(t: number): Vector2{ 52 | const res = this.pointIndexAtTime(t); 53 | const i = res.range; 54 | return Bezier.getPointThree(this._points[i], this._points[i + 1], this._points[i + 2], 55 | this._points[i + 3], t); 56 | } 57 | 58 | /** 59 | * 得到贝塞尔在时间t的速度(第一导数) 60 | * @param t 61 | */ 62 | public getVelocityAtTime(t: number): Vector2 { 63 | const res = this.pointIndexAtTime(t); 64 | const i = res.range; 65 | return Bezier.getFirstDerivativeThree(this._points[i], this._points[i + 1], this._points[i + 2], 66 | this._points[i + 3], t); 67 | } 68 | 69 | /** 70 | * 得到时间t时贝塞尔的方向(归一化第一导数) 71 | * @param t 72 | */ 73 | public getDirectionAtTime(t: number) { 74 | return this.getVelocityAtTime(t).normalize(); 75 | } 76 | 77 | /** 78 | * 在贝塞尔曲线上添加一条曲线 79 | * @param start 80 | * @param firstControlPoint 81 | * @param secondControlPoint 82 | * @param end 83 | */ 84 | public addCurve(start: Vector2, firstControlPoint: Vector2, secondControlPoint: Vector2, end: Vector2) { 85 | // 只有当这是第一条曲线时,我们才会添加起始点。对于其他所有的曲线,前一个曲线的终点应该等于新曲线的起点。 86 | if (this._points.length == 0) 87 | this._points.push(start); 88 | 89 | this._points.push(firstControlPoint); 90 | this._points.push(secondControlPoint); 91 | this._points.push(end); 92 | 93 | this._curveCount = (this._points.length - 1) / 3; 94 | } 95 | 96 | /** 97 | * 重置bezier,移除所有点 98 | */ 99 | public reset() { 100 | this._points.length = 0; 101 | } 102 | 103 | /** 104 | * 将splitine分解成totalSegments部分,并返回使用线条绘制所需的所有点 105 | * @param totalSegments 106 | */ 107 | public getDrawingPoints(totalSegments: number): Vector2[] { 108 | let points: Vector2[] = []; 109 | for (let i = 0; i < totalSegments; i ++) { 110 | let t = i / totalSegments; 111 | points[i] = this.getPointAtTime(t); 112 | } 113 | 114 | return points; 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /source/src/Math/Flags.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 一个用于操作二进制标志(也称为位字段) 4 | */ 5 | export class Flags { 6 | /** 7 | * 检查指定二进制数字中是否已设置了指定标志位 8 | * @param self 二进制数字 9 | * @param flag 标志位,应该为2的幂 10 | * @returns 如果设置了指定的标志位,则返回true,否则返回false 11 | */ 12 | public static isFlagSet(self: number, flag: number): boolean { 13 | return (self & flag) !== 0; 14 | } 15 | 16 | /** 17 | * 检查指定二进制数字中是否已设置未移位的指定标志位 18 | * @param self 二进制数字 19 | * @param flag 标志位,不应移位(应为2的幂) 20 | * @returns 如果设置了指定的标志位,则返回true,否则返回false 21 | */ 22 | public static isUnshiftedFlagSet(self: number, flag: number): boolean { 23 | flag = 1 << flag; 24 | return (self & flag) !== 0; 25 | } 26 | 27 | /** 28 | * 将指定的标志位设置为二进制数字的唯一标志 29 | * @param self 二进制数字 30 | * @param flag 标志位,应该为2的幂 31 | */ 32 | public static setFlagExclusive(self: Ref, flag: number) { 33 | self.value = 1 << flag; 34 | } 35 | 36 | /** 37 | * 将指定的标志位设置为二进制数字 38 | * @param self 二进制数字的引用 39 | * @param flag 标志位,应该为2的幂 40 | */ 41 | public static setFlag(self: Ref, flag: number) { 42 | self.value |= 1 << flag; 43 | } 44 | 45 | /** 46 | * 将指定的标志位从二进制数字中取消设置 47 | * @param self 二进制数字的引用 48 | * @param flag 标志位,应该为2的幂 49 | */ 50 | public static unsetFlag(self: Ref, flag: number) { 51 | flag = 1 << flag; 52 | self.value &= ~flag; 53 | } 54 | 55 | /** 56 | * 反转二进制数字中的所有位(将1变为0,将0变为1) 57 | * @param self 二进制数字的引用 58 | */ 59 | public static invertFlags(self: Ref) { 60 | self.value = ~self.value; 61 | } 62 | 63 | /** 64 | * 返回二进制数字的字符串表示形式(以二进制形式) 65 | * @param self 二进制数字 66 | * @param leftPadWidth 返回的字符串的最小宽度(在左侧填充0) 67 | * @returns 二进制数字的字符串表示形式 68 | */ 69 | public static binaryStringRepresentation( 70 | self: number, 71 | leftPadWidth = 10 72 | ): string { 73 | let str = self.toString(2); 74 | while (str.length < (leftPadWidth || 2)) { 75 | str = "0" + str; 76 | } 77 | return str; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/src/Math/MatrixHelper.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class MatrixHelper { 3 | /** 4 | * 该静态方法用于计算两个 Matrix2D 对象的和。 5 | * @param {Matrix2D} matrix1 - 加数矩阵。 6 | * @param {Matrix2D} matrix2 - 加数矩阵。 7 | * @returns {Matrix2D} - 计算结果的 Matrix2D 对象。 8 | */ 9 | public static add(matrix1: Matrix2D, matrix2: Matrix2D): Matrix2D { 10 | // 创建一个新的 Matrix2D 对象以存储计算结果。 11 | const result = new Matrix2D(); 12 | 13 | // 计算两个矩阵的和。 14 | result.m11 = matrix1.m11 + matrix2.m11; 15 | result.m12 = matrix1.m12 + matrix2.m12; 16 | result.m21 = matrix1.m21 + matrix2.m21; 17 | result.m22 = matrix1.m22 + matrix2.m22; 18 | result.m31 = matrix1.m31 + matrix2.m31; 19 | result.m32 = matrix1.m32 + matrix2.m32; 20 | 21 | // 返回计算结果的 Matrix2D 对象。 22 | return result; 23 | } 24 | 25 | /** 26 | * 该静态方法用于计算两个 Matrix2D 对象的商。 27 | * @param {Matrix2D} matrix1 - 被除数矩阵。 28 | * @param {Matrix2D} matrix2 - 除数矩阵。 29 | * @returns {Matrix2D} - 计算结果的 Matrix2D 对象。 30 | */ 31 | public static divide(matrix1: Matrix2D, matrix2: Matrix2D): Matrix2D { 32 | // 创建一个新的 Matrix2D 对象以存储计算结果。 33 | const result = new Matrix2D(); 34 | 35 | // 计算两个矩阵的商。 36 | result.m11 = matrix1.m11 / matrix2.m11; 37 | result.m12 = matrix1.m12 / matrix2.m12; 38 | result.m21 = matrix1.m21 / matrix2.m21; 39 | result.m22 = matrix1.m22 / matrix2.m22; 40 | result.m31 = matrix1.m31 / matrix2.m31; 41 | result.m32 = matrix1.m32 / matrix2.m32; 42 | 43 | // 返回计算结果的 Matrix2D 对象。 44 | return result; 45 | } 46 | 47 | /** 48 | * 该静态方法用于计算两个 Matrix2D 对象或一个 Matrix2D 对象和一个数字的乘积。 49 | * @param {Matrix2D} matrix1 - 第一个矩阵。 50 | * @param {Matrix2D | number} matrix2 - 第二个矩阵或一个数字。 51 | * @returns {Matrix2D} - 计算结果的 Matrix2D 对象。 52 | */ 53 | public static multiply(matrix1: Matrix2D, matrix2: Matrix2D | number): Matrix2D { 54 | // 创建一个新的 Matrix2D 对象以存储计算结果。 55 | const result = new Matrix2D(); 56 | 57 | // 根据第二个参数的类型执行不同的计算。 58 | if (matrix2 instanceof Matrix2D) { 59 | // 执行矩阵乘法。 60 | result.m11 = matrix1.m11 * matrix2.m11 + matrix1.m12 * matrix2.m21; 61 | result.m12 = matrix1.m11 * matrix2.m12 + matrix1.m12 * matrix2.m22; 62 | result.m21 = matrix1.m21 * matrix2.m11 + matrix1.m22 * matrix2.m21; 63 | result.m22 = matrix1.m21 * matrix2.m12 + matrix1.m22 * matrix2.m22; 64 | result.m31 = matrix1.m31 * matrix2.m11 + matrix1.m32 * matrix2.m21 + matrix2.m31; 65 | result.m32 = matrix1.m31 * matrix2.m12 + matrix1.m32 * matrix2.m22 + matrix2.m32; 66 | } else { 67 | // 执行矩阵和标量的乘法。 68 | result.m11 = matrix1.m11 * matrix2; 69 | result.m12 = matrix1.m12 * matrix2; 70 | result.m21 = matrix1.m21 * matrix2; 71 | result.m22 = matrix1.m22 * matrix2; 72 | result.m31 = matrix1.m31 * matrix2; 73 | result.m32 = matrix1.m32 * matrix2; 74 | } 75 | 76 | // 返回计算结果的 Matrix2D 对象。 77 | return result; 78 | } 79 | 80 | /** 81 | * 该静态方法用于计算两个 Matrix2D 对象的差。 82 | * @param {Matrix2D} matrix1 - 第一个矩阵。 83 | * @param {Matrix2D} matrix2 - 第二个矩阵。 84 | * @returns {Matrix2D} - 计算结果的 Matrix2D 对象。 85 | */ 86 | public static subtract(matrix1: Matrix2D, matrix2: Matrix2D): Matrix2D { 87 | // 创建一个新的 Matrix2D 对象以存储计算结果。 88 | const result = new Matrix2D(); 89 | 90 | // 计算两个矩阵的差。 91 | result.m11 = matrix1.m11 - matrix2.m11; 92 | result.m12 = matrix1.m12 - matrix2.m12; 93 | result.m21 = matrix1.m21 - matrix2.m21; 94 | result.m22 = matrix1.m22 - matrix2.m22; 95 | result.m31 = matrix1.m31 - matrix2.m31; 96 | result.m32 = matrix1.m32 - matrix2.m32; 97 | 98 | // 返回计算结果的 Matrix2D 对象。 99 | return result; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /source/src/Math/SubpixelFloat.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 该类用于存储具有亚像素分辨率的浮点数。 4 | */ 5 | export class SubpixelFloat { 6 | /** 7 | * 存储 SubpixelFloat 值的浮点余数。 8 | */ 9 | public remainder: number = 0; 10 | 11 | /** 12 | * 通过将给定数量的像素添加到余数中来更新 SubpixelFloat 值。 13 | * 返回更新后的整数部分,余数表示当前值中包含的亚像素部分。 14 | * @param {number} amount - 要添加到余数中的像素数。 15 | * @returns {number} 更新后的整数部分。 16 | */ 17 | public update(amount: number): number { 18 | // 将给定的像素数量添加到余数中 19 | this.remainder += amount; 20 | 21 | // 检查余数是否超过一个像素的大小 22 | let motion = Math.trunc(this.remainder); 23 | this.remainder -= motion; 24 | 25 | // 返回整数部分作为更新后的值 26 | return motion; 27 | } 28 | 29 | /** 30 | * 将 SubpixelFloat 值重置为零。 31 | */ 32 | public reset(): void { 33 | this.remainder = 0; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /source/src/Math/SubpixelVector2.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 该类用于存储具有亚像素分辨率的二维向量。 4 | */ 5 | export class SubpixelVector2 { 6 | /** 7 | * 用于存储 x 坐标的 SubpixelFloat 对象。 8 | */ 9 | public _x: SubpixelFloat = new SubpixelFloat(); 10 | 11 | /** 12 | * 用于存储 y 坐标的 SubpixelFloat 对象。 13 | */ 14 | public _y: SubpixelFloat = new SubpixelFloat(); 15 | 16 | /** 17 | * 通过将给定数量的像素添加到余数中来更新 SubpixelVector2 值。 18 | * @param {Vector2} amount - 要添加到余数中的像素向量。 19 | */ 20 | public update(amount: Vector2): void { 21 | // 更新 x 和 y 坐标 22 | amount.x = this._x.update(amount.x); 23 | amount.y = this._y.update(amount.y); 24 | } 25 | 26 | /** 27 | * 将 SubpixelVector2 值的余数重置为零。 28 | */ 29 | public reset(): void { 30 | this._x.reset(); 31 | this._y.reset(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /source/src/Physics/Ray2D.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 不是真正的射线(射线只有开始和方向),作为一条线和射线。 4 | */ 5 | export class Ray2D { 6 | public get start(): Vector2 { 7 | return this._start; 8 | } 9 | 10 | public get direction(): Vector2 { 11 | return this._direction; 12 | } 13 | 14 | public get end(): Vector2 { 15 | return this._end; 16 | } 17 | 18 | constructor(pos: Vector2, end: Vector2) { 19 | this._start = pos.clone(); 20 | this._end = end.clone(); 21 | this._direction = this._end.sub(this._start); 22 | } 23 | 24 | private _start: Vector2; 25 | private _direction: Vector2; 26 | private _end: Vector2; 27 | } 28 | } -------------------------------------------------------------------------------- /source/src/Physics/RaycastHit.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class RaycastHit { 3 | /** 4 | * 对撞机被射线击中 5 | */ 6 | public collider: Collider; 7 | 8 | /** 9 | * 撞击发生时沿射线的距离。 10 | */ 11 | public fraction: number = 0; 12 | 13 | /** 14 | * 从射线原点到碰撞点的距离 15 | */ 16 | public distance: number = 0; 17 | 18 | /** 19 | * 世界空间中光线击中对撞机表面的点 20 | */ 21 | public point: Vector2 = Vector2.zero; 22 | 23 | /** 24 | * 被射线击中的表面的法向量 25 | */ 26 | public normal: Vector2 = Vector2.zero; 27 | 28 | /** 29 | * 用于执行转换的质心。使其接触的形状的位置。 30 | */ 31 | public centroid: Vector2; 32 | 33 | constructor(collider?: Collider, fraction?: number, distance?: number, point?: Vector2, normal?: Vector2) { 34 | this.collider = collider; 35 | this.fraction = fraction; 36 | this.distance = distance; 37 | this.point = point; 38 | this.centroid = Vector2.zero; 39 | } 40 | 41 | public setAllValues(collider: Collider, fraction: number, distance: number, point: Vector2, normal: Vector2) { 42 | this.collider = collider; 43 | this.fraction = fraction; 44 | this.distance = distance; 45 | this.point = point; 46 | this.normal = normal; 47 | } 48 | 49 | public setValues(fraction: number, distance: number, point: Vector2, normal: Vector2) { 50 | this.fraction = fraction; 51 | this.distance = distance; 52 | this.point = point; 53 | this.normal = normal; 54 | } 55 | 56 | public reset() { 57 | this.collider = null; 58 | this.fraction = this.distance = 0; 59 | } 60 | 61 | public clone(): RaycastHit { 62 | const hit = new RaycastHit(); 63 | hit.setAllValues( 64 | this.collider, 65 | this.fraction, 66 | this.distance, 67 | this.point, 68 | this.normal 69 | ); 70 | return hit; 71 | } 72 | 73 | public toString() { 74 | return `[RaycastHit] fraction: ${this.fraction}, distance: ${this.distance}, normal: ${this.normal}, centroid: ${this.centroid}, point: ${this.point}`; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/Circle.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | export class Circle extends Shape { 4 | public radius: number; 5 | public _originalRadius: number; 6 | 7 | constructor(radius: number) { 8 | super(); 9 | this.radius = radius; 10 | this._originalRadius = radius; 11 | } 12 | 13 | public recalculateBounds(collider: Collider) { 14 | // 如果我们没有旋转或不关心TRS我们使用localOffset作为中心 15 | this.center = collider.localOffset; 16 | 17 | if (collider.shouldColliderScaleAndRotateWithTransform) { 18 | // 我们只将直线缩放为一个圆,所以我们将使用最大值 19 | const scale = collider.entity.transform.scale; 20 | const hasUnitScale = scale.x === 1 && scale.y === 1; 21 | const maxScale = Math.max(scale.x, scale.y); 22 | this.radius = this._originalRadius * maxScale; 23 | 24 | if (collider.entity.transform.rotation !== 0) { 25 | // 为了处理偏移原点的旋转,我们只需要将圆心围绕(0,0)在一个圆上移动,我们的偏移量就是0角 26 | const offsetAngle = Math.atan2(collider.localOffset.y, collider.localOffset.x) * MathHelper.Rad2Deg; 27 | const offsetLength = hasUnitScale ? collider._localOffsetLength : collider.localOffset.multiply(collider.entity.transform.scale).magnitude(); 28 | this.center = MathHelper.pointOnCircle(Vector2.zero, offsetLength, collider.entity.transform.rotation + offsetAngle); 29 | } 30 | } 31 | 32 | this.position = collider.transform.position.add(this.center); 33 | this.bounds = new Rectangle(this.position.x - this.radius, this.position.y - this.radius, this.radius * 2, this.radius * 2); 34 | } 35 | 36 | public overlaps(other: Shape) { 37 | const result = new Out(); 38 | if (other instanceof Box && (other as Box).isUnrotated) 39 | return Collisions.rectToCircle(other.bounds, this.position, this.radius); 40 | 41 | if (other instanceof Circle) 42 | return Collisions.circleToCircle(this.position, this.radius, other.position, (other as Circle).radius); 43 | 44 | if (other instanceof Polygon) 45 | return ShapeCollisionsCircle.circleToPolygon(this, other, result); 46 | 47 | throw new Error(`overlaps of circle to ${other} are not supported`); 48 | } 49 | 50 | public collidesWithShape(other: Shape, result: Out): boolean { 51 | if (other instanceof Box && (other as Box).isUnrotated) { 52 | return ShapeCollisionsCircle.circleToBox(this, other, result); 53 | } 54 | 55 | if (other instanceof Circle) { 56 | return ShapeCollisionsCircle.circleToCircle(this, other, result); 57 | } 58 | 59 | if (other instanceof Polygon) { 60 | return ShapeCollisionsCircle.circleToPolygon(this, other, result); 61 | } 62 | 63 | throw new Error(`Collisions of Circle to ${other} are not supported`); 64 | } 65 | 66 | public collidesWithLine(start: Vector2, end: Vector2, hit: Out): boolean { 67 | return ShapeCollisionsLine.lineToCircle(start, end, this, hit); 68 | } 69 | 70 | public getPointAlongEdge(angle: number): Vector2 { 71 | return new Vector2( 72 | this.position.x + this.radius * Math.cos(angle), 73 | this.position.y + this.radius * Math.sin(angle) 74 | ); 75 | } 76 | 77 | /** 78 | * 获取所提供的点是否在此范围内 79 | * @param point 80 | */ 81 | public containsPoint(point: Vector2) { 82 | return (point.sub(this.position)).lengthSquared() <= this.radius * this.radius; 83 | } 84 | 85 | public pointCollidesWithShape(point: Vector2, result: Out): boolean { 86 | return ShapeCollisionsPoint.pointToCircle(point, this, result); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/src/Physics/Shapes/CollisionResult.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class CollisionResult { 3 | /** 4 | * 与之相撞的对撞机 5 | */ 6 | public collider: Collider; 7 | /** 8 | * 被形状击中的表面的法向量 9 | */ 10 | public normal: Vector2 = Vector2.zero; 11 | /** 12 | * 应用于第一个形状以推入形状的转换 13 | */ 14 | public minimumTranslationVector: Vector2 = Vector2.zero; 15 | /** 16 | * 不是所有冲突类型都使用!在依赖这个字段之前,请检查ShapeCollisions切割类! 17 | */ 18 | public point: Vector2 = Vector2.zero; 19 | 20 | public reset() { 21 | this.collider = null; 22 | this.normal.setTo(0, 0); 23 | this.minimumTranslationVector.setTo(0, 0); 24 | if (this.point) { 25 | this.point.setTo(0, 0); 26 | } 27 | } 28 | 29 | public cloneTo(cr: CollisionResult) { 30 | cr.collider = this.collider; 31 | cr.normal.setTo(this.normal.x, this.normal.y); 32 | cr.minimumTranslationVector.setTo( 33 | this.minimumTranslationVector.x, 34 | this.minimumTranslationVector.y 35 | ); 36 | if (this.point) { 37 | if (!cr.point) { 38 | cr.point = new Vector2(0, 0); 39 | } 40 | cr.point.setTo(this.point.x, this.point.y); 41 | } 42 | } 43 | 44 | /** 45 | * 从移动向量中移除水平方向的位移,以确保形状只沿垂直方向运动。如果移动向量包含水平移动,则通过计算垂直位移来修复响应距离。 46 | * @param deltaMovement - 移动向量 47 | */ 48 | public removeHorizontalTranslation(deltaMovement: Vector2) { 49 | // 如果运动方向和法线方向不在同一方向或者移动量为0且法线方向不为0,则需要修复 50 | if (Math.sign(this.normal.x) !== Math.sign(deltaMovement.x) || (deltaMovement.x === 0 && this.normal.x !== 0)) { 51 | // 获取响应距离 52 | const responseDistance = this.minimumTranslationVector.magnitude(); 53 | // 计算需要修复的位移 54 | const fix = responseDistance / this.normal.y; 55 | 56 | // 如果法线方向不是完全水平或垂直,并且修复距离小于移动向量的3倍,则修复距离 57 | if (Math.abs(this.normal.x) != 1 && Math.abs(fix) < Math.abs(deltaMovement.y * 3)) { 58 | this.minimumTranslationVector = new Vector2(0, -fix); 59 | } 60 | } 61 | } 62 | 63 | public invertResult() { 64 | this.minimumTranslationVector = this.minimumTranslationVector.negate(); 65 | this.normal = this.normal.negate(); 66 | } 67 | 68 | public toString() { 69 | return `[CollisionResult] normal: ${this.normal}, minimumTranslationVector: ${this.minimumTranslationVector}`; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/src/Physics/Shapes/Line.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Line { 3 | public start: Vector2; 4 | public end: Vector2; 5 | 6 | constructor(start: Vector2, end: Vector2) { 7 | this.start = start.clone(); 8 | this.end = end.clone(); 9 | } 10 | 11 | public get direction(): Vector2 { 12 | return this.end.sub(this.start).normalize(); 13 | } 14 | 15 | public getNormal(): Vector2 { 16 | const angle = this.direction.getAngle() - Math.PI / 2; 17 | return new Vector2(Math.cos(angle), Math.sin(angle)); 18 | } 19 | 20 | public getDirection(out: Vector2) { 21 | return out.copyFrom(this.end).sub(this.start).normalize(); 22 | } 23 | 24 | public getLength() { 25 | return this.start.getDistance(this.end); 26 | } 27 | 28 | public getLengthSquared() { 29 | return this.start.getDistanceSquared(this.end); 30 | } 31 | 32 | public distanceToPoint(normal: Vector2, center: Vector2): number { 33 | return Math.abs((this.end.y - this.start.y) * normal.x - (this.end.x - this.start.x) * normal.y + this.end.x * this.start.y - this.end.y * this.start.x) / (2 * normal.magnitude()); 34 | } 35 | 36 | public getFurthestPoint(direction: Vector2): Vector2 { 37 | const d1 = this.start.dot(direction); 38 | const d2 = this.end.dot(direction); 39 | return d1 > d2 ? this.start : this.end; 40 | } 41 | 42 | public getClosestPoint(point: Vector2, out: Vector2) { 43 | const delta = out.copyFrom(this.end).sub(this.start); 44 | const t = (point.sub(this.start)).dot(delta) / delta.lengthSquared(); 45 | 46 | if (t < 0) { 47 | return out.copyFrom(this.start); 48 | } else if (t > 1) { 49 | return out.copyFrom(this.end); 50 | } 51 | 52 | return out.copyFrom(delta).multiplyScaler(t).add(this.start); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/Projection.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 计算投影和重叠区域 4 | */ 5 | export class Projection { 6 | public min: number; 7 | public max: number; 8 | 9 | constructor() { 10 | this.min = Number.MAX_VALUE; 11 | this.max = -Number.MAX_VALUE; 12 | } 13 | 14 | public project(axis: Vector2, polygon: Polygon) { 15 | const points = polygon.points; 16 | let min = axis.dot(points[0]); 17 | let max = min; 18 | 19 | for (let i = 1; i < points.length; i++) { 20 | const p = points[i]; 21 | const dot = axis.dot(p); 22 | if (dot < min) { 23 | min = dot; 24 | } else if (dot > max) { 25 | max = dot; 26 | } 27 | } 28 | 29 | this.min = min; 30 | this.max = max; 31 | } 32 | 33 | public overlap(other: Projection): boolean { 34 | return this.max >= other.min && other.max >= this.min; 35 | } 36 | 37 | public getOverlap(other: Projection): number { 38 | return Math.min(this.max, other.max) - Math.max(this.min, other.min); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/RealtimeCollisions.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class RealtimeCollisions { 3 | /** 4 | * 判断移动的圆是否与矩形相交,并返回相撞的时间。 5 | * @param s 移动的圆 6 | * @param b 矩形 7 | * @param movement 移动的向量 8 | * @param time 时间 9 | * @returns 是否相撞 10 | */ 11 | public static intersectMovingCircleBox(s: Circle, b: Box, movement: Vector2, time: number): boolean { 12 | // 计算将b按球面半径r扩大后的AABB 13 | const e = b.bounds; 14 | e.inflate(s.radius, s.radius); 15 | 16 | // 将射线与展开的矩形e相交,如果射线错过了e,则以无交点退出,否则得到交点p和时间t作为结果。 17 | const ray = new Ray2D(s.position.sub(movement), s.position); // 创建射线,将移动后的圆心作为起点 18 | const res = e.rayIntersects(ray); // 判断射线与展开的矩形是否相交 19 | if (!res.intersected && res.distance > 1) 20 | return false; // 如果没有相交且距离大于1,则无法相撞 21 | 22 | // 求交点 23 | const point = ray.start.add(ray.direction.scale(time)); 24 | 25 | // 计算交点p位于b的哪个最小面和最大面之外。注意,u和v不能有相同的位集,它们之间必须至少有一个位集。 26 | let u: number, v: number = 0; 27 | if (point.x < b.bounds.left) 28 | u |= 1; // 如果交点在最小的x面的左边,将第一位设为1 29 | if (point.x > b.bounds.right) 30 | v |= 1; // 如果交点在最大的x面的右边,将第一位设为1 31 | if (point.y < b.bounds.top) 32 | u |= 2; // 如果交点在最小的y面的上面,将第二位设为1 33 | if (point.y > b.bounds.bottom) 34 | v |= 2; // 如果交点在最大的y面的下面,将第二位设为1 35 | 36 | // 'or'将所有的比特集合在一起,形成一个比特掩码(注意u + v == u | v) 37 | const m = u + v; 38 | 39 | // 如果这3个比特都被设置,那么该点就在顶点区域内。 40 | if (m == 3) { 41 | // 如果有一条或多条命中,则必须在两条边的顶点相交,并返回最佳时间。 42 | console.log(`m == 3. corner ${Time.frameCount}`); 43 | } 44 | 45 | // 如果在m中只设置了一个位,那么该点就在一个面的区域。 46 | if ((m & (m - 1)) == 0) { 47 | // 从扩大的矩形交点开始的时间就是正确的时间 48 | return true; 49 | } 50 | 51 | // 点在边缘区域,与边缘相交。 52 | return true; 53 | } 54 | 55 | 56 | /** 57 | * 返回矩形的第n个角的坐标。 58 | * @param b 矩形 59 | * @param n 第n个角的编号 60 | * @returns 第n个角的坐标 61 | */ 62 | public static corner(b: Rectangle, n: number): es.Vector2 { 63 | let p = es.Vector2.zero; 64 | p.x = (n & 1) === 0 ? b.right : b.left; 65 | p.y = (n & 1) === 0 ? b.bottom : b.top; 66 | return p; 67 | } 68 | 69 | /** 70 | * 测试一个圆和一个矩形是否相交,并返回是否相交。 71 | * @param circle 圆 72 | * @param box 矩形 73 | * @param point 离圆心最近的点 74 | * @returns 是否相交 75 | */ 76 | public static testCircleBox(circle: Circle, box: Box, point: Vector2): boolean { 77 | // 找出离圆心最近的点 78 | point = box.bounds.getClosestPointOnRectangleToPoint(circle.position); 79 | 80 | // 如果圆心到点的距离小于等于圆的半径,则圆和方块相交 81 | const v = point.sub(circle.position); 82 | const dist = v.dot(v); 83 | 84 | return dist <= circle.radius * circle.radius; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/Shape.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export abstract class Shape { 3 | public position: Vector2; // 形状的位置,表示形状在 2D 空间中的位置。 4 | public center: Vector2; // 形状的中心,表示形状的重心或几何中心。 5 | public bounds: Rectangle; // 形状的边界矩形,表示形状所占用的矩形区域。 6 | 7 | /** 8 | * 根据形状的碰撞器重新计算形状的边界。 9 | * @param {Collider} collider - 用于重新计算形状边界的碰撞器。 10 | */ 11 | public abstract recalculateBounds(collider: Collider); 12 | 13 | /** 14 | * 确定形状是否与另一个形状重叠。 15 | * @param {Shape} other - 要检查重叠的形状。 16 | * @returns {boolean} 如果形状重叠,则为 true;否则为 false。 17 | */ 18 | public abstract overlaps(other: Shape): boolean; 19 | 20 | /** 21 | * 确定形状是否与另一个形状碰撞。 22 | * @param {Shape} other - 要检查碰撞的形状。 23 | * @param {Out} collisionResult - 如果形状碰撞,则要填充的碰撞结果对象。 24 | * @returns {boolean} 如果形状碰撞,则为 true;否则为 false。 25 | */ 26 | public abstract collidesWithShape(other: Shape, collisionResult: Out): boolean; 27 | 28 | /** 29 | * 确定形状是否与线段相交。 30 | * @param {Vector2} start - 线段的起点。 31 | * @param {Vector2} end - 线段的终点。 32 | * @param {Out} hit - 如果形状与线段相交,则要填充的射线命中结果对象。 33 | * @returns {boolean} 如果形状与线段相交,则为 true;否则为 false。 34 | */ 35 | public abstract collidesWithLine(start: Vector2, end: Vector2, hit: Out): boolean; 36 | 37 | /** 38 | * 确定形状是否包含一个点。 39 | * @param {Vector2} point - 要检查包含的点。 40 | */ 41 | public abstract containsPoint(point: Vector2); 42 | 43 | /** 44 | * 确定一个点是否与形状相交。 45 | * @param {Vector2} point - 要检查与形状相交的点。 46 | * @param {Out} result - 如果点与形状相交,则要填充的碰撞结果对象。 47 | * @returns {boolean} 如果点与形状相交,则为 true;否则为 false。 48 | */ 49 | public abstract pointCollidesWithShape(point: Vector2, result: Out): boolean; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/src/Physics/Shapes/ShapeCollisions/ShapeCollisionSector.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ShapeCollisionSector { 3 | public static sectorToPolygon(first: Sector, second: Polygon, result: Out): boolean { 4 | const numPoints = second.points.length; 5 | let collision = false; 6 | const edgeStart = new Vector2(); 7 | const edgeEnd = new Vector2(); 8 | const hit = new Out(); 9 | 10 | for (let i = 0; i < numPoints; i++) { 11 | const point = second.points[i]; 12 | if (first.containsPoint(point)) { 13 | if (result) { 14 | result.value = new CollisionResult(); 15 | result.value.point = point.clone(); 16 | result.value.normal = point.sub(first.center).normalize(); 17 | } 18 | collision = true; 19 | break; 20 | } 21 | } 22 | 23 | if (!collision && second.containsPoint(first.center)) { 24 | if (result) { 25 | result.value = new CollisionResult(); 26 | result.value.point = first.center.clone(); 27 | result.value.normal = new Vector2(0, 0); 28 | } 29 | collision = true; 30 | } 31 | 32 | if (!collision) { 33 | for (let i = 0; i < numPoints; i++) { 34 | const p1 = second.points[i]; 35 | const p2 = second.points[(i + 1) % numPoints]; 36 | edgeStart.copyFrom(p1); 37 | edgeEnd.copyFrom(p2); 38 | 39 | if (first.collidesWithLine(edgeStart, edgeEnd, hit)) { 40 | if (result) { 41 | result.value = new CollisionResult(); 42 | result.value.point.copyFrom(hit.value.point); 43 | result.value.normal.copyFrom(hit.value.normal); 44 | } 45 | collision = true; 46 | break; 47 | } 48 | } 49 | } 50 | 51 | return collision; 52 | } 53 | 54 | public static sectorToCircle(first: Sector, second: Circle, result: Out): boolean { 55 | const radiusSquared = second.radius * second.radius; 56 | const distanceSquared = first.center.getDistanceSquared(second.center); 57 | const angleDiff = Math.abs(second.center.sub(first.center).getAngle() - first.getAngle()); 58 | const sectorAngle = first.endAngle - first.startAngle; 59 | 60 | if (distanceSquared <= radiusSquared && angleDiff <= sectorAngle / 2) { 61 | if (result) { 62 | result.value = new CollisionResult(); 63 | result.value.normal = second.center.sub(first.center).normalize(); 64 | result.value.point = second.center.clone().add(result.value.normal.clone().multiplyScaler(second.radius)); 65 | } 66 | return true; 67 | } 68 | 69 | if (result) { 70 | result.value = null; 71 | } 72 | return false; 73 | } 74 | 75 | 76 | public static sectorToBox(first: Sector, second: Box, result: Out): boolean { 77 | result.value = new CollisionResult(); 78 | 79 | // 获取box的四条边 80 | let boxEdges = second.getEdges(); 81 | 82 | // 遍历box的每一条边 83 | for (let i = 0; i < boxEdges.length; i++) { 84 | let normal = boxEdges[i].getNormal(); 85 | let furthestPointBox = second.getFurthestPoint(normal); 86 | let furthestPointSector = first.getFurthestPoint(normal.negate()); 87 | 88 | let distance = normal.dot(furthestPointSector.sub(furthestPointBox)); 89 | 90 | // 没有相交 91 | if (distance > 0) 92 | return false; 93 | 94 | if (result.value && Math.abs(distance) < result.value.minimumTranslationVector.getLength()) { 95 | result.value.minimumTranslationVector = normal.clone().multiplyScaler(distance); 96 | result.value.normal = normal; 97 | } 98 | } 99 | 100 | return true; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/ShapeCollisions/ShapeCollisionsBox.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ShapeCollisionsBox { 3 | public static boxToBox(first: Box, second: Box, result: Out): boolean { 4 | result.value = new CollisionResult(); 5 | 6 | const minkowskiDiff = this.minkowskiDifference(first, second); 7 | if (minkowskiDiff.contains(0, 0)) { 8 | // 计算MTV。如果它是零,我们就可以称它为非碰撞 9 | result.value.minimumTranslationVector = minkowskiDiff.getClosestPointOnBoundsToOrigin(); 10 | 11 | if (result.value.minimumTranslationVector.equals(Vector2.zero)) 12 | return false; 13 | 14 | result.value.normal = result.value.minimumTranslationVector.scale(-1); 15 | result.value.normal = result.value.normal.normalize(); 16 | 17 | return true; 18 | } 19 | 20 | return false; 21 | } 22 | 23 | /** 24 | * 用second检查被deltaMovement移动的框的结果 25 | * @param first 26 | * @param second 27 | * @param movement 28 | * @param hit 29 | */ 30 | public static boxToBoxCast(first: Box, second: Box, movement: Vector2, hit: RaycastHit): boolean { 31 | // 首先,我们检查是否有重叠。如果有重叠,我们就不做扫描测试 32 | const minkowskiDiff = this.minkowskiDifference(first, second); 33 | if (minkowskiDiff.contains(0, 0)) { 34 | // 计算MTV。如果它是零,我们就可以称它为非碰撞 35 | const mtv = minkowskiDiff.getClosestPointOnBoundsToOrigin(); 36 | if (mtv.equals(Vector2.zero)) 37 | return false; 38 | 39 | hit.normal = new Vector2(-mtv.x, -mtv.y); 40 | hit.normal = hit.normal.normalize(); 41 | hit.distance = 0; 42 | hit.fraction = 0; 43 | 44 | return true; 45 | } else { 46 | // 射线投射移动矢量 47 | const ray = new Ray2D(Vector2.zero, movement.scale(-1)); 48 | const res = minkowskiDiff.rayIntersects(ray); 49 | if (res.intersected && res.distance <= 1) { 50 | hit.fraction = res.distance; 51 | hit.distance = movement.magnitude() * res.distance; 52 | hit.normal = movement.scale(-1); 53 | hit.normal = hit.normal.normalize(); 54 | hit.centroid = first.bounds.center.add(movement.scale(res.distance)); 55 | 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | } 62 | 63 | private static minkowskiDifference(first: Box, second: Box): Rectangle { 64 | // 我们需要第一个框的左上角 65 | // 碰撞器只会修改运动的位置所以我们需要用位置来计算出运动是什么。 66 | const positionOffset = first.position.sub(first.bounds.center); 67 | const topLeft = first.bounds.location.add(positionOffset.sub(second.bounds.max)); 68 | const fullSize = first.bounds.size.add(second.bounds.size); 69 | 70 | return new Rectangle(topLeft.x, topLeft.y, fullSize.x, fullSize.y) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/ShapeCollisions/ShapeCollisionsLine.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ShapeCollisionsLine { 3 | public static lineToPoly(start: Vector2, end: Vector2, polygon: Polygon, hit: Out): boolean { 4 | hit.value = new RaycastHit(); 5 | 6 | let normal = Vector2.zero; 7 | let intersectionPoint = Vector2.zero; 8 | let fraction = Number.MAX_VALUE; 9 | let hasIntersection = false; 10 | 11 | for (let j = polygon.points.length - 1, i = 0; i < polygon.points.length; j = i, i ++){ 12 | const edge1 = Vector2.add(polygon.position, polygon.points[j]); 13 | const edge2 = Vector2.add(polygon.position, polygon.points[i]); 14 | const intersection: Vector2 = Vector2.zero; 15 | if (ShapeCollisionsLine.lineToLine(edge1, edge2, start, end, intersection)){ 16 | hasIntersection = true; 17 | 18 | // TODO: 这是得到分数的正确和最有效的方法吗? 19 | // 先检查x分数。如果是NaN,就用y代替 20 | let distanceFraction = (intersection.x - start.x) / (end.x - start.x); 21 | if (Number.isNaN(distanceFraction) || Math.abs(distanceFraction) == Infinity) 22 | distanceFraction = (intersection.y - start.y) / (end.y - start.y); 23 | 24 | if (distanceFraction < fraction){ 25 | const edge = edge2.sub(edge1); 26 | normal = new Vector2(edge.y, -edge.x); 27 | fraction = distanceFraction; 28 | intersectionPoint = intersection; 29 | } 30 | } 31 | } 32 | 33 | if (hasIntersection){ 34 | normal = normal.normalize(); 35 | const distance = Vector2.distance(start, intersectionPoint); 36 | hit.value.setValues(fraction, distance, intersectionPoint, normal); 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public static lineToLine(a1: Vector2, a2: Vector2, b1: Vector2, b2: Vector2, intersection: Vector2){ 44 | const b = a2.sub(a1); 45 | const d = b2.sub(b1); 46 | const bDotDPerp = b.x * d.y - b.y * d.x; 47 | 48 | // 如果b*d = 0,表示这两条直线平行,因此有无穷个交点 49 | if (bDotDPerp == 0) 50 | return false; 51 | 52 | const c = b1.sub(a1); 53 | const t = (c.x * d.y - c.y * d.x) / bDotDPerp; 54 | if (t < 0 || t > 1) 55 | return false; 56 | 57 | const u = (c.x * b.y - c.y * b.x) / bDotDPerp; 58 | if (u < 0 || u > 1) 59 | return false; 60 | 61 | const r = a1.add(b.scale(t)); 62 | intersection.x = r.x; 63 | intersection.y = r.y; 64 | 65 | return true; 66 | } 67 | 68 | public static lineToCircle(start: Vector2, end: Vector2, s: Circle, hit: Out): boolean{ 69 | hit.value = new RaycastHit(); 70 | 71 | // 计算这里的长度并分别对d进行标准化,因为如果我们命中了我们需要它来得到分数 72 | const lineLength = Vector2.distance(start, end); 73 | const d = Vector2.divideScaler(end.sub(start), lineLength); 74 | const m = start.sub(s.position); 75 | const b = m.dot(d); 76 | const c = m.dot(m) - s.radius * s.radius; 77 | 78 | // 如果r的原点在s之外,(c>0)和r指向s (b>0) 则返回 79 | if (c > 0 && b > 0) 80 | return false; 81 | 82 | let discr = b * b - c; 83 | // 线不在圆圈上 84 | if (discr < 0) 85 | return false; 86 | 87 | // 射线相交圆 88 | hit.value.fraction = -b - Math.sqrt(discr); 89 | 90 | // 如果分数为负数,射线从圈内开始, 91 | if (hit.value.fraction < 0) 92 | hit.value.fraction = 0; 93 | 94 | hit.value.point = start.add(d.scale(hit.value.fraction)); 95 | hit.value.distance = Vector2.distance(start, hit.value.point); 96 | hit.value.normal = hit.value.point.sub(s.position).normalize(); 97 | hit.value.fraction = hit.value.distance / lineLength; 98 | 99 | return true; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/ShapeCollisions/ShapeCollisionsPoint.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ShapeCollisionsPoint { 3 | public static pointToCircle(point: Vector2, circle: Circle, result: Out): boolean { 4 | result.value = new CollisionResult(); 5 | 6 | let distanceSquared = Vector2.sqrDistance(point, circle.position); 7 | let sumOfRadii = 1 + circle.radius; 8 | let collided = distanceSquared < sumOfRadii * sumOfRadii; 9 | if (collided) { 10 | result.value.normal = point.sub(circle.position).normalize(); 11 | let depth = sumOfRadii - Math.sqrt(distanceSquared); 12 | result.value.minimumTranslationVector = result.value.normal.scale(-depth);; 13 | result.value.point = circle.position.add(result.value.normal.scale(circle.radius)); 14 | 15 | return true; 16 | } 17 | 18 | return false; 19 | } 20 | 21 | public static pointToBox(point: Vector2, box: Box, result: Out) { 22 | result.value = new CollisionResult(); 23 | 24 | if (box.containsPoint(point)) { 25 | // 在方框的空间里找到点 26 | const normal = new Out(); 27 | result.value.point = box.bounds.getClosestPointOnRectangleBorderToPoint(point, normal); 28 | result.value.normal = normal.value; 29 | result.value.minimumTranslationVector = point.sub(result.value.point); 30 | 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | public static pointToPoly(point: Vector2, poly: Polygon, result: Out): boolean { 38 | result.value = new CollisionResult(); 39 | 40 | if (poly.containsPoint(point)) { 41 | const res = Polygon.getClosestPointOnPolygonToPoint( 42 | poly.points, 43 | point.sub(poly.position) 44 | ); 45 | result.value.normal = res.edgeNormal; 46 | result.value.minimumTranslationVector = result.value.normal.scale(Math.sqrt(res.distanceSquared)); 47 | result.value.point = res.closestPoint.sub(poly.position); 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /source/src/Physics/Shapes/ShapeCollisions/ShapeCollisionsPolygon.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class ShapeCollisionsPolygon { 3 | /** 4 | * 检查两个多边形之间的碰撞 5 | * @param first 6 | * @param second 7 | * @param result 8 | */ 9 | public static polygonToPolygon(first: Polygon, second: Polygon, result: Out): boolean { 10 | result.value = new CollisionResult(); 11 | let isIntersecting = true; 12 | 13 | const firstEdges = first.edgeNormals; 14 | const secondEdges = second.edgeNormals; 15 | let minIntervalDistance = Number.POSITIVE_INFINITY; 16 | let translationAxis = Vector2.zero; 17 | let polygonOffset = first.position.sub(second.position); 18 | let axis: Vector2; 19 | 20 | // 循环穿过两个多边形的所有边 21 | for (let edgeIndex = 0; edgeIndex < firstEdges.length + secondEdges.length; edgeIndex++) { 22 | // 1. 找出当前多边形是否相交 23 | // 多边形的归一化轴垂直于缓存给我们的当前边 24 | axis = edgeIndex < firstEdges.length ? firstEdges[edgeIndex] : secondEdges[edgeIndex - firstEdges.length]; 25 | 26 | // 求多边形在当前轴上的投影 27 | let intervalDist = 0; 28 | let { min: minA, max: maxA } = this.getInterval(axis, first); 29 | const { min: minB, max: maxB } = this.getInterval(axis, second); 30 | 31 | // 将区间设为第二个多边形的空间。由轴上投影的位置差偏移。 32 | const relativeIntervalOffset = polygonOffset.dot(axis); 33 | minA += relativeIntervalOffset; 34 | maxA += relativeIntervalOffset; 35 | 36 | // 检查多边形投影是否正在相交 37 | intervalDist = this.intervalDistance(minA, maxA, minB, maxB); 38 | if (intervalDist > 0) 39 | isIntersecting = false; 40 | 41 | // 对于多对多数据类型转换,添加一个Vector2?参数称为deltaMovement。为了提高速度,我们这里不使用它 42 | // TODO: 现在找出多边形是否会相交。只要检查速度就行了 43 | 44 | // 如果多边形不相交,也不会相交,退出循环 45 | if (!isIntersecting) 46 | return false; 47 | 48 | // 检查当前间隔距离是否为最小值。如果是,则存储间隔距离和当前距离。这将用于计算最小平移向量 49 | intervalDist = Math.abs(intervalDist); 50 | if (intervalDist < minIntervalDistance) { 51 | minIntervalDistance = intervalDist; 52 | translationAxis.setTo(axis.x, axis.y); 53 | 54 | if (translationAxis.dot(polygonOffset) < 0) 55 | translationAxis = translationAxis.scale(-1); 56 | } 57 | } 58 | 59 | // 利用最小平移向量对多边形进行推入。 60 | result.value.normal = translationAxis; 61 | result.value.minimumTranslationVector = translationAxis.scale(-minIntervalDistance); 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * 计算一个多边形在一个轴上的投影,并返回一个[min,max]区间 68 | * @param axis 69 | * @param polygon 70 | * @param min 71 | * @param max 72 | */ 73 | public static getInterval(axis: Vector2, polygon: Polygon): { min: number, max: number } { 74 | const res = { min: 0, max: 0 }; 75 | let dot: number; 76 | dot = polygon.points[0].dot(axis); 77 | res.max = dot; 78 | res.min = dot; 79 | for (let i = 1; i < polygon.points.length; i++) { 80 | dot = polygon.points[i].dot(axis); 81 | if (dot < res.min) { 82 | res.min = dot; 83 | } else if (dot > res.max) { 84 | res.max = dot; 85 | } 86 | } 87 | return res; 88 | } 89 | 90 | /** 91 | * 计算[minA, maxA]和[minB, maxB]之间的距离。如果间隔重叠,距离是负的 92 | * @param minA 93 | * @param maxA 94 | * @param minB 95 | * @param maxB 96 | */ 97 | public static intervalDistance(minA: number, maxA: number, minB: number, maxB: number) { 98 | if (minA < minB) 99 | return minB - maxA; 100 | 101 | return minA - maxB; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /source/src/Tween/AbstractTweenable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * `AbstractTweenable` 是一个抽象类,实现了 `ITweenable` 接口。 4 | * 这个类提供了 `start`、`pause`、`resume` 和 `stop` 等方法, 5 | * 并且具有判断动画是否运行的方法 `isRunning`。 6 | * 它还有一个 `tick` 方法,子类需要根据自己的需要实现这个方法。 7 | * 8 | * `AbstractTweenable` 在完成后往往会被保留下来, `_isCurrentlyManagedByTweenManager` 标志可以让它们知道自己当前是否被 `TweenManager` 监控着, 9 | * 以便在必要时可以重新添加自己。 10 | */ 11 | export abstract class AbstractTweenable implements ITweenable { 12 | readonly discriminator = "ITweenable"; 13 | protected _isPaused: boolean; 14 | 15 | /** 16 | * abstractTweenable在完成后往往会被保留下来。 17 | * 这个标志可以让它们在内部知道自己当前是否被TweenManager盯上了,以便在必要时可以重新添加自己。 18 | */ 19 | protected _isCurrentlyManagedByTweenManager: boolean; 20 | 21 | public abstract tick(): boolean; 22 | 23 | public recycleSelf() { 24 | } 25 | 26 | public isRunning(): boolean { 27 | return this._isCurrentlyManagedByTweenManager && !this._isPaused; 28 | } 29 | 30 | public start() { 31 | if (this._isCurrentlyManagedByTweenManager) { 32 | this._isPaused = false; 33 | return; 34 | } 35 | 36 | TweenManager.addTween(this); 37 | this._isCurrentlyManagedByTweenManager = true; 38 | this._isPaused = false; 39 | } 40 | 41 | public pause() { 42 | this._isPaused = true; 43 | } 44 | 45 | public resume() { 46 | this._isPaused = false; 47 | } 48 | 49 | public stop(bringToCompletion: boolean = false) { 50 | TweenManager.removeTween(this); 51 | this._isCurrentlyManagedByTweenManager = false; 52 | this._isPaused = true; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /source/src/Tween/Easing/Lerps.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 一系列静态方法来处理所有常见的tween类型结构,以及它们的unclamped lerps.unclamped lerps对于超过0-1范围的bounce、elastic或其他tweens是必需的 4 | */ 5 | export class Lerps { 6 | /** 7 | * 提供通用的线性插值方法,支持数字、矩形和二维向量类型。 8 | * @param from 起点值 9 | * @param to 终点值 10 | * @param t 插值参数,取值范围[0, 1] 11 | * @returns 返回两个值的插值结果 12 | */ 13 | public static lerp(from: number, to: number, t: number): number; 14 | public static lerp(from: Rectangle, to: Rectangle, t: number): Rectangle; 15 | public static lerp(from: Vector2, to: Vector2, t: number): Vector2; 16 | public static lerp(from: any, to: any, t: number) { 17 | // 判断传入的数据类型,并执行对应的插值逻辑 18 | if (typeof (from) == "number" && typeof (to) == "number") { 19 | return from + (to - from) * t; 20 | } 21 | 22 | if (from instanceof Rectangle && to instanceof Rectangle) { 23 | return new Rectangle( 24 | (from.x + (to.x - from.x) * t), 25 | (from.y + (to.x - from.y) * t), 26 | (from.width + (to.width - from.width) * t), 27 | (from.height + (to.height - from.height) * t) 28 | ); 29 | } 30 | 31 | if (from instanceof Vector2 && to instanceof Vector2) { 32 | return new Vector2(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t); 33 | } 34 | } 35 | 36 | /** 37 | * 计算两个向量之间的角度差并使用线性插值函数进行插值 38 | * @param from 起始向量 39 | * @param to 目标向量 40 | * @param t 插值因子 41 | * @returns 插值后的向量 42 | */ 43 | public static angleLerp(from: Vector2, to: Vector2, t: number): Vector2 { 44 | // 计算最短的角差,确保角度在[-180, 180]度之间 45 | let toMinusFrom = new Vector2( 46 | MathHelper.deltaAngle(from.x, to.x), 47 | MathHelper.deltaAngle(from.y, to.y) 48 | ); 49 | // 使用线性插值函数计算插值后的向量 50 | return new Vector2( 51 | from.x + toMinusFrom.x * t, 52 | from.y + toMinusFrom.y * t 53 | ); 54 | } 55 | 56 | /** 57 | * 根据不同类型的数据,使用指定的缓动类型对两个值进行插值 58 | * @param easeType 缓动类型 59 | * @param from 起始值 60 | * @param to 目标值 61 | * @param t 当前时间(相对于持续时间的百分比) 62 | * @param duration 持续时间 63 | * @returns 两个值之间的插值 64 | */ 65 | public static ease(easeType: EaseType, from: Rectangle, to: Rectangle, t: number, duration: number); 66 | public static ease(easeType: EaseType, from: Vector2, to: Vector2, t: number, duration: number); 67 | public static ease(easeType: EaseType, from: number, to: number, t: number, duration: number); 68 | public static ease(easeType: EaseType, from: any, to: any, t: number, duration: number) { 69 | // 如果传入的值都是 number 类型,就直接返回两个值之间的线性插值 70 | if (typeof (from) == 'number' && typeof (to) == "number") { 71 | return this.lerp(from, to, EaseHelper.ease(easeType, t, duration)); 72 | } 73 | 74 | // 如果传入的值都是 Vector2 类型,就返回两个 Vector2 之间的插值 75 | if (from instanceof Vector2 && to instanceof Vector2) { 76 | return this.lerp(from, to, EaseHelper.ease(easeType, t, duration)); 77 | } 78 | 79 | // 如果传入的值都是 Rectangle 类型,就返回两个 Rectangle 之间的插值 80 | if (from instanceof Rectangle && to instanceof Rectangle) { 81 | return this.lerp(from, to, EaseHelper.ease(easeType, t, duration)); 82 | } 83 | } 84 | 85 | /** 86 | * 通过提供的t值和持续时间使用给定的缓动类型在两个Vector2之间进行角度插值。 87 | * @param easeType 缓动类型 88 | * @param from 开始的向量 89 | * @param to 结束的向量 90 | * @param t 当前时间在持续时间内的比例 91 | * @param duration 持续时间 92 | * @returns 插值后的Vector2值 93 | */ 94 | public static easeAngle(easeType: EaseType, from: Vector2, to: Vector2, t: number, duration: number): Vector2 { 95 | return this.angleLerp(from, to, EaseHelper.ease(easeType, t, duration)); 96 | } 97 | 98 | 99 | /** 100 | * 使用快速弹簧算法来实现平滑过渡。返回经过弹簧计算后的当前值。 101 | * @param currentValue 当前值 102 | * @param targetValue 目标值 103 | * @param velocity 当前速度 104 | * @param dampingRatio 阻尼比例 105 | * @param angularFrequency 角频率 106 | */ 107 | public static fastSpring(currentValue: Vector2, targetValue: Vector2, velocity: Vector2, 108 | dampingRatio: number, angularFrequency: number): Vector2 { 109 | // 计算下一帧的速度 110 | velocity.add(velocity.scale(-2 * Time.deltaTime * dampingRatio * angularFrequency) 111 | .add(targetValue.sub(currentValue).scale(Time.deltaTime * angularFrequency * angularFrequency))); 112 | // 计算下一帧的当前值 113 | currentValue.add(velocity.scale(Time.deltaTime)); 114 | // 返回计算后的当前值 115 | return currentValue; 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /source/src/Tween/Interfaces/ITween.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 一系列强类型、可链式的方法来设置各种tween属性 4 | */ 5 | export interface ITween extends ITweenControl { 6 | /** 7 | * 设置该tween的易用性类型 8 | * @param easeType 9 | */ 10 | setEaseType(easeType: EaseType): ITween; 11 | /** 12 | * 设置启动tween前的延迟 13 | * @param delay 14 | */ 15 | setDelay(delay: number): ITween; 16 | /** 17 | * 设置tween的持续时间 18 | * @param duration 19 | */ 20 | setDuration(duration: number): ITween; 21 | /** 22 | * 设置这个tween使用的timeScale。 23 | * TimeScale将与Time.deltaTime/Time.unscaledDeltaTime相乘,从而得到tween实际使用的delta时间 24 | * @param timeScale 25 | */ 26 | setTimeScale(timeScale: number): ITween; 27 | /** 28 | * 设置tween使用Time.unscaledDeltaTime代替Time.deltaTime 29 | */ 30 | setIsTimeScaleIndependent(): ITween; 31 | /** 32 | * 设置当tween完成时应该调用的动作 33 | * @param completionHandler 34 | */ 35 | setCompletionHandler(completionHandler: (tween: ITween) => void): ITween; 36 | /** 37 | * 设置tween的循环类型。一个pingpong循环意味着从开始-结束-开始 38 | * @param loopType 39 | * @param loops 40 | * @param delayBetweenLoops 41 | */ 42 | setLoops(loopType: LoopType, loops: number, delayBetweenLoops: number): ITween; 43 | /** 44 | * 设置tween的起始位置 45 | * @param from 46 | */ 47 | setFrom(from: T): ITween; 48 | /** 49 | * 通过重置tween的from/to值和持续时间,为重复使用tween做准备。 50 | * @param from 51 | * @param to 52 | * @param duration 53 | */ 54 | prepareForReuse(from: T, to: T, duration: number): ITween; 55 | /** 56 | * 如果为true(默认值),tween将在使用后被回收。 57 | * 如果在TweenManager类中进行了配置,所有的Tween子类都有自己相关的自动缓存 58 | * @param shouldRecycleTween 59 | */ 60 | setRecycleTween(shouldRecycleTween: boolean): ITween; 61 | /** 62 | * 帮助程序,只是将tween的to值设置为相对于其当前值的+从使tween 63 | */ 64 | setIsRelative(): ITween; 65 | /** 66 | * 允许你通过tween.context.context来设置任何可检索的对象引用。 67 | * 这对于避免完成处理程序方法的闭包分配是很方便的。 68 | * 你也可以在TweenManager中搜索具有特定上下文的所有tweens 69 | * @param context 70 | */ 71 | setContext(context): ITween; 72 | /** 73 | * 允许你添加一个tween,这个tween完成后会被运行。 74 | * 注意 nextTween 必须是一个 ITweenable! 同时注意,所有的ITweenT都是ITweenable 75 | * @param nextTween 76 | */ 77 | setNextTween(nextTween: ITweenable): ITween; 78 | } 79 | } -------------------------------------------------------------------------------- /source/src/Tween/Interfaces/ITweenControl.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 更多具体的Tween播放控制在这里 4 | */ 5 | export interface ITweenControl extends ITweenable { 6 | readonly discriminator: "ITweenControl"; 7 | /** 8 | * 当使用匿名方法时,您可以在任何回调(如完成处理程序)中使用该属性来避免分配 9 | */ 10 | context; 11 | /** 12 | * 将tween扭曲为elapsedTime,并将其限制在0和duration之间,无论tween对象是暂停、完成还是运行,都会立即更新 13 | * @param elapsedTime 所用时间 14 | */ 15 | jumpToElapsedTime(elapsedTime: number); 16 | /** 17 | * 当从StartCoroutine调用时,它将直到tween完成 18 | */ 19 | waitForCompletion(): any; 20 | /** 21 | * 获取tween的目标,如果TweenTargets不一定都是一个对象,则为null,它的唯一真正用途是让TweenManager按目标查找tweens的列表 22 | */ 23 | getTargetObject(): any; 24 | } 25 | } -------------------------------------------------------------------------------- /source/src/Tween/Interfaces/ITweenTarget.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 任何想要被weened的对象都需要实现这个功能。 4 | * TweenManager内部喜欢做一个简单的对象来实现这个接口,并存储一个对被tweened对象的引用 5 | */ 6 | export interface ITweenTarget { 7 | /** 8 | * 在你选择的对象上设置最终的tweened值 9 | * @param value 10 | */ 11 | setTweenedValue(value: T); 12 | 13 | getTweenedValue(): T; 14 | /** 15 | * 获取tween的目标,如果TweenTargets不一定都是一个对象,则为null,它的唯一真正用途是让TweenManager按目标查找tweens的列表 16 | */ 17 | getTargetObject(): any; 18 | } 19 | } -------------------------------------------------------------------------------- /source/src/Tween/Interfaces/ITweenable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface ITweenable { 3 | readonly discriminator: string; 4 | /** 5 | * 就像内部的Update一样,每一帧都被TweenManager调用 6 | */ 7 | tick(): boolean; 8 | /** 9 | * 当一个tween被移除时,由TweenManager调用。子 10 | * 类可以选择自己回收。子类应该首先在其实现中检查_shouldRecycleTween bool! 11 | */ 12 | recycleSelf(); 13 | /** 14 | * 检查是否有tween在运行 15 | */ 16 | isRunning(): boolean; 17 | /** 18 | * 启动tween 19 | */ 20 | start(); 21 | /** 22 | * 暂停 23 | */ 24 | pause(); 25 | /** 26 | * 暂停后恢复tween 27 | */ 28 | resume(); 29 | /** 30 | * 停止tween,并可选择将其完成 31 | * @param bringToCompletion 32 | */ 33 | stop(bringToCompletion: boolean); 34 | } 35 | } -------------------------------------------------------------------------------- /source/src/Tween/PropertyTweens.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | class PropertyTarget implements ITweenTarget { 3 | protected _target: any; // 属性动画的目标对象 4 | protected _propertyName: string; // 属性名 5 | 6 | /** 7 | * @param target 属性动画的目标对象 8 | * @param propertyName 属性名 9 | */ 10 | constructor(target: any, propertyName: string) { 11 | this._target = target; 12 | this._propertyName = propertyName; 13 | } 14 | 15 | public getTargetObject(): any { 16 | return this._target; 17 | } 18 | 19 | public setTweenedValue(value: T) { 20 | // 将属性动画的目标对象的属性值设置为动画的当前值 21 | this._target[this._propertyName] = value; 22 | } 23 | 24 | public getTweenedValue(): T { 25 | // 获取属性动画的目标对象的属性值 26 | return this._target[this._propertyName]; 27 | } 28 | } 29 | 30 | /** 31 | * 属性动画工具类 32 | */ 33 | export class PropertyTweens { 34 | /** 35 | * 创建一个属性为number类型的动画对象 36 | * @param self 属性动画的目标对象 37 | * @param memberName 属性名 38 | * @param to 动画结束时的属性值 39 | * @param duration 动画时长 40 | */ 41 | public static NumberPropertyTo(self: any, memberName: string, to: number, duration: number): ITween { 42 | let tweenTarget = new PropertyTarget(self, memberName); 43 | let tween = TweenManager.cacheNumberTweens ? Pool.obtain(NumberTween) : new NumberTween(); 44 | tween.initialize(tweenTarget, to, duration); 45 | 46 | return tween; 47 | } 48 | 49 | /** 50 | * 创建一个属性为Vector2类型的动画对象 51 | * @param self 属性动画的目标对象 52 | * @param memberName 属性名 53 | * @param to 动画结束时的属性值 54 | * @param duration 动画时长 55 | */ 56 | public static Vector2PropertyTo(self: any, memberName: string, to: Vector2, duration: number): ITween { 57 | let tweenTarget = new PropertyTarget(self, memberName); 58 | let tween = TweenManager.cacheVector2Tweens ? Pool.obtain(Vector2Tween) : new Vector2Tween(); 59 | tween.initialize(tweenTarget, to, duration); 60 | 61 | return tween; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /source/src/Tween/TransformSpringTween.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class TransformSpringTween extends AbstractTweenable { 3 | public get targetType() { 4 | return this._targetType; 5 | } 6 | 7 | private _transform: Transform; 8 | private _targetType: TransformTargetType; 9 | private _targetValue: Vector2; 10 | private _velocity: Vector2; 11 | 12 | // 阻尼比(dampingRatio)和角频率(angularFrequency)的配置是公开的,以便于在设计时进行调整 13 | 14 | /** 15 | * 值越低,阻尼越小,值越高,阻尼越大,导致弹簧度越小,应在0.01-1之间,以避免系统不稳定 16 | */ 17 | public dampingRatio: number = 0.23; 18 | /** 19 | * 角频率为2pi(弧度/秒)意味着振荡在一秒钟内完成一个完整的周期,即1Hz.应小于35左右才能保持稳定角频率 20 | */ 21 | public angularFrequency: number = 25; 22 | 23 | constructor(transform: Transform, targetType: TransformTargetType, targetValue: Vector2) { 24 | super(); 25 | this._transform = transform; 26 | this._targetType = targetType; 27 | this.setTargetValue(targetValue); 28 | } 29 | 30 | /** 31 | * 你可以在任何时候调用setTargetValue来重置目标值到一个新的Vector2。 32 | * 如果你没有调用start来添加spring tween,它会为你调用 33 | * @param targetValue 34 | */ 35 | public setTargetValue(targetValue: Vector2) { 36 | this._velocity = Vector2.zero; 37 | this._targetValue = targetValue; 38 | 39 | if (!this._isCurrentlyManagedByTweenManager) 40 | this.start(); 41 | } 42 | 43 | /** 44 | * lambda应该是振荡幅度减少50%时的理想持续时间 45 | * @param lambda 46 | */ 47 | public updateDampingRatioWithHalfLife(lambda: number) { 48 | this.dampingRatio = (-lambda / this.angularFrequency) * Math.log(0.5); 49 | } 50 | 51 | public tick() { 52 | if (!this._isPaused) 53 | this.setTweenedValue(Lerps.fastSpring(this.getCurrentValueOfTweenedTargetType(), this._targetValue, this._velocity, 54 | this.dampingRatio, this.angularFrequency)); 55 | 56 | return false; 57 | } 58 | 59 | private setTweenedValue(value: Vector2) { 60 | switch (this._targetType) { 61 | case TransformTargetType.position: 62 | this._transform.position = value; 63 | break; 64 | case TransformTargetType.localPosition: 65 | this._transform.localPosition = value; 66 | break; 67 | case TransformTargetType.scale: 68 | this._transform.scale = value; 69 | break; 70 | case TransformTargetType.localScale: 71 | this._transform.localScale = value; 72 | break; 73 | case TransformTargetType.rotationDegrees: 74 | this._transform.rotationDegrees = value.x; 75 | case TransformTargetType.localRotationDegrees: 76 | this._transform.localRotationDegrees = value.x; 77 | break; 78 | } 79 | } 80 | 81 | private getCurrentValueOfTweenedTargetType() { 82 | switch (this._targetType) { 83 | case TransformTargetType.position: 84 | return this._transform.position; 85 | case TransformTargetType.localPosition: 86 | return this._transform.localPosition; 87 | case TransformTargetType.scale: 88 | return this._transform.scale; 89 | case TransformTargetType.localScale: 90 | return this._transform.localScale; 91 | case TransformTargetType.rotationDegrees: 92 | return new Vector2(this._transform.rotationDegrees); 93 | case TransformTargetType.localRotationDegrees: 94 | return new Vector2(this._transform.localRotationDegrees, 0); 95 | default: 96 | return Vector2.zero; 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /source/src/Tween/TransformVector2Tween.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | /** 4 | * 对任何与Transform相关的属性tweens都是有用的枚举 5 | */ 6 | export enum TransformTargetType { 7 | position, 8 | localPosition, 9 | scale, 10 | localScale, 11 | rotationDegrees, 12 | localRotationDegrees, 13 | } 14 | 15 | /** 16 | * 这是一个特殊的情况,因为Transform是迄今为止最被ween的对象。 17 | * 我们将Tween和ITweenTarget封装在一个单一的、可缓存的类中 18 | */ 19 | export class TransformVector2Tween extends Vector2Tween implements ITweenTarget { 20 | private _transform: Transform; 21 | private _targetType: TransformTargetType; 22 | 23 | public setTweenedValue(value: Vector2) { 24 | switch (this._targetType) { 25 | case TransformTargetType.position: 26 | this._transform.position = value; 27 | break; 28 | case TransformTargetType.localPosition: 29 | this._transform.localPosition = value; 30 | break; 31 | case TransformTargetType.scale: 32 | this._transform.scale = value; 33 | break; 34 | case TransformTargetType.localScale: 35 | this._transform.localScale = value; 36 | break; 37 | case TransformTargetType.rotationDegrees: 38 | this._transform.rotationDegrees = value.x; 39 | case TransformTargetType.localRotationDegrees: 40 | this._transform.localRotationDegrees = value.x; 41 | break; 42 | } 43 | } 44 | 45 | public getTweenedValue(): Vector2 { 46 | switch (this._targetType) { 47 | case TransformTargetType.position: 48 | return this._transform.position; 49 | case TransformTargetType.localPosition: 50 | return this._transform.localPosition; 51 | case TransformTargetType.scale: 52 | return this._transform.scale; 53 | case TransformTargetType.localScale: 54 | return this._transform.localScale; 55 | case TransformTargetType.rotationDegrees: 56 | return new Vector2(this._transform.rotationDegrees, this._transform.rotationDegrees); 57 | case TransformTargetType.localRotationDegrees: 58 | return new Vector2(this._transform.localRotationDegrees, 0); 59 | } 60 | } 61 | 62 | public getTargetObject() { 63 | return this._transform; 64 | } 65 | 66 | public setTargetAndType(transform: Transform, targetType: TransformTargetType) { 67 | this._transform = transform; 68 | this._targetType = targetType; 69 | } 70 | 71 | protected updateValue() { 72 | // 非相对角勒普的特殊情况,使他们采取尽可能短的旋转 73 | if ((this._targetType == TransformTargetType.rotationDegrees || 74 | this._targetType == TransformTargetType.localRotationDegrees) && !this._isRelative) { 75 | this.setTweenedValue(Lerps.easeAngle(this._easeType, this._fromValue, this._toValue, this._elapsedTime, this._duration)); 76 | } else { 77 | this.setTweenedValue(Lerps.ease(this._easeType, this._fromValue, this._toValue, this._elapsedTime, this._duration)); 78 | } 79 | } 80 | 81 | public recycleSelf() { 82 | if (this._shouldRecycleTween) { 83 | this._target = null; 84 | this._nextTween = null; 85 | this._transform = null; 86 | Pool.free(Vector2Tween, this); 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /source/src/Tween/Tweens.ts: -------------------------------------------------------------------------------- 1 | /// 2 | module es { 3 | export class NumberTween extends Tween { 4 | public static create(): NumberTween { 5 | return TweenManager.cacheNumberTweens ? Pool.obtain(NumberTween) : new NumberTween(); 6 | } 7 | 8 | constructor(target?: ITweenTarget, to?: number, duration?: number) { 9 | super(); 10 | this.initialize(target, to, duration); 11 | } 12 | 13 | public setIsRelative(): ITween { 14 | this._isRelative = true; 15 | this._toValue += this._fromValue; 16 | return this; 17 | } 18 | 19 | protected updateValue() { 20 | this._target.setTweenedValue(Lerps.ease(this._easeType, this._fromValue, this._toValue, this._elapsedTime, this._duration)); 21 | } 22 | 23 | public recycleSelf() { 24 | super.recycleSelf(); 25 | 26 | if (this._shouldRecycleTween && TweenManager.cacheNumberTweens) 27 | Pool.free(NumberTween, this); 28 | } 29 | } 30 | 31 | export class Vector2Tween extends Tween { 32 | public static create(): Vector2Tween { 33 | return TweenManager.cacheVector2Tweens ? Pool.obtain(Vector2Tween) : new Vector2Tween(); 34 | } 35 | 36 | constructor(target?: ITweenTarget, to?: Vector2, duration?: number) { 37 | super(); 38 | this.initialize(target, to, duration); 39 | } 40 | 41 | public setIsRelative(): ITween { 42 | this._isRelative = true; 43 | this._toValue.add(this._fromValue); 44 | return this; 45 | } 46 | 47 | protected updateValue() { 48 | this._target.setTweenedValue(Lerps.ease(this._easeType, this._fromValue, this._toValue, this._elapsedTime, this._duration)); 49 | } 50 | 51 | public recycleSelf() { 52 | super.recycleSelf(); 53 | 54 | if (this._shouldRecycleTween && TweenManager.cacheVector2Tweens) 55 | Pool.free(Vector2Tween, this); 56 | } 57 | } 58 | 59 | export class RectangleTween extends Tween { 60 | public static create(): RectangleTween { 61 | return TweenManager.cacheRectTweens ? Pool.obtain(RectangleTween) : new RectangleTween(); 62 | } 63 | 64 | constructor(target?: ITweenTarget, to?: Rectangle, duration?: number) { 65 | super(); 66 | this.initialize(target, to, duration); 67 | } 68 | 69 | public setIsRelative(): ITween { 70 | this._isRelative = true; 71 | this._toValue = new Rectangle( 72 | this._toValue.x + this._fromValue.x, 73 | this._toValue.y + this._fromValue.y, 74 | this._toValue.width + this._fromValue.width, 75 | this._toValue.height + this._fromValue.height); 76 | 77 | return this; 78 | } 79 | 80 | protected updateValue() { 81 | this._target.setTweenedValue(Lerps.ease(this._easeType, this._fromValue, this._toValue, this._elapsedTime, this._duration)); 82 | } 83 | 84 | public recycleSelf() { 85 | super.recycleSelf(); 86 | 87 | if (this._shouldRecycleTween && TweenManager.cacheRectTweens) 88 | Pool.free(RectangleTween, this); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /source/src/Utils/AnimCurve.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface IAnimFrame { 3 | t: number; 4 | value: number; 5 | } 6 | 7 | export class AnimCurve { 8 | public get points(): IAnimFrame[] { 9 | return this._points; 10 | } 11 | 12 | public constructor(points: IAnimFrame[]) { 13 | if (points.length < 2) { 14 | throw new Error('curve length must be >= 2'); 15 | } 16 | points.sort((a: IAnimFrame, b: IAnimFrame) => { 17 | return a.t - b.t; 18 | }); 19 | if (points[0].t !== 0) { 20 | throw new Error('curve must start with 0'); 21 | } 22 | if (points[points.length - 1].t !== 1) { 23 | throw new Error('curve must end with 1'); 24 | } 25 | this._points = points; 26 | } 27 | 28 | public lerp(t: number): number { 29 | for (let i = 1; i < this._points.length; i++) { 30 | if (t <= this._points[i].t) { 31 | const m = MathHelper.map01( 32 | t, 33 | this._points[i - 1].t, 34 | this._points[i].t 35 | ); 36 | return MathHelper.lerp( 37 | this._points[i - 1].value, 38 | this._points[i].value, 39 | m 40 | ); 41 | } 42 | } 43 | throw new Error('should never be here'); 44 | } 45 | 46 | public _points: IAnimFrame[]; 47 | } 48 | } -------------------------------------------------------------------------------- /source/src/Utils/Collections/Bag.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Bag implements ImmutableBag { 3 | public size_ = 0; 4 | public length = 0; 5 | private array: Array = []; 6 | 7 | constructor(capacity = 64) { 8 | this.length = capacity; 9 | } 10 | 11 | removeAt(index: number): E { 12 | const e: E = this.array[index]; 13 | this.array[index] = this.array[--this.size_]; 14 | this.array[this.size_] = null; 15 | return e; 16 | } 17 | 18 | remove(e: E): boolean { 19 | let i: number; 20 | let e2: E; 21 | const size = this.size_; 22 | 23 | for (i = 0; i < size; i++) { 24 | e2 = this.array[i]; 25 | 26 | if (e == e2) { 27 | this.array[i] = this.array[--this.size_]; 28 | this.array[this.size_] = null; 29 | return true; 30 | } 31 | } 32 | 33 | return false; 34 | } 35 | 36 | removeLast(): E { 37 | if (this.size_ > 0) { 38 | const e: E = this.array[--this.size_]; 39 | this.array[this.size_] = null; 40 | return e; 41 | } 42 | 43 | return null; 44 | } 45 | 46 | contains(e: E): boolean { 47 | let i: number; 48 | let size: number; 49 | 50 | for (i = 0, size = this.size_; size > i; i++) { 51 | if (e === this.array[i]) { 52 | return true; 53 | } 54 | } 55 | return false; 56 | } 57 | 58 | removeAll(bag: ImmutableBag): boolean { 59 | let modified = false; 60 | let i: number; 61 | let j: number; 62 | let l: number; 63 | let e1: E; 64 | let e2: E; 65 | 66 | for (i = 0, l = bag.size(); i < l; i++) { 67 | e1 = bag[i]; 68 | 69 | for (j = 0; j < this.size_; j++) { 70 | e2 = this.array[j]; 71 | 72 | if (e1 === e2) { 73 | this.removeAt(j); 74 | j--; 75 | modified = true; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | return modified; 82 | } 83 | 84 | get(index: number): E { 85 | if (index >= this.length) { 86 | throw new Error("ArrayIndexOutOfBoundsException"); 87 | } 88 | return this.array[index]; 89 | } 90 | 91 | safeGet(index: number): E { 92 | if (index >= this.length) { 93 | this.grow((index * 7) / 4 + 1); 94 | } 95 | return this.array[index]; 96 | } 97 | 98 | size(): number { 99 | return this.size_; 100 | } 101 | 102 | getCapacity(): number { 103 | return this.length; 104 | } 105 | 106 | isIndexWithinBounds(index: number): boolean { 107 | return index < this.getCapacity(); 108 | } 109 | 110 | isEmpty(): boolean { 111 | return this.size_ == 0; 112 | } 113 | 114 | add(e: E): void { 115 | if (this.size_ === this.length) { 116 | this.grow(); 117 | } 118 | 119 | this.array[this.size_++] = e; 120 | } 121 | 122 | set(index: number, e: E): void { 123 | if (index >= this.length) { 124 | this.grow(index * 2); 125 | } 126 | this.size_ = index + 1; 127 | this.array[index] = e; 128 | } 129 | 130 | grow(newCapacity: number = ~~((this.length * 3) / 2) + 1): void { 131 | this.length = ~~newCapacity; 132 | } 133 | 134 | ensureCapacity(index: number): void { 135 | if (index >= this.length) { 136 | this.grow(index * 2); 137 | } 138 | } 139 | 140 | clear(): void { 141 | let i: number; 142 | let size: number; 143 | for (i = 0, size = this.size_; i < size; i++) { 144 | this.array[i] = null; 145 | } 146 | 147 | this.size_ = 0; 148 | } 149 | 150 | addAll(items: ImmutableBag): void { 151 | let i: number; 152 | 153 | for (i = 0; items.size() > i; i++) { 154 | this.add(items.get(i)); 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /source/src/Utils/Collections/HashMap.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | interface Map { 3 | clear(): void; 4 | containsKey(key): boolean; 5 | containsValue(value): boolean; 6 | get(key): V; 7 | isEmpty(): boolean; 8 | put(key, value): void; 9 | remove(key): V; 10 | size(): number; 11 | values(): V[]; 12 | } 13 | 14 | function decode(key): string { 15 | switch (typeof key) { 16 | case "boolean": 17 | return "" + key; 18 | case "number": 19 | return "" + key; 20 | case "string": 21 | return "" + key; 22 | case "function": 23 | return getClassName(key); 24 | default: 25 | key.uuid = key.uuid ? key.uuid : UUID.randomUUID(); 26 | return key.uuid; 27 | } 28 | } 29 | 30 | export class HashMap implements Map { 31 | private map_; 32 | private keys_; 33 | 34 | constructor() { 35 | this.clear(); 36 | } 37 | 38 | clear(): void { 39 | this.map_ = {}; 40 | this.keys_ = {}; 41 | } 42 | 43 | values(): V[] { 44 | const result = []; 45 | const map = this.map_; 46 | 47 | for (const key in map) { 48 | result.push(map[key]); 49 | } 50 | return result; 51 | } 52 | 53 | contains(value): boolean { 54 | const map = this.map_; 55 | 56 | for (const key in map) { 57 | if (value === map[key]) { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | containsKey(key): boolean { 65 | return decode(key) in this.map_; 66 | } 67 | 68 | containsValue(value): boolean { 69 | const map = this.map_; 70 | 71 | for (const key in map) { 72 | if (value === map[key]) { 73 | return true; 74 | } 75 | } 76 | return false; 77 | } 78 | 79 | get(key: K): V { 80 | return this.map_[decode(key)]; 81 | } 82 | 83 | isEmpty(): boolean { 84 | return Object.keys(this.map_).length === 0; 85 | } 86 | 87 | keys(): K[] { 88 | const keys = this.map_; 89 | 90 | const result = []; 91 | for (const key in keys) { 92 | result.push(keys[key]); 93 | } 94 | return result; 95 | } 96 | 97 | /** 98 | * if key is a string, use as is, else use key.id_ or key.name 99 | */ 100 | put(key, value): void { 101 | const k = decode(key); 102 | this.map_[k] = value; 103 | this.keys_[k] = key; 104 | } 105 | 106 | remove(key): V { 107 | const map = this.map_; 108 | const k = decode(key); 109 | const value = map[k]; 110 | delete map[k]; 111 | delete this.keys_[k]; 112 | return value; 113 | } 114 | 115 | size(): number { 116 | return Object.keys(this.map_).length; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /source/src/Utils/Collections/ImmutableBag.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface ImmutableBag { 3 | get(index: number): E; 4 | 5 | size(): number; 6 | 7 | isEmpty(): boolean; 8 | 9 | contains(e: E): boolean; 10 | } 11 | } -------------------------------------------------------------------------------- /source/src/Utils/Collections/ListPool.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 可以用于列表池的简单类 4 | */ 5 | export class ListPool { 6 | private static readonly _objectQueue: Map = new Map(); 7 | 8 | /** 9 | * 预热缓存,使用最大的cacheCount对象填充缓存 10 | * @param cacheCount 11 | */ 12 | public static warmCache(type: new (...args) => T, cacheCount: number) { 13 | this.checkCreate(type); 14 | cacheCount -= this._objectQueue.get(type).length; 15 | if (cacheCount > 0) { 16 | for (let i = 0; i < cacheCount; i++) { 17 | this._objectQueue.get(type).push([]); 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * 将缓存修剪为cacheCount项目 24 | * @param cacheCount 25 | */ 26 | public static trimCache(type: new (...args) => T, cacheCount: number) { 27 | this.checkCreate(type); 28 | while (cacheCount > this._objectQueue.get(type).length) 29 | this._objectQueue.get(type).splice(0, 1); 30 | } 31 | 32 | /** 33 | * 清除缓存 34 | */ 35 | public static clearCache(type: new (...args) => T) { 36 | this.checkCreate(type); 37 | this._objectQueue.get(type).length = 0; 38 | } 39 | 40 | /** 41 | * 如果可以的话,从堆栈中弹出一个项 42 | */ 43 | public static obtain(type: new (...args) => T): T[] { 44 | this.checkCreate(type); 45 | if (this._objectQueue.get(type).length > 0) 46 | return this._objectQueue.get(type).shift(); 47 | 48 | return []; 49 | } 50 | 51 | /** 52 | * 将项推回堆栈 53 | * @param obj 54 | */ 55 | public static free(type: new (...args) => T, obj: T[]) { 56 | this.checkCreate(type); 57 | this._objectQueue.get(type).push(obj); 58 | obj.length = 0; 59 | } 60 | 61 | private static checkCreate(type: new (...args) => T) { 62 | if (!this._objectQueue.has(type)) 63 | this._objectQueue.set(type, []); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /source/src/Utils/Collections/Pair.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 用于管理一对对象的简单DTO 4 | */ 5 | export class Pair { 6 | public first: T; 7 | public second: T; 8 | 9 | constructor(first: T, second: T) { 10 | this.first = first; 11 | this.second = second; 12 | } 13 | 14 | public clear() { 15 | this.first = this.second = null; 16 | } 17 | 18 | public equals(other: Pair): boolean { 19 | // 这两种方法在功能上应该是等价的 20 | return this.first === other.first && this.second === other.second; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/src/Utils/Collections/PairSet.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class PairSet { 3 | public get all(): Array> { 4 | return this._all; 5 | } 6 | 7 | public has(pair: Pair) { 8 | const index = this._all.findIndex(p => p.equals(pair)); 9 | return index > -1; 10 | } 11 | 12 | public add(pair: Pair) { 13 | if (!this.has(pair)) { 14 | this._all.push(pair); 15 | } 16 | } 17 | 18 | public remove(pair: Pair) { 19 | const index = this._all.findIndex(p => p.equals(pair)); 20 | if (index > -1) { 21 | const temp = this._all[index]; 22 | this._all[index] = this._all[this._all.length - 1]; 23 | this._all[this._all.length - 1] = temp; 24 | this._all = this._all.slice(0, this._all.length - 1); 25 | } 26 | } 27 | 28 | public clear() { 29 | this._all = []; 30 | } 31 | 32 | public union(other: PairSet) { 33 | const otherAll = other.all; 34 | 35 | if (otherAll.length > 0) 36 | for (let i = 0; i < otherAll.length; i ++) { 37 | const elem = otherAll[i]; 38 | this.add(elem); 39 | } 40 | } 41 | 42 | public except(other: PairSet) { 43 | const otherAll = other.all; 44 | 45 | if (otherAll.length > 0) 46 | for (let i = 0; i < otherAll.length; i ++) { 47 | const elem = otherAll[i]; 48 | this.remove(elem); 49 | } 50 | } 51 | 52 | private _all: Array> = new Array>(); 53 | } 54 | } -------------------------------------------------------------------------------- /source/src/Utils/Collections/Pool.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Pool { 3 | private static _objectQueue = new Map any, any[]>(); 4 | 5 | /** 6 | * 预热缓存,使用最大的cacheCount对象填充缓存 7 | * @param type 要预热的类型 8 | * @param cacheCount 预热缓存数量 9 | */ 10 | public static warmCache(type: new (...args: any[]) => T, cacheCount: number) { 11 | this.checkCreate(type); 12 | const queue = this._objectQueue.get(type); 13 | cacheCount -= queue.length; 14 | 15 | // 如果需要预热更多的对象,则创建并添加到缓存 16 | if (cacheCount > 0) { 17 | for (let i = 0; i < cacheCount; i++) { 18 | queue.push(new type()); 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * 将缓存修剪为cacheCount项目 25 | * @param type 要修剪的类型 26 | * @param cacheCount 修剪后的缓存数量 27 | */ 28 | public static trimCache(type: new (...args) => T, cacheCount: number) { 29 | this.checkCreate(type); 30 | const objectQueue = this._objectQueue.get(type); 31 | 32 | // 如果需要修剪缓存,则弹出多余的对象 33 | while (cacheCount < objectQueue.length) { 34 | objectQueue.pop(); 35 | } 36 | } 37 | 38 | /** 39 | * 清除缓存 40 | * @param type 要清除缓存的类型 41 | */ 42 | public static clearCache(type: new (...args) => T) { 43 | this.checkCreate(type); 44 | const objectQueue = this._objectQueue.get(type); 45 | 46 | // 清空缓存数组 47 | objectQueue.length = 0; 48 | } 49 | 50 | /** 51 | * 如果可以的话,从缓存中获取一个对象 52 | * @param type 要获取的类型 53 | */ 54 | public static obtain(type: new (...args) => T): T { 55 | this.checkCreate(type); 56 | const objectQueue = this._objectQueue.get(type); 57 | 58 | // 如果缓存中有对象,弹出一个并返回 59 | if (objectQueue.length > 0) { 60 | return objectQueue.pop(); 61 | } 62 | 63 | // 如果没有缓存对象,则创建一个新的对象并返回 64 | return new type() as T; 65 | } 66 | 67 | /** 68 | * 将对象推回缓存 69 | * @param type 对象的类型 70 | * @param obj 要推回的对象 71 | */ 72 | public static free(type: new (...args) => T, obj: T) { 73 | this.checkCreate(type); 74 | const objectQueue = this._objectQueue.get(type); 75 | 76 | // 将对象推回缓存 77 | objectQueue.push(obj); 78 | 79 | // 如果对象实现了IPoolable接口,则调用reset方法重置对象 80 | if (isIPoolable(obj)) { 81 | obj.reset(); 82 | } 83 | } 84 | 85 | /** 86 | * 检查缓存中是否已存在给定类型的对象池,如果不存在则创建一个 87 | * @param type 要检查的类型 88 | */ 89 | private static checkCreate(type: new (...args: any[]) => T) { 90 | if (!this._objectQueue.has(type)) { 91 | this._objectQueue.set(type, []); 92 | } 93 | } 94 | } 95 | 96 | export interface IPoolable { 97 | reset(): void; 98 | } 99 | 100 | export const isIPoolable = (props: any): props is IPoolable => { 101 | return typeof props.reset === 'function'; 102 | }; 103 | } -------------------------------------------------------------------------------- /source/src/Utils/Coroutines/Coroutine.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * startCoroutine返回的接口,它提供了中途停止coroutine的能力。 4 | */ 5 | export interface ICoroutine { 6 | /** 7 | * 停止Coroutine 8 | */ 9 | stop(); 10 | /** 11 | * 设置Coroutine是否应该使用deltaTime或unscaledDeltaTime进行计时 12 | * @param useUnscaledDeltaTime 13 | */ 14 | setUseUnscaledDeltaTime(useUnscaledDeltaTime: boolean): ICoroutine; 15 | } 16 | 17 | export class Coroutine { 18 | /** 19 | * 导致Coroutine在指定的时间内暂停。在Coroutine.waitForSeconds的基础上,在Coroutine中使用Yield 20 | * @param seconds 21 | */ 22 | public static waitForSeconds(seconds: number) { 23 | return WaitForSeconds.waiter.wait(seconds); 24 | } 25 | } 26 | 27 | /** 28 | * 帮助类,用于当一个coroutine想要暂停一段时间时。返回Coroutine.waitForSeconds返回其中一个 29 | */ 30 | export class WaitForSeconds { 31 | public static waiter: WaitForSeconds = new WaitForSeconds(); 32 | public waitTime: number = 0; 33 | 34 | public wait(seconds: number): WaitForSeconds { 35 | WaitForSeconds.waiter.waitTime = seconds; 36 | return WaitForSeconds.waiter; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /source/src/Utils/Emitter.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 用于包装事件的一个小类 4 | */ 5 | export class FuncPack { 6 | /** 函数 */ 7 | public func: Function; 8 | /** 上下文 */ 9 | public context: any; 10 | 11 | constructor(func: Function, context: any) { 12 | this.func = func; 13 | this.context = context; 14 | } 15 | } 16 | 17 | /** 18 | * 用于事件管理 19 | */ 20 | export class Emitter { 21 | private _messageTable: Map; 22 | 23 | constructor() { 24 | this._messageTable = new Map(); 25 | } 26 | 27 | /** 28 | * 开始监听项 29 | * @param eventType 监听类型 30 | * @param handler 监听函数 31 | * @param context 监听上下文 32 | */ 33 | public addObserver(eventType: T, handler: Function, context: any) { 34 | let list = this._messageTable.get(eventType); 35 | if (!list) { 36 | list = []; 37 | this._messageTable.set(eventType, list); 38 | } 39 | 40 | if (!this.hasObserver(eventType, handler)) { 41 | list.push(new FuncPack(handler, context)); 42 | } 43 | } 44 | 45 | /** 46 | * 移除监听项 47 | * @param eventType 事件类型 48 | * @param handler 事件函数 49 | */ 50 | public removeObserver(eventType: T, handler: Function) { 51 | let messageData = this._messageTable.get(eventType); 52 | if (messageData) { 53 | let index = messageData.findIndex(data => data.func == handler); 54 | if (index != -1) 55 | messageData.splice(index, 1); 56 | } 57 | } 58 | 59 | /** 60 | * 触发该事件 61 | * @param eventType 事件类型 62 | * @param data 事件数据 63 | */ 64 | public emit(eventType: T, ...data: any[]) { 65 | let list = this._messageTable.get(eventType); 66 | if (list) { 67 | for (let observer of list) { 68 | observer.func.call(observer.context, ...data); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * 判断是否存在该类型的观察者 75 | * @param eventType 事件类型 76 | * @param handler 事件函数 77 | */ 78 | public hasObserver(eventType: T, handler: Function): boolean { 79 | let list = this._messageTable.get(eventType); 80 | return list ? list.some(observer => observer.func === handler) : false; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/src/Utils/Enum.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export enum Edge { 3 | top, 4 | bottom, 5 | left, 6 | right 7 | } 8 | } -------------------------------------------------------------------------------- /source/src/Utils/EqualityComparer.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class EqualityComparer implements IEqualityComparer { 3 | public static default() { 4 | return new EqualityComparer(); 5 | } 6 | 7 | protected constructor() { } 8 | 9 | public equals(x: T, y: T): boolean { 10 | if (typeof x["equals"] == 'function') { 11 | return x["equals"](y); 12 | } else { 13 | return x === y; 14 | } 15 | } 16 | 17 | public getHashCode(o: T): number { 18 | if (typeof o == 'number') { 19 | return this._getHashCodeForNumber(o); 20 | } 21 | 22 | if (typeof o == 'string') { 23 | return this._getHashCodeForString(o); 24 | } 25 | 26 | let hashCode = 385229220; 27 | this.forOwn(o, (value) => { 28 | if (typeof value == 'number') { 29 | hashCode += this._getHashCodeForNumber(value); 30 | } else if (typeof value == 'string') { 31 | hashCode += this._getHashCodeForString(value); 32 | } else if (typeof value == 'object') { 33 | this.forOwn(value, () => { 34 | hashCode += this.getHashCode(value); 35 | }); 36 | } 37 | }); 38 | 39 | return hashCode; 40 | } 41 | 42 | private _getHashCodeForNumber(n: number): number { 43 | return n; 44 | } 45 | 46 | private _getHashCodeForString(s: string): number { 47 | let hashCode = 385229220; 48 | 49 | for (let i = 0; i < s.length; i++) { 50 | hashCode = (hashCode * -1521134295) ^ s.charCodeAt(i); 51 | } 52 | 53 | return hashCode; 54 | } 55 | 56 | private forOwn(object, iteratee){ 57 | object = Object(object); 58 | Object.keys(object).forEach((key) => iteratee(object[key], key, object)); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /source/src/Utils/Extensions/Base64Utils.ts: -------------------------------------------------------------------------------- 1 | module es{ 2 | export class Base64Utils { 3 | private static _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 4 | 5 | /** 6 | * 判断是否原生支持Base64位解析 7 | */ 8 | static get nativeBase64() { 9 | return (typeof (window.atob) === "function"); 10 | } 11 | 12 | 13 | /** 14 | * 解码 15 | * @param input 16 | */ 17 | static decode(input:string): string { 18 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 19 | 20 | if (this.nativeBase64) { 21 | return window.atob(input); 22 | } else { 23 | var output: any = [], chr1: number, chr2: number, chr3: number, enc1: number, enc2: number, enc3: number, enc4: number, i: number = 0; 24 | 25 | while (i < input.length) { 26 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 27 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 28 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 29 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 30 | 31 | chr1 = (enc1 << 2) | (enc2 >> 4); 32 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 33 | chr3 = ((enc3 & 3) << 6) | enc4; 34 | 35 | output.push(String.fromCharCode(chr1)); 36 | 37 | if (enc3 !== 64) { 38 | output.push(String.fromCharCode(chr2)); 39 | } 40 | if (enc4 !== 64) { 41 | output.push(String.fromCharCode(chr3)); 42 | } 43 | } 44 | 45 | output = output.join(""); 46 | return output; 47 | } 48 | } 49 | 50 | 51 | /** 52 | * 编码 53 | * @param input 54 | */ 55 | static encode(input:string): string { 56 | input = input.replace(/\r\n/g, "\n"); 57 | if (this.nativeBase64) { 58 | window.btoa(input); 59 | } else { 60 | var output: any = [], chr1: number, chr2: number, chr3: number, enc1: number, enc2: number, enc3: number, enc4: number, i: number = 0; 61 | while (i < input.length) { 62 | chr1 = input.charCodeAt(i++); 63 | chr2 = input.charCodeAt(i++); 64 | chr3 = input.charCodeAt(i++); 65 | 66 | enc1 = chr1 >> 2; 67 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 68 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 69 | enc4 = chr3 & 63; 70 | 71 | if (isNaN(chr2)) { 72 | enc3 = enc4 = 64; 73 | } else if (isNaN(chr3)) { 74 | enc4 = 64; 75 | } 76 | 77 | output.push(this._keyStr.charAt(enc1)); 78 | output.push(this._keyStr.charAt(enc2)); 79 | output.push(this._keyStr.charAt(enc3)); 80 | output.push(this._keyStr.charAt(enc4)); 81 | } 82 | 83 | output = output.join(""); 84 | return output; 85 | } 86 | } 87 | 88 | 89 | /** 90 | * 解析Base64格式数据 91 | * @param input 92 | * @param bytes 93 | */ 94 | static decodeBase64AsArray(input: string, bytes: number): Uint32Array { 95 | bytes = bytes || 1; 96 | 97 | var dec = Base64Utils.decode(input), i, j, len; 98 | var ar: Uint32Array = new Uint32Array(dec.length / bytes); 99 | 100 | for (i = 0, len = dec.length / bytes; i < len; i++) { 101 | ar[i] = 0; 102 | for (j = bytes - 1; j >= 0; --j) { 103 | ar[i] += dec.charCodeAt((i * bytes) + j) << (j << 3); 104 | } 105 | } 106 | return ar; 107 | } 108 | 109 | /** 110 | * 暂时不支持 111 | * @param data 112 | * @param decoded 113 | * @param compression 114 | * @private 115 | */ 116 | static decompress(data: string, decoded: any, compression: string): any { 117 | throw new Error("GZIP/ZLIB compressed TMX Tile Map not supported!"); 118 | } 119 | 120 | /** 121 | * 解析csv数据 122 | * @param input 123 | */ 124 | static decodeCSV(input: string): Array { 125 | var entries: Array = input.replace("\n", "").trim().split(","); 126 | 127 | var result:Array = []; 128 | for (var i:number = 0; i < entries.length; i++) { 129 | result.push(+entries[i]); 130 | } 131 | return result; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /source/src/Utils/Extensions/EdgeExt.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class EdgeExt { 3 | public static oppositeEdge(self: Edge) { 4 | switch (self) { 5 | case Edge.bottom: 6 | return Edge.top; 7 | case Edge.top: 8 | return Edge.bottom; 9 | case Edge.left: 10 | return Edge.right; 11 | case Edge.right: 12 | return Edge.left; 13 | } 14 | } 15 | 16 | /** 17 | * 如果边是右或左,则返回true 18 | * @param self 19 | */ 20 | public static isHorizontal(self: Edge): boolean{ 21 | return self == Edge.right || self == Edge.left; 22 | } 23 | 24 | /** 25 | * 如果边是顶部或底部,则返回true 26 | * @param self 27 | */ 28 | public static isVertical(self: Edge): boolean { 29 | return self == Edge.top || self == Edge.bottom; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /source/src/Utils/Extensions/NumberExtension.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class NumberExtension { 3 | public static toNumber(value){ 4 | if (value == undefined) return 0; 5 | 6 | return Number(value); 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /source/src/Utils/Extensions/RandomUtils.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class RandomUtils { 3 | /** 4 | * 在 start 与 stop之间取一个随机整数,可以用step指定间隔, 但不包括较大的端点(start与stop较大的一个) 5 | * 如 6 | * this.randrange(1, 10, 3) 7 | * 则返回的可能是 1 或 4 或 7 , 注意 这里面不会返回10,因为是10是大端点 8 | * 9 | * @param start 10 | * @param stop 11 | * @param step 12 | * @return 假设 start < stop, [start, stop) 区间内的随机整数 13 | * 14 | */ 15 | public static randrange(start: number, stop: number, step: number = 1): number { 16 | if (step == 0) 17 | throw new Error('step 不能为 0'); 18 | 19 | let width: number = stop - start; 20 | if (width == 0) 21 | throw new Error('没有可用的范围(' + start + ',' + stop + ')'); 22 | if (width < 0) 23 | width = start - stop; 24 | 25 | let n: number = Math.floor((width + step - 1) / step); 26 | return Math.floor(this.random() * n) * step + Math.min(start, stop); 27 | } 28 | 29 | /** 30 | * 返回a 到 b之间的随机整数,包括 a 和 b 31 | * @param a 32 | * @param b 33 | * @return [a, b] 之间的随机整数 34 | * 35 | */ 36 | public static randint(a: number, b: number): number { 37 | a = Math.floor(a); 38 | b = Math.floor(b); 39 | if (a > b) 40 | a++; 41 | else 42 | b++; 43 | return this.randrange(a, b); 44 | } 45 | 46 | /** 47 | * 返回 a - b之间的随机数,不包括 Math.max(a, b) 48 | * @param a 49 | * @param b 50 | * @return 假设 a < b, [a, b) 51 | */ 52 | public static randnum(a: number, b: number): number { 53 | return this.random() * (b - a) + a; 54 | } 55 | 56 | /** 57 | * 打乱数组 58 | * @param array 59 | * @return 60 | */ 61 | public static shuffle(array: any[]): any[] { 62 | array.sort(this._randomCompare); 63 | return array; 64 | } 65 | 66 | /** 67 | * 从序列中随机取一个元素 68 | * @param sequence 可以是 数组、 vector,等只要是有length属性,并且可以用数字索引获取元素的对象, 69 | * 另外,字符串也是允许的。 70 | * @return 序列中的某一个元素 71 | * 72 | */ 73 | public static choice(sequence: any): any { 74 | if (!sequence.hasOwnProperty("length")) 75 | throw new Error('无法对此对象执行此操作'); 76 | let index: number = Math.floor(this.random() * sequence.length); 77 | if (sequence instanceof String) 78 | return String(sequence).charAt(index); 79 | else 80 | return sequence[index]; 81 | } 82 | 83 | /** 84 | * 对列表中的元素进行随机采æ ? 85 | *
 86 |          * this.sample([1, 2, 3, 4, 5],  3)  // Choose 3 elements
 87 |          * [4, 1, 5]
 88 |          * 
89 | * @param sequence 90 | * @param num 91 | * @return 92 | * 93 | */ 94 | public static sample(sequence: any[], num: number): any[] { 95 | let len: number = sequence.length; 96 | if (num <= 0 || len < num) 97 | throw new Error("采样数量不够"); 98 | 99 | let selected: any[] = []; 100 | let indices: any[] = []; 101 | for (let i: number = 0; i < num; i++) { 102 | let index: number = Math.floor(this.random() * len); 103 | while (indices.indexOf(index) >= 0) 104 | index = Math.floor(this.random() * len); 105 | 106 | selected.push(sequence[index]); 107 | indices.push(index); 108 | } 109 | 110 | return selected; 111 | } 112 | 113 | /** 114 | * 返回 0.0 - 1.0 之间的随机数,等同于 Math.random() 115 | * @return Math.random() 116 | * 117 | */ 118 | public static random(): number { 119 | return Math.random(); 120 | } 121 | 122 | /** 123 | * 计算概率 124 | * @param chance 概率 125 | * @return 126 | */ 127 | public static boolean(chance: number = .5): boolean { 128 | return (this.random() < chance) ? true : false; 129 | } 130 | 131 | private static _randomCompare(a: Object, b: Object): number { 132 | return (this.random() > .5) ? 1 : -1; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /source/src/Utils/Extensions/TextureUtils.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class TextureUtils { 3 | public static premultiplyAlpha(pixels: number[]) { 4 | let b = pixels[0]; 5 | for (let i = 0; i < pixels.length; i += 4) { 6 | if (b[i + 3] != 255) { 7 | let alpha = b[i + 3] / 255; 8 | b[i + 0] = b[i + 0] * alpha; 9 | b[i + 1] = b[i + 1] * alpha; 10 | b[i + 2] = b[i + 2] * alpha; 11 | } 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /source/src/Utils/Extensions/TypeUtils.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class TypeUtils { 3 | public static getType(obj: any){ 4 | return obj.constructor; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /source/src/Utils/GlobalManager.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 全局管理器的基类。所有全局管理器都应该从此类继承。 4 | */ 5 | export class GlobalManager { 6 | /** 7 | * 表示管理器是否启用 8 | */ 9 | public _enabled: boolean; 10 | 11 | /** 12 | * 获取或设置管理器是否启用 13 | */ 14 | public get enabled() { 15 | return this._enabled; 16 | } 17 | 18 | public set enabled(value: boolean) { 19 | this.setEnabled(value); 20 | } 21 | 22 | /** 23 | * 设置管理器是否启用 24 | * @param isEnabled 如果为true,则启用管理器;否则禁用管理器 25 | */ 26 | public setEnabled(isEnabled: boolean) { 27 | if (this._enabled != isEnabled) { 28 | this._enabled = isEnabled; 29 | if (this._enabled) { 30 | // 如果启用了管理器,则调用onEnabled方法 31 | this.onEnabled(); 32 | } else { 33 | // 如果禁用了管理器,则调用onDisabled方法 34 | this.onDisabled(); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * 在启用管理器时调用的回调方法 41 | */ 42 | public onEnabled() { 43 | } 44 | 45 | /** 46 | * 在禁用管理器时调用的回调方法 47 | */ 48 | public onDisabled() { 49 | } 50 | 51 | /** 52 | * 更新管理器状态的方法 53 | */ 54 | public update() { 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /source/src/Utils/Hash.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Hash { 3 | /** 4 | * 从一个字节数组中计算一个哈希值 5 | * @param data 6 | */ 7 | public static computeHash(...data: number[]) { 8 | const p: number = 16777619; 9 | let hash = 2166136261; 10 | 11 | for (let i = 0; i < data.length; i++) 12 | hash = (hash ^ data[i]) * p; 13 | 14 | hash += hash << 13; 15 | hash ^= hash >> 7; 16 | hash += hash << 3; 17 | hash ^= hash >> 17; 18 | hash += hash << 5; 19 | return hash; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /source/src/Utils/IComparer.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface IComparer{ 3 | compare(x: T, y: T): number; 4 | } 5 | } -------------------------------------------------------------------------------- /source/src/Utils/IEqualityComparable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 对象声明自己的平等方法和Hashcode的生成 4 | */ 5 | export interface IEqualityComparable { 6 | /** 7 | * 确定另一个对象是否等于这个实例 8 | * @param other 9 | */ 10 | equals(other: any): boolean; 11 | } 12 | } -------------------------------------------------------------------------------- /source/src/Utils/IEqualityComparer.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 为确定对象的哈希码和两个项目是否相等提供接口 4 | */ 5 | export interface IEqualityComparer { 6 | /** 7 | * 判断两个对象是否相等 8 | * @param x 9 | * @param y 10 | */ 11 | equals(x: T, y: T): boolean; 12 | 13 | /** 14 | * 生成对象的哈希码 15 | * @param value 16 | */ 17 | getHashCode(value: T): number; 18 | } 19 | } -------------------------------------------------------------------------------- /source/src/Utils/IEquatable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 实现该接口用于判定两个对象是否相等的快速接口 4 | */ 5 | export interface IEquatable { 6 | equals(other: T): boolean; 7 | } 8 | } -------------------------------------------------------------------------------- /source/src/Utils/Linq/enumerable.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Enumerable { 3 | /** 4 | * 在指定范围内生成一个整数序列。 5 | */ 6 | public static range(start: number, count: number): List { 7 | let result = new List(); 8 | while (count--) { 9 | result.add(start++) 10 | } 11 | return result 12 | } 13 | 14 | /** 15 | * 生成包含一个重复值的序列。 16 | */ 17 | public static repeat(element: T, count: number): List { 18 | let result = new List(); 19 | while (count--) { 20 | result.add(element) 21 | } 22 | return result 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /source/src/Utils/Linq/helpers.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 检查传递的参数是否为对象 4 | */ 5 | export const isObj = (x: T): boolean => !!x && typeof x === 'object'; 6 | 7 | /** 8 | * 创建一个否定谓词结果的函数 9 | */ 10 | export const negate = ( 11 | pred: (...args: T[]) => boolean 12 | ): ((...args: T[]) => boolean) => (...args) => !pred(...args); 13 | 14 | /** 15 | * 比较器助手 16 | */ 17 | 18 | export const composeComparers = ( 19 | previousComparer: (a: T, b: T) => number, 20 | currentComparer: (a: T, b: T) => number 21 | ): ((a: T, b: T) => number) => (a: T, b: T) => 22 | previousComparer(a, b) || currentComparer(a, b); 23 | 24 | export const keyComparer = ( 25 | _keySelector: (key: T) => string, 26 | descending?: boolean 27 | ): ((a: T, b: T) => number) => (a: T, b: T) => { 28 | const sortKeyA = _keySelector(a); 29 | const sortKeyB = _keySelector(b); 30 | if (sortKeyA > sortKeyB) { 31 | return !descending ? 1 : -1 32 | } else if (sortKeyA < sortKeyB) { 33 | return !descending ? -1 : 1 34 | } else { 35 | return 0 36 | } 37 | }; 38 | } -------------------------------------------------------------------------------- /source/src/Utils/Out.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Out { 3 | public value: T; 4 | 5 | constructor(value: T = null) { 6 | this.value = value; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /source/src/Utils/PolygonLight/EndPoint.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 一段的终点 4 | */ 5 | export class EndPoint { 6 | /** 该部分的位置 */ 7 | public position: Vector2; 8 | /** 如果这个端点是一个段的起始点或终点(每个段只有一个起始点和一个终点) */ 9 | public begin: boolean; 10 | /** 该端点所属的段 */ 11 | public segment: Segment; 12 | /** 端点相对于能见度测试位置的角度 */ 13 | public angle: number; 14 | 15 | constructor() { 16 | this.position = Vector2.zero; 17 | this.begin = false; 18 | this.segment = null; 19 | this.angle = 0; 20 | } 21 | } 22 | 23 | export class EndPointComparer implements IComparer { 24 | /** 25 | * 按角度对点进行排序的比较功能 26 | * @param a 27 | * @param b 28 | */ 29 | public compare(a: EndPoint, b: EndPoint) { 30 | // 按角度顺序移动 31 | if (a.angle > b.angle) 32 | return 1; 33 | 34 | if (a.angle < b.angle) 35 | return -1; 36 | 37 | // 但对于纽带,我们希望Begin节点在End节点之前 38 | if (!a.begin && b.begin) 39 | return 1; 40 | 41 | if (a.begin && !b.begin) 42 | return -1; 43 | 44 | return 0; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /source/src/Utils/PolygonLight/Segment.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 表示可见性网格中的遮挡线段 4 | */ 5 | export class Segment { 6 | /** 7 | * 该部分的第一个终点 8 | */ 9 | public p1: EndPoint; 10 | /** 11 | * 该部分的第二个终点 12 | */ 13 | public p2: EndPoint; 14 | 15 | constructor(){ 16 | this.p1 = null; 17 | this.p2 = null; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /source/src/Utils/Ref.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Ref { 3 | public value: T; 4 | 5 | constructor(value: T) { 6 | this.value = value; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /source/src/Utils/Screen.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export class Screen { 3 | public static width: number; 4 | public static height: number; 5 | 6 | public static get size(): Vector2 { 7 | return new Vector2(this.width, this.height); 8 | } 9 | 10 | public static get center(): Vector2 { 11 | return new Vector2(this.width / 2, this.height / 2); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /source/src/Utils/SubpixelNumber.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 管理数值的简单助手类。它存储值,直到累计的总数大于1。一旦超过1,该值将在调用update时添加到amount中。 4 | */ 5 | export class SubpixelNumber { 6 | public remainder: number; 7 | 8 | /** 9 | * 以amount递增余数,将值截断为int,存储新的余数并将amount设置为当前值。 10 | * @param amount 11 | */ 12 | public update(amount: number){ 13 | this.remainder += amount; 14 | let motion = Math.trunc(this.remainder); 15 | this.remainder -= motion; 16 | return motion; 17 | } 18 | 19 | /** 20 | * 将余数重置为0。当一个物体与一个不可移动的物体碰撞时有用。 21 | * 在这种情况下,您将希望将亚像素余数归零,因为它是空的和无效的碰撞。 22 | */ 23 | public reset(){ 24 | this.remainder = 0; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /source/src/Utils/Timers/ITimer.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface ITimer { 3 | context: any; 4 | 5 | /** 6 | * 调用stop以停止此计时器再次运行。这对非重复计时器没有影响。 7 | */ 8 | stop(); 9 | 10 | /** 11 | * 将计时器的运行时间重置为0 12 | */ 13 | reset(); 14 | 15 | /** 16 | * 返回投向T的上下文,作为方便 17 | */ 18 | getContext(): T; 19 | } 20 | } -------------------------------------------------------------------------------- /source/src/Utils/Timers/Timer.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 私有类隐藏ITimer的实现 4 | */ 5 | export class Timer implements ITimer{ 6 | public context: any; 7 | public _timeInSeconds: number = 0; 8 | public _repeats: boolean = false; 9 | public _onTime: (timer: ITimer) => void; 10 | public _isDone: boolean = false; 11 | public _elapsedTime: number = 0; 12 | 13 | public getContext(): T { 14 | return this.context as T; 15 | } 16 | 17 | public reset() { 18 | this._elapsedTime = 0; 19 | } 20 | 21 | public stop() { 22 | this._isDone = true; 23 | } 24 | 25 | public tick(){ 26 | // 如果stop在tick之前被调用,那么isDone将为true,我们不应该再做任何事情 27 | if (!this._isDone && this._elapsedTime > this._timeInSeconds){ 28 | this._elapsedTime -= this._timeInSeconds; 29 | this._onTime(this); 30 | 31 | if (!this._isDone && !this._repeats) 32 | this._isDone = true; 33 | } 34 | 35 | this._elapsedTime += Time.deltaTime; 36 | 37 | return this._isDone; 38 | } 39 | 40 | public initialize(timeInsSeconds: number, repeats: boolean, context: any, onTime: (timer: ITimer)=>void){ 41 | this._timeInSeconds = timeInsSeconds; 42 | this._repeats = repeats; 43 | this.context = context; 44 | this._onTime = onTime.bind(context); 45 | } 46 | 47 | /** 48 | * 空出对象引用,以便在js需要时GC可以清理它们的引用 49 | */ 50 | public unload(){ 51 | this.context = null; 52 | this._onTime = null; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /source/src/Utils/Timers/TimerManager.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 允许动作的延迟和重复执行 4 | */ 5 | export class TimerManager extends GlobalManager { 6 | public _timers: Timer[] = []; 7 | 8 | public update() { 9 | for (let i = this._timers.length - 1; i >= 0; i --){ 10 | if (this._timers[i].tick()){ 11 | this._timers[i].unload(); 12 | this._timers.splice(i, 1); 13 | } 14 | } 15 | } 16 | 17 | /** 18 | * 调度一个一次性或重复的计时器,该计时器将调用已传递的动作 19 | * @param timeInSeconds 20 | * @param repeats 21 | * @param context 22 | * @param onTime 23 | */ 24 | public schedule(timeInSeconds: number, repeats: boolean, context: any, onTime: (timer: ITimer)=>void){ 25 | let timer = new Timer(); 26 | timer.initialize(timeInSeconds, repeats, context, onTime); 27 | this._timers.push(timer); 28 | 29 | return timer; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /source/src/Utils/Triangulator.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | /** 3 | * 简单的剪耳三角测量器,最终的三角形将出现在triangleIndices列表中。 4 | */ 5 | export class Triangulator { 6 | /** 7 | * 上次三角函数调用中使用的点列表的三角列表条目索引 8 | */ 9 | public triangleIndices: number[] = []; 10 | 11 | private _triPrev: number[] = new Array(12); 12 | private _triNext: number[] = new Array(12); 13 | 14 | public static testPointTriangle(point: Vector2, a: Vector2, b: Vector2, c: Vector2): boolean { 15 | // 如果点在AB的右边,那么外边的三角形是 16 | if (Vector2Ext.cross(point.sub(a), b.sub(a)) < 0) 17 | return false; 18 | 19 | // 如果点在BC的右边,则在三角形的外侧 20 | if (Vector2Ext.cross(point.sub(b), c.sub(b)) < 0) 21 | return false; 22 | 23 | // 如果点在ca的右边,则在三角形的外面 24 | if (Vector2Ext.cross(point.sub(c), a.sub(c)) < 0) 25 | return false; 26 | 27 | // 点在三角形上 28 | return true; 29 | } 30 | 31 | /** 32 | * 计算一个三角形列表,该列表完全覆盖给定点集所包含的区域。如果点不是CCW,则将arePointsCCW参数传递为false 33 | * @param points 定义封闭路径的点列表 34 | * @param arePointsCCW 35 | */ 36 | public triangulate(points: Vector2[], arePointsCCW: boolean = true) { 37 | let count = points.length; 38 | 39 | // 设置前一个链接和下一个链接 40 | this.initialize(count); 41 | 42 | // 非三角的多边形断路器 43 | let iterations = 0; 44 | 45 | // 从0开始 46 | let index = 0; 47 | 48 | // 继续移除所有的三角形,直到只剩下一个三角形 49 | while (count > 3 && iterations < 500) { 50 | iterations++; 51 | 52 | let isEar = true; 53 | 54 | let a = points[this._triPrev[index]]; 55 | let b = points[index]; 56 | let c = points[this._triNext[index]]; 57 | 58 | if (Vector2Ext.isTriangleCCW(a, b, c)) { 59 | let k = this._triNext[this._triNext[index]]; 60 | do { 61 | if (Triangulator.testPointTriangle(points[k], a, b, c)) { 62 | isEar = false; 63 | break; 64 | } 65 | 66 | k = this._triNext[k]; 67 | } while (k != this._triPrev[index]); 68 | } else { 69 | isEar = false; 70 | } 71 | 72 | if (isEar) { 73 | this.triangleIndices.push(this._triPrev[index]); 74 | this.triangleIndices.push(index); 75 | this.triangleIndices.push(this._triNext[index]); 76 | 77 | // 删除vert通过重定向相邻vert的上一个和下一个链接,从而减少vertext计数 78 | this._triNext[this._triPrev[index]] = this._triNext[index]; 79 | this._triPrev[this._triNext[index]] = this._triPrev[index]; 80 | count--; 81 | 82 | // 接下来访问前一个vert 83 | index = this._triPrev[index]; 84 | } else { 85 | index = this._triNext[index]; 86 | } 87 | } 88 | 89 | this.triangleIndices.push(this._triPrev[index]); 90 | this.triangleIndices.push(index); 91 | this.triangleIndices.push(this._triNext[index]); 92 | 93 | if (!arePointsCCW) 94 | this.triangleIndices.reverse(); 95 | } 96 | 97 | private initialize(count: number) { 98 | this.triangleIndices.length = 0; 99 | 100 | if (this._triNext.length < count) { 101 | this._triNext.reverse(); 102 | this._triNext.length = Math.max(this._triNext.length * 2, count); 103 | } 104 | if (this._triPrev.length < count) { 105 | this._triPrev.reverse(); 106 | this._triPrev.length = Math.max(this._triPrev.length * 2, count); 107 | } 108 | 109 | for (let i = 0; i < count; i++) { 110 | this._triPrev[i] = i - 1; 111 | this._triNext[i] = i + 1; 112 | } 113 | 114 | this._triPrev[0] = count - 1; 115 | this._triNext[count - 1] = 0; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /source/src/Utils/prelog.ts: -------------------------------------------------------------------------------- 1 | module es { 2 | export interface Class extends Function {} 3 | 4 | export function getClassName(klass): string { 5 | return klass.className || klass.name; 6 | } 7 | } -------------------------------------------------------------------------------- /source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "downlevelIteration": true, 4 | "importHelpers": true, 5 | "pretty": true, 6 | "preserveConstEnums": true, 7 | "alwaysStrict": true, 8 | "module": "system", 9 | "target": "es5", 10 | "declaration": true, 11 | "sourceMap": false, 12 | "removeComments": false, 13 | "outFile": "./framework.js", 14 | "lib": [ 15 | "es5", 16 | "dom", 17 | "es2015.promise", 18 | "es6" 19 | ] 20 | }, 21 | "include": [ 22 | "src", 23 | "lib" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } -------------------------------------------------------------------------------- /sponsor/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esengine/ecs-framework/9909a7f7b07d8a4801593e4fe59590463d5d1814/sponsor/alipay.jpg -------------------------------------------------------------------------------- /sponsor/wechatpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esengine/ecs-framework/9909a7f7b07d8a4801593e4fe59590463d5d1814/sponsor/wechatpay.png --------------------------------------------------------------------------------