把NFT导入高素质的场景,即能获得100个MANA - 只限于500个最佳的场景
X
你好,请选择
语言:
关闭

Systems 系统

当场景发生变化时,Decentraland 场景使用 systems 系统 来更新存储在每个实体组件中的信息。

systems 系统 使得场景动态化,它在场景游戏循环的每帧定期执行函数,改变渲染的内容。

以下是基本的 system 声明示例:

// Define the system
export class MoveSystem implements ISystem {
  //Executed ths function on every frame
  update() {
    // Iterate over the entities in an component group
    for (let entity of myEntityGroup.entities) {
      let transform = entity.getComponent(Transform)
      transform.translate(Vector3.Forward)
    }
  }
}

// Add system to engine
engine.addSystem(new MoveSystem())

在上面的例子中,MoveSystem 在游戏循环中的每帧中都会执行的 update() 函数,改变场景中每个实体的位置。

注意:必须先将 System 添加到引擎才会调用其函数。

通过改变存储在实体组件中的值,所有 System 系统能对实体起作用。

在场景中针对不同的行为可以使用多个系统,从而使您的代码更清晰,更容易扩展。 例如,一个系统用于处理物理现象,一个系统使实体连续来回移动,另一个系统可以处理人物的 AI。

多个系统也可以作用于单个实体,例如,非玩家角色可能会根据其 AI 系统自行移动,但走到悬崖边时显然会受到重力系统的影响而掉下。

update 方法

update() 是一个虚函数,您需要扩展它来定义系统的功能。 意味着需要把它重写从而以预先建立的方式与引擎交互。

系统的 update() 方法定期执行,会在游戏循环中的每帧调用一次。并且它自动发生的,您无需在代码中的任何位置显式调用此函数。

通常,update() 方法是实现系统大部分逻辑的地方。

在 Decentraland 场景中,您可以将游戏循环视为场景系统中所有 update() 函数的集合。

遍历组件组

大多数情况下,您并不会希望系统的 update 函数遍历场景中的全部实体,因为这样会消耗大量的处理性能。 为避免出现这种情况,您可以创建组件组来跟踪相关实体, 然后让你的系统遍历该列表。

例如,场景有一个 PhysicsSystem,用于计算重力对场景实体的影响。 场景中的某些实体(例如树木)是固定的,因此在计算重力对这些实体的影响时,避免浪费性能是有意义的。 所以,您可以定义一个组件组,该组件组跟踪不固定的实体,然后让 PhysicsSystem 仅处理该组中的实体。

// Create component group
const movableEntities = engine.getComponentGroup(Physics)

// Create system
export class PhysicsSystem implements ISystem {
  update(dt: number) {
    // Iterate over component group
    for (let entity of movableEntities.entities) {
      // Calculate effect of physics
    }
  }
}

处理单个实体

某些组件和系统仅用于场景中的一个实体。 例如,存储游戏分数的实体,或者场景中只有一扇门。 要从系统中访问其中一个实体,可能并不希望创建仅包含一个实体的组件组。

如果在定义系统的同一文件中创建了实体,则可以在系统函数中按名称简单地引用实体或其组件。

export class UpdateScore implements ISystem {
  update(dt: number) {   
    log(score.points)
    // ...
  }
}

game = new Entity()
let score = new Score()
game.addComponent(score)
engine.addSystem(new UpdateScore(game))

对于较大的项目,我们建议您将系统定义与实体实例保存在不同的单独文件中。 如果这样,那么从系统中引用实体就更困难了,因为你不能只是将game.ts 中的实体导入到系统的模块中。

由于系统也是对象,因此您可以自由地添加变量,还可以定义构造函数以将值传递给这些变量。

export class UpdateScore implements ISystem {
  score: GameScore // the type is a custom component
  constructor(scoreComponent){
      this.score = scoreComponent
  }
  update(dt: number) {
    log(this.score.points)
    // update values the score object
  }
}

game.ts 文件中实例化系统时,必须传入组件的引用参数:

game = new Entity()
let score = new Score()
game.addComponent(score)
engine.addSystem(new UpdateScore(game))

engine.addSystem(new UpdateScore(score))

注意:为了简单化,您可以将此数据存储在自定义对象中而不是自定义组件中。

在添加实体时执行

onAddEntity() 是系统的另一个虚函数,你可以使用自己的代码覆盖它。

每次将新实体添加到引擎时,都会调用此函数一次,并将新实体作为参数传递。

export class mySystem implements ISystem {
  onAddEntity(entity: Entity){
    // Code to run once
  }
}

在删除实体时执行

onRemoveEntity() 是系统的另一个虚函数,您可以使用自己的代码覆盖它。

每次从引擎中删除实体时,都会调用此函数一次,并将删除的实体作为参数传递。

export class mySystem implements ISystem {
  onRemoveEntity(entity: Entity){}
    // Code to run once
  }
}

系统激活时执行

activate() 函数是另一个范例,它会在系统激活时执行一次。

 export class mySystem implements ISystem {
   activate(){}
     // Code to run once
   }
 }

这对 system 需要执行一些初始化时很有用,例如,创建一组不同颜色的材质,用于系统的 update 函数切换使用。 使用 activate 函数而不是在场景开始时直接执行这些步骤的优点是,如果您的场景从不使用 system ,它就不会被执行。

帧间 Delta

update() 方法总会接收一个名为 dt 的参数,其类型为 number(表示 delta time),即使未明确声明此参数。

export class mySystem implements ISystem {
  update(dt: number) {
    // Update scene
  }
}

delta time 表示处理最后一帧所用的时间,以毫秒为单位。

默认情况下,Decentraland 场景以每秒 30 帧的速度更新。 这意味着传递给所有 update() 方法的 dt 参数通常等于 30/1000

如果帧的处理花费的时间少于此间隔,则引擎将等待剩余时间以保持定期更新并且 dt 将保持等于 30/1000

如果帧的处理时间超过 30/1000 毫秒,则该帧的绘制将延迟。 然后引擎尝试完成该帧并尽快显示它。 然后它进入下一帧并尝试在最后一帧之后的 30/1000 毫秒显示它。 它不能弥补先前的延迟。

理想情况下,您应该尽量避免让场景达到这种状态,因为它会导致帧速率下降。

当帧处理超过默认时间时,dt 变量就有用了。 假设当前帧花费的时间与前一帧相同,该信息可用于计算调整变化的程度,以使其保持稳定并与帧之间的滞后成比例。

有关如何使用 dt 使运动更平滑的示例,请参阅 实体定位

固定时间循环执行

如果您希望系统以固定的时间间隔执行某些操作,可以通过将 dt 参数与计时器相结合来实现。

 let timer: number = 10

 export class LoopSystem {
   update() {
     if (timer > 0) {
       timer -= dt
     } else {
         timer = 10
         // DO SOMETHING
     }
   }
 }

对于更复杂的用例,可能会动态创建多个循环,可以创建一个用于存储计时器的组件。

 // define custom component
 @Component("timer")
 export class Timer {
     totalTime: number
     timeLeft: number
     constructor( time: number){
          this.totalTime = time
          this.timeLeft = time
     }
 }

 // component group to list all entities with a Timer component
 export const timers = engine.getComponentGroup(Timer)

 // system to update all timers
 export class LoopSystem {
   update(dt: number) {
       for (let timerEntity of timers.entities) {
         let timer = ent.getComponent(Timer)
         if (timer.timeLeft > 0) {
           timer.timeLeft -= dt
         } else {
             timer.timeLeft = timer.totalTime
             // DO SOMETHING
         }  
      } 
   }
 }

系统执行顺序

在某些情况下,当您有多个系统运行 update() 函数时,您可能会关心场景会首先执行哪个系统。

例如,有一个 physics 系统负责更新场景中实体的位置,另一个 boundaries 系统确保没有任何实体位于场景边界之外。 在这种情况下,您需要确保最后执行 boundaries 系统。 否则,physics 系统可能会将实体移出场景边界,但 boundaries 系统不会发现,直到它在下一帧中再次执行。

将系统添加到引擎时,可以设置可选的 priority 字段来确定系统相对于其他系统执行时间先后的优先级。

engine.addSystem(new PhysicsSystem(1))
engine.addSystem(new BoundariesSystem(5))

首先执行具有较低优先级编号的系统,因此优先级 5 之前执行优先级为 1 的系统。

未显式设置优先级的系统的默认优先级为 0,因此会首先执行。

如果两个系统具有相同的优先级编号,则无法确定将首先执行哪个系统。

删除系统

为了打开或关闭系统,可以在引擎中添加或移除系统。

如果系统未添加到引擎,则引擎不会调用其功能。

// add system
engine.addSystem(new mySystem())

// remove system
engine.removeSystem(mySystem)