diff --git a/README.md b/README.md index 3fd6405b..7d65f9b2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ecs-framework 的目标是成为功能强大的框架。它为您构建游戏提 ## 交流群 点击链接加入群聊【ecs游戏框架交流】:https://jq.qq.com/?_wv=1027&k=29w1Nud6 -## 快速开始 +## 快速开始 [进入文档](docs/getting_start.md) - 初始化框架 ```typescript es.Core.create(); diff --git a/docs/coroutine.md b/docs/coroutine.md new file mode 100644 index 00000000..19bddbeb --- /dev/null +++ b/docs/coroutine.md @@ -0,0 +1,189 @@ +# Coroutine +## 协程介绍 +框架的协程系统是基于js的一个简单而强大的迭代器。这一点你不必关注太多,我们直接进入一个简单的例子来看看协程到底能干什么。首先,我们来看一下这段简单的代码 + +### 倒计时器 +这是一个简单的脚本组件,只做了倒计时,并且在到达0的时候log一个信息。 +```typescript +export class AComponent extends es.Component implements es.IUpdatable { + public timer = 3; + update() { + this.timer -= es.Time.deltaTime; + if(this.timer <= 0) { + console.Log("Timer has finished!"); + } + } +} +``` + +还不错,代码简短实用,但问题是,如果我们需要复杂的脚本组件(像一个角色或者敌人的类),拥有多个计时器呢?刚开始的时候,我们的代码也许会是这样的: + +```typescript +export class AComponent extends es.Component implements es.IUpdatable +{ + public firstTimer = 3; + public secondTimer = 2; + public thirdTimer = 1; + update() { + this.firstTimer -= es.Time.deltaTime; + if(this.firstTimer <= 0) + console.Log("First timer has finished!"); + this.secondTimer -= es.Time.deltaTime; + if(this.secondTimer <= 0) + console.Log("Second timer has finished!"); + this.thirdTimer -= es.Time.deltaTime; + if(this.thirdTimer <= 0) + console.Log("Third timer has finished!"); + } +} + +``` + +尽管不是太糟糕,但是我个人不是很喜欢自己的代码中充斥着这些计时器变量,它们看上去很乱,而且当我需要重新开始计时的时候还得记得去重置它们(这活我经常忘记做)。 + + + +如果我只用一个for循环来做这些,看上去是否会好很多? + +```typescript +for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime) { + //Just do nothing... +} +console.Log("This happens after 5 seconds!"); +``` + +现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。 + + + +好的,你可能现在明白我的意思:协程可以做的正是这一点! + +## 码入你的协程! + +现在,这里提供了上面例子运用协程的版本!我建议你从这里开始跟着我来写一个简单的脚本组件,这样你可以在你自己的程序中看到它是如何工作的。 + +```typescript +export class AComponent extends es.Component implements es.IUpdatable +{ + onAddedToEntity() { + es.Core.startCoroutine(this.countdown()); + } + + *countdown() { + for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime) + yield null; + console.Log("This message appears after 3 seconds!"); + } +} + +``` + +这看上去有点不一样,没关系,接下来我会解释这里到底发生了什么。 + +```typescript +es.Core.startCoroutine(this.countdown()); +``` + +这一行用来开始我们的countdown程序,注意,我并没有给它传入参数,但是这个方法调用了它自己(这是通过传递countdown的yield返回值来实现的)。 + +### Yield + +为了能在连续的多帧中(在这个例子中,3秒钟等同于很多帧)调用该方法,框架必须通过某种方式来存储这个方法的状态,这是通过迭代器中使用yield语句得到的返回值,当你`yield`一个方法时,你相当于说了,**现在停止这个方法,然后在下一帧中从这里重新开始!**。 + +> 注意:用0或者null来yield的意思是告诉协程等待下一帧,直到继续执行为止。当然,同样的你可以继续yield其他协程,我会在下一个教程中讲到这些。 + +## 一些例子 + +协程在刚开始接触的时候是非常难以理解的,无论是新手还是经验丰富的程序员我都见过他们对于协程语句一筹莫展的时候。因此我认为通过例子来理解它是最好的方法,这里有一些简单的协程例子: + +### 多次输出Hello + +记住,yield是 **停止执行方法,并且在下一帧从这里重新开始**,这意味着你可以这样做: + +```typescript +//这将打招呼 5 次,每帧一次,持续 5 帧 +*sayHelloFiveTimes() { + yield null; + console.Log("Hello"); + yield null; + console.Log("Hello"); + yield null; + console.Log("Hello"); + yield null; + console.Log("Hello"); + yield null; + console.Log("Hello"); +} +//这将做与上述功能完全相同的事情! +*sayHello5Times() { + for(let i = 0; i < 5; i++) { + console.Log("Hello"); + yield null; + } +} +``` + +### 每一帧输出“Hello”,无限循环。。。 + +通过在一个while循环中使用yield,你可以得到一个无限循环的协程,这几乎就跟一个update()循环等同 + +```typescript +//一旦启动,这将一直运行直到手动停止 +*sayHelloEveryFrame(){ + while(true) { + console.Log("Hello"); + yield null; + } +} +``` + +### 计时 +不过跟update()不一样的是,你可以在协程中做一些更有趣的事 + +```typescript +*countSeconds(){ + let seconds = 0; + while(true) + { + // 1秒后执行下一帧 + yield 1; + seconds++; + console.Log("自协程启动以来已经过去了" + seconds + "秒钟."); + } +} +``` + +这个方法突出了协程一个非常酷的地方:方法的状态被存储了,这使得方法中定义的这些变量都会保存它们的值,即使是在不同的帧中。还记得这个教程开始时那些烦人的计时器变量吗?通过协程,我们再也不需要担心它们了,只需要把变量直接放到方法里面! + +### 开始和终止协程 + +之前,我们已经学过了通过 es.Core.startCoroutine()方法来开始一个协程,就像这样: + +```typescript +const coroutine = es.Core.startCoroutine(this.countdown()); +``` + +我们可以像这样停止协程 + +```typescript +coroutine.stop(); +``` + +或者你可以再迭代器内返回`yield "break"`方式中止协程 + +```typescript +*countSeconds(){ + let seconds = 0; + while(true) + { + for(let timer = 0; timer < 1; timer += es.Time.deltaTime) + yield null; + seconds++; + console.Log("自协程启动以来已经过去了" + seconds + "秒钟."); + + // 如果大于10秒,终止协程 + if (second > 10) + yield "break"; + } +} +``` \ No newline at end of file diff --git a/docs/create_entity_component.md b/docs/create_entity_component.md new file mode 100644 index 00000000..a9165de1 --- /dev/null +++ b/docs/create_entity_component.md @@ -0,0 +1,121 @@ +# 创建实体 + +实体必须依赖于场景,不能单独存在。创建实体方法由场景提供。 + +## 方式一 +```typescript +// 通过全局快捷获取场景创建实体 +const playerEntity = es.Core.scene.createEntity("player"); +``` + +## 方式二 +```typescript +export class MainScene extends es.Scene { + onStart() { + // 通过场景内创建 + const playerEntity = this.createEntity("player"); + } +} +``` + +### Transform + +框架中提供的实体不同于其他框架实体,它更偏向于游戏使用,实体内含有`Transform`属性。可用于快速访问位置,旋转,缩放等。如果需要应用于游戏引擎,请再组件重写`onTransformChanged`监听这些属性的变化。 + +> 实体内包含对transform里位置、旋转、缩放的快捷tween方法。`tweenPositionTo`/`tweenLocalPositionTo`/`tweenScaleTo`/`tweenLocalScaleTo`/`tweenRotationDegreesTo`/`tweenLocalRotationDegreesTo` + +### tag / setTag + +实体还提供`tag`属性及`setTag`方法来快速设置实体的标记,可再场景中使用`findEntitiesWithTag`快速查询拥有该标记的实体或使用`findEntityWithTag`来查找第一个拥有该标记的实体,你可以把它当作组来使用。 + +### detachFromScene / attachToScene +当你不想实体与场景被销毁时一同被销毁。可先 `detachFromScene`,等待合适的时机再调用 `attachToScene` 放入新的场景。 + + +# 创建组件 + +组件一般配合实体使用。组件需要继承 `es.Component` 来标识为组件,如果想让组件拥有每帧更新能力则额外继承`es.IUpdatable` 接口。在实现的`update`方法当中进行更新逻辑。 + +```typescript +// es.IUpdatable接口为可选接口,如果不需要更新能力则不必继承 +export class AComponent extends es.Component implements es.IUpdatable { + update() { + // 更新逻辑 + } +} +``` + +## 加入组件 + +组件必须挂载于实体上,不能单独存在,如果需要单独于场景的组件则参考 [es.SceneComponent](scene_component.md) 组件。 + +- 方式一:将现有的AComponent加入实体 +```typescript +const aCom = playerEntity.addComponent(new AComponent()); +``` + +- 方式二:在实体上直接创建组件 +```typescript +const aCom = playerEntity.createComponent(AComponent); +``` + +## 获取组件 + +- 方式一: 根据类型获取找到满足条件的第一个组件 +```typescript +// 不能保证已经加入场景 +const aCom = playerEntity.getComponent(AComponent); +``` + +```typescript +// 保证已经加入场景 +const aCom = playerEntity.getComponentInScene(AComponent); +``` + +- 方式二: 尝试找到一个组件,返回是否找到组件标志,第二参数需要一个引用组件用于存储已找到的组件 +```typescript +const outCom = new Ref(); +const find = playerEntity.tryGetComponent(AComponent, outCom); +if (find) { + const aCom = outCom.value; +} +``` + +- 方式三:获取该类型的组件,如果未找到则创建一个并返回 +```typescript +const aCom = playerEntity.getOrCreateComponent(AComponent); +``` + +- 方式四:根据第二参数中的列表找到该类型的所有组件并返回 +```typescript +const findArray: Component[] = [ + new AComponent(), + new BComponent(), + new CComponent() +]; +// findArray可不传,则在实体上寻找满足第一个条件的所有组件 +const coms = playerEntity.getComponents(AComponent, findArray); +``` + +- 组件是否存在 + +```typescript +const find = playerEntity.hasComponent(AComponent); +``` + +## 移除组件 + +- 方式一: 移除已实例组件 +```typescript +playerEntity.removeComponent(aCom); +``` + +- 方式二:移除满足类型的第一个组件 +```typescript +playerEntity.removeComponentForType(AComponent); +``` + +- 方式三: 移除所有组件 +```typescript +playerEntity.removeAllComponents(); +``` \ No newline at end of file diff --git a/docs/getting_start.md b/docs/getting_start.md new file mode 100644 index 00000000..785a2053 --- /dev/null +++ b/docs/getting_start.md @@ -0,0 +1,70 @@ +# 如何开始 + +## 初始化框架 + +```typescript +// 参数为false则开启debug模式 +es.Core.create(false); +``` + +## 分发帧事件 + +```typescript +// 放置于引擎每帧更新处 +// dt为可选参数,传入引擎的deltaTime代替框架内的es.Time.deltaTime +es.Core.emitter.emit(es.CoreEvents.frameUpdated, dt); +``` + +> 尽可能使用引擎的dt,以免再游戏暂停继续时由于dt导致的跳帧问题 + +> **您还需要一个默认的场景以使得游戏可以进行使用ecs框架以及物理类或tween系统** + +## 创建场景类 + +场景类需要继承框架中的 `es.Scene` + +```typescript +export class MainScene extends es.Scene { + /** + * 可重写方法,从contructor中调用这个函数 + */ + initialize() { + console.log('initialize'); + } + + /** + * 可重写方法。当Core将这个场景设置为活动场景时调用 + */ + onStart() { + console.log('onStart'); + } + + /** + * 可重写方法。当Core把这个场景从活动槽中移除时调用。 + */ + unload() { + console.log('unload'); + } + + /** + * 可重写方法。 + */ + update() { + // 如果重写update方法 一定要调用该方法 + // 不调用将导致实体无法加入/组件无法更新 + super.update(); + } +} +``` + +要想激活该场景需要通过核心类 `Core` 来设置当前 `MainScene` 为使用的场景 + +```typescript +es.Core.scene = new MainScene(); +``` + +# 下一章节 +- [创建实体与组件](create_entity_component.md) +- [创建系统](system.md) +- [全局时间Time](time.md) +- [协程Coroutine](coroutine.md) \ No newline at end of file diff --git a/docs/scene_component.md b/docs/scene_component.md new file mode 100644 index 00000000..6af7cc01 --- /dev/null +++ b/docs/scene_component.md @@ -0,0 +1,71 @@ +# scene_component +这是一个场景组件的基类,如果您需要一个不在实体上的组件则继承它 `es.SceneComponent`。场景组件默认包含`update`/`onEnabled`/`onDisabled`/`onRemovedFromScene`,你可以对他们进行重载。 + +```typescript +export class ASceneComponent extends es.SceneComponent { + /** + * 在启用此SceneComponent时调用 + */ + onEnabled() { + + } + + /** + * 当禁用此SceneComponent时调用 + */ + onDisabled() { + + } + + /** + * 当该SceneComponent从场景中移除时调用 + */ + onRemovedFromScene() { + + } + + update() { + + } +} +``` + +- 场景组件需要添加至场景上, 通过场景中的 `addSceneComponent` 方法加入。 + +```typescript +export class MainScene extends es.Scene { + onStart() { + const aSceneCom = this.addSceneComponent(new ASceneComponent()); + } +} +``` + +- 如果想要获取该场景组件则通过`getSceneComponent`方法获取 + +```typescript +export class MainScene extends es.Scene { + onStart() { + const aSceneCom = this.getSceneComponent(ASceneComponent); + } +} +``` + +- 如果获取时发现没有可以自动创建则通过 `getOrCreateSceneComponent` 方法 + +```typescript +export class MainScene extends es.Scene { + onStart() { + const aSceneCom = this.getOrCreateSceneComponent(ASceneComponent); + } +} +``` + +- 删除场景组件 + +```typescript +export class MainScene extends es.Scene { + onStart() { + this.removeSceneComponent(aSceneCom); + } +} +``` \ No newline at end of file diff --git a/docs/system.md b/docs/system.md new file mode 100644 index 00000000..12ecbad6 --- /dev/null +++ b/docs/system.md @@ -0,0 +1,67 @@ +# system +系统是ecs的核心。你的游戏逻辑应该在这里进行处理,所有的实体及组件也会在这里进行集中处理。用于处理实体的系统叫做 `es.EntityProcessingSystem`。 你需要继承他并实现`processEntity(entity: Entity)`方法。 + +```typescript +export class ASystem extends es.EntityProcessingSystem { + processEntity(entity: Entity){ + + } +} +``` + +系统也依赖于场景,如果想要系统被激活则需要使用场景中`addEntityProcessor`方法。系统被实例化需要传入一个`es.Matcher` 参数。 + +```typescript +export class MainScene extends es.Scene { + onStart() { + this.addEntityProcessor(new ASystem(es.Matcher.empty().all(AComponent))); + } +} +``` + +## Matcher +matcher是系统的匹配器,用于匹配满足条件的实体传入系统进行处理。如果想要一个空的匹配器则直接 `es.Matcher.empty()` + +- all +同时拥有多个组件 + +```typescript +es.Matcher.empty().all(AComponent, BComponent); +``` + +- one +拥有任意一个组件 + +```typescript +es.Matcher.empty().one(AComponent, BComponent); +``` + +- exclude +拥有某些组件,并且不包含某些组件 +```typescript +// 不包含CComponent或者DComponent +es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent, DComponent); + +// 不同时包含CComponent和DComponent +es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent).exclude(DComponent); +``` + +## 获取系统 + +```typescript +export class MainScene extends es.Scene { + onStart() { + const aSystem = this.getEntityProcessor(ASystem); + } +} +``` + +## 移除系统 + +```typescript +export class MainScene extends es.Scene { + onStart() { + this.removeEntityProcessor(aSystem); + } +} +``` \ No newline at end of file diff --git a/docs/time.md b/docs/time.md new file mode 100644 index 00000000..89c79bc7 --- /dev/null +++ b/docs/time.md @@ -0,0 +1,23 @@ +# Time +游戏中会经常使用到关于时间类。框架内提供了关于时间的多个属性 + +## 游戏运行的总时间 +`es.Time.totalTime` + +## deltaTime的未缩放版本。不受时间尺度的影响 +`es.Time.unscaledDeltaTime` + +## 前一帧到当前帧的时间增量(按时间刻度进行缩放) +`es.Time.deltaTime` + +## 时间刻度缩放 +`es.Time.timeScale` + +## DeltaTime可以为的最大值 +`es.Time.maxDeltaTime` 默认为Number.MAX_VALUE + +## 已传递的帧总数 +`es.Time.frameCount` + +## 自场景加载以来的总时间 +`es.Time.timeSinceSceneLoad` \ No newline at end of file