How to Use the ECS FrameworkOn this pageHow to Use the ECS Framework 1. Framework Introduction The ECS framework in Dora SSR is inspired by Entitas, with slight functional adjustments. Its basic concepts can be understood using Entitas's schematic diagram. Entitas ECS+-----------------+| Context ||-----------------|| e e | +-----------+| e e--|----> | Entity || e e | |-----------|| e e e | | Component || e e | | | +-----------+| e e | | Component-|----> | Component || e e e | | | |-----------|| e e e | | Component | | Data |+-----------------+ +-----------+ +-----------+ | | | +-------------+ Groups: | | e | Entity groups are subsets of all game entities, | | e e | categorized by their components, for fast traversal +---> | +------------+ and querying of entities with specific components. | e | | | | e | e | e | +--------|----+ e | | e | | e e | +------------+ Unlike Entitas, in Dora SSR's ECS framework, a system component is managed as a single field on the entity object. This introduces some performance overhead but significantly simplifies writing logical code. 2. Code Example In this tutorial, we will demonstrate how to write game logic using Dora SSR’s ECS (Entity Component System) framework through a code example. LuaTealTypeScriptYueScriptBefore writing the actual code, let’s first import the functional modules required for this Lua tutorial.local Group <const> = require("Group")local Observer <const> = require("Observer")local Entity <const> = require("Entity")local Node <const> = require("Node")local Director <const> = require("Director")local Touch <const> = require("Touch")local Sprite <const> = require("Sprite")local Scale <const> = require("Scale")local Ease <const> = require("Ease")local Vec2 <const> = require("Vec2")local Roll <const> = require("Roll")First, we create two entity groups sceneGroup and positionGroup, used to access and manage all entities with the component names "scene" and "position."local sceneGroup = Group {"scene"}local positionGroup = Group {"position"}Next, we use an observer to listen for changes to entities. When developing a game using the ECS framework, you may need to trigger some actions when an entity adds a specific component. In such cases, you can use an observer to listen for entity addition events and perform the corresponding logic when they occur. Here's an example code snippet on how to use an observer to listen for entity addition events:Observer("Add", {"scene"}) :watch(function(_entity, scene) Director.entry:addChild(scene) scene:onTapEnded(function(touch) local location = touch.location positionGroup:each(function(entity) entity.target = location end) end) end)First, create an observer object using the Observer class and specify that it monitors the "Add" event, which listens for the addition of an entity. We also pass a list containing the string "scene" as a parameter, indicating that the observer should monitor entities containing the "scene" component.Observer("Add", {"scene"})Next, in the observer object's watch method, we define a callback function (_entity, scene)->. This function is triggered when an entity addition event occurs. The first parameter is the entity that triggered the event, and subsequent parameters correspond to the monitored component list.:watch(function(_entity, scene)Inside the callback function, we perform a series of actions. First, we add scene to the game scene through Director.entry.Director.entry:addChild(scene)Next, we add an "onTapEnded" event handler to the scene, which gets called when a touch end event occurs.scene:onTapEnded(function(touch)Inside the event handler, we first obtain the touch location and assign it to the location variable.local location = touch.locationFinally, we iterate over all entities in positionGroup and set each entity's target property to the touch point's location.positionGroup:each(function(entity) entity.target = locationend)Thus, whenever a new entity adds the "scene" component, this observer is triggered, executing the above actions, adding the scene node to the game, and completing a series of initialization steps.Next, we will create additional observers to handle other "Add" and "Remove" types of entity changes, specifying the monitored components as sprite.Observer("Add", {"image"}):watch(function(entity, image) sceneGroup:each(function(e) local sprite = Sprite(image) sprite:addTo(e.scene) sprite:runAction(Scale(0.5, 0, 0.5, Ease.OutBack)) return true end)end)Observer("Remove", {"sprite"}):watch(function(entity) local sprite = entity.oldValues.sprite sprite:removeFromParent()end)Then, we create an entity group with "position", "direction", "speed", and "target" components and define an observer to handle component changes within the group. On each game update frame, it iterates over a specific set of entities, updating the rotation angle and position properties based on their speed and time elapsed.Group({"position", "direction", "speed", "target"}):watch( function(entity, position, _direction, speed, target) if target == position then return end local dir = target - position dir = dir:normalize() local newPos = position + dir * speed newPos = newPos:clamp(position, target) entity.position = newPos if newPos == target then entity.target = nil end local angle = math.deg(math.atan(dir.x, dir.y)) entity.direction = angleend)In this code, first, we use the Group class to create an entity group object, specifying that the group contains entities with "position", "direction", "speed", and "target" components.Group({"position", "direction", "speed", "target"})Then, we use the entity group's watch method to iterate through all entities in the group each frame, executing the callback function to handle component logic.:watch( function(entity, position, _direction, speed, target)Inside the callback function, we perform a conditional check using return if target == position to see if the entity's target position matches its current position. If they are the same, the function returns, and no further updates are performed.if target == position then returnendNext, we calculate the entity's direction vector dir, which is equal to the target position minus the current position, and normalize it.local dir = target - positiondir = dir:normalize()Then, based on the entity's speed and direction vector, we calculate the entity’s new position newPos for the current frame. We multiply the direction vector dir by the speed speed, then add it to the current position position.local newPos = position + dir * speedNext, we adjust the position using newPos and the target position target. By clamping newPos between the current and target positions, we ensure the new position remains within this range. The final corrected position is assigned back to the entity's position component.newPos = newPos:clamp(position, target)entity.position = newPosNext, if the new position equals the target position, we clear the entity's target.if newPos == target then entity.target = nilendFinally, we calculate the entity's rotation angle angle using the math.atan function to find the angle of the direction vector `dirin radians and convert it to degrees. The entity's rotation angle componentdirection` is then updated with the calculated value.local angle = math.deg(math.atan(dir.x, dir.y))entity.direction = angleThus, on each game update, this code is triggered for each entity in the group, updating their current "position", "direction", "speed", or "target" components.After the data calculation and updates are completed, we need to update the rendered graphics based on these results.Observer("AddOrChange", {"position", "direction", "sprite"}) :watch(function(entity, position, direction, sprite) -- Update the display position of the sprite sprite.position = position local lastDirection = entity.oldValues.direction or sprite.angle -- If the sprite’s rotation angle changes, play a rotation animation if math.abs(direction - lastDirection) > 1 then sprite:runAction(Roll(0.3, lastDirection, direction)) end end)Finally, we create three entities and assign different components to them. The game system will now officially start running.Entity { scene = Node() }Entity { image = "Image/logo.png", position = Vec2.zero, direction = 45.0, speed = 4.0}Entity { image = "Image/logo.png", position = Vec2(-100, 200), direction = 90.0, speed = 10.0}Before writing the actual code, let’s first import the functional modules required for this Teal tutorial.local Group <const> = require("Group")local Observer <const> = require("Observer")local Entity <const> = require("Entity")local Node <const> = require("Node")local Director <const> = require("Director")local Touch <const> = require("Touch")local Sprite <const> = require("Sprite")local Scale <const> = require("Scale")local Ease <const> = require("Ease")local Vec2 <const> = require("Vec2")local Roll <const> = require("Roll")First, we create two entity groups sceneGroup and positionGroup, used to access and manage all entities with the component names "scene" and "position."local sceneGroup = Group {"scene"}local positionGroup = Group {"position"}Next, we use an observer to listen for changes to entities. When developing a game using the ECS framework, you may need to trigger some actions when an entity adds a specific component. In such cases, you can use an observer to listen for entity addition events and perform the corresponding logic when they occur. Here's an example code snippet on how to use an observer to listen for entity addition events:Observer("Add", {"scene"}) :watch(function(_entity: Entity.Type, scene: Node.Type) Director.entry:addChild(scene) scene:onTapEnded(function(touch: Touch.Type) local location = touch.location positionGroup:each(function(entity: Entity.Type): boolean entity.target = location return false end) end) end)First, create an observer object using the Observer class and specify that it monitors the "Add" event, which listens for the addition of an entity. We also pass a list containing the string "scene" as a parameter, indicating that the observer should monitor entities containing the "scene" component.Observer("Add", {"scene"})Next, in the observer object's watch method, we define a callback function (_entity, scene)->. This function is triggered when an entity addition event occurs. The first parameter is the entity that triggered the event, and subsequent parameters correspond to the monitored component list.:watch(function(_entity: Entity.Type, scene: Node.Type)Inside the callback function, we perform a series of actions. First, we add scene to the game scene through Director.entry.Director.entry:addChild(scene)Next, we add an "onTapEnded" event handler to the scene, which gets called when a touch end event occurs.scene:onTapEnded(function(touch: Touch.Type)Inside the event handler, we first obtain the touch location and assign it to the location variable.local location = touch.locationFinally, we iterate over all entities in positionGroup and set each entity's target property to the touch point's location.positionGroup:each(function(entity: Entity.Type): boolean entity.target = location return falseend)Thus, whenever a new entity adds the "scene" component, this observer is triggered, executing the above actions, adding the scene node to the game, and completing a series of initialization steps.Next, we will create additional observers to handle other "Add" and "Remove" types of entity changes, specifying the monitored components as image and sprite.Observer("Add", {"image"}):watch(function(entity: Entity.Type, image: string) sceneGroup:each(function(e: Entity.Type): boolean local scene = e.scene as Node.Type local sprite = Sprite(image) if sprite then sprite:runAction(Scale(0.5, 0, 0.5, Ease.OutBack)) sprite:addTo(scene) entity.sprite = sprite end return true end)end)Observer("Remove", {"sprite"}):watch(function(self: Entity.Type) local sprite = self.oldValues.sprite as Node.Type sprite:removeFromParent()end)Then, we create an entity group with "position", "direction", "speed", and "target" components and define an observer to handle component changes within the group. On each game update frame, it iterates over a specific set of entities, updating the rotation angle and position properties based on their speed and time elapsed.Group({"position", "direction", "speed", "target"}):watch( function(entity: Entity.Type, position: Vec2.Type, _direction: number, speed: number, target: Vec2.Type) if target == position then return end local dir = target - position dir = dir:normalize() local newPos = position + dir * speed newPos = newPos:clamp(position, target) entity.position = newPos if newPos == target then entity.target = nil end local angle = math.deg(math.atan(dir.x, dir.y)) entity.direction = angleend)In this code, first, we use the Group class to create an entity group object, specifying that the group contains entities with "position", "direction", "speed", and "target" components.Group({"position", "direction", "speed", "target"})Then, we use the entity group's watch method to iterate through all entities in the group each frame, executing the callback function to handle component logic.:watch( function(entity: Entity.Type, position: Vec2.Type, _direction: number, speed: number, target: Vec2.Type)Inside the callback function, we perform a conditional check using return if target == position to see if the entity's target position matches its current position. If they are the same, the function returns, and no further updates are performed.if target == position then returnendNext, we calculate the entity's direction vector dir, which is equal to the target position minus the current position, and normalize it.local dir = target - positiondir = dir:normalize()Then, based on the entity's speed and direction vector, we calculate the entity’s new position newPos for the current frame. We multiply the direction vector dir by the speed speed, then add it to the current position position.local newPos = position + dir * speedNext, we adjust the position using newPos and the target position target. By clamping newPos between the current and target positions, we ensure the new position remains within this range. The final corrected position is assigned back to the entity's position component.newPos = newPos:clamp(position, target)entity.position = newPosNext, if the new position equals the target position, we clear the entity's target.if newPos == target then entity.target = nilendFinally, we calculate the entity's rotation angle angle using the math.atan function to find the angle of the direction vector dir in radians and convert it to degrees. The entity's rotation angle component `direction` is then updated with the calculated value.local angle = math.deg(math.atan(dir.x, dir.y))entity.direction = angleThus, on each game update, this code is triggered for each entity in the group, updating their current "position", "direction", "speed", or "target" components.After the data calculation and updates are completed, we need to update the rendered graphics based on these results.Observer("AddOrChange", {"position", "direction", "sprite"}) :watch(function(entity: Entity.Type, position: Vec2.Type, direction: number, sprite: Sprite.Type) -- Update the display position of the sprite sprite.position = position local lastDirection = entity.oldValues.direction as number or sprite.angle -- If the sprite’s rotation angle changes, play a rotation animation if math.abs(direction - lastDirection) > 1 then sprite:runAction(Roll(0.3, lastDirection, direction)) end end)Finally, we create three entities and assign different components to them. The game system will now officially start running.Entity { scene = Node() }Entity { image = "Image/logo.png", position = Vec2.zero, direction = 45.0, speed = 4.0}Entity { image = "Image/logo.png", position = Vec2(-100, 200), direction = 90.0, speed = 10.0}Before writing the actual code, let’s first import the functional modules required for this TypeScript tutorial.import { Group, Observer, Entity, Node, Director, Touch, Sprite, Scale, Ease, Vec2, Roll, EntityEvent} from "Dora";First, we create two entity groups sceneGroup and positionGroup, used to access and manage all entities with the component names "scene" and "position."const sceneGroup = Group(["scene"]);const positionGroup = Group(["position"]);Next, we use an observer to listen for changes to entities. When developing a game using the ECS framework, you may need to trigger some actions when an entity adds a specific component. In such cases, you can use an observer to listen for entity addition events and perform the corresponding logic when they occur. Here's an example code snippet on how to use an observer to listen for entity addition events:Observer(EntityEvent.Add, ["scene"]) .watch((_entity, scene: Node.Type) => { Director.entry.addChild(scene); scene.onTapEnded(touch => { const {location} = touch; positionGroup.each(entity => { entity.target = location; return false; }) }); return false; });First, create an observer object using the Observer class and specify that it monitors the "Add" event, which listens for the addition of an entity. We also pass a list containing the string "scene" as a parameter, indicating that the observer should monitor entities containing the "scene" component.Observer(EntityEvent.Add, ["scene"])Next, in the observer object's watch method, we define a callback function (_entity, scene) =>. This function is triggered when an entity addition event occurs. The first parameter is the entity that triggered the event, and subsequent parameters correspond to the monitored component list..watch((_entity, scene: Node.Type) => {Inside the callback function, we perform a series of actions. First, we add scene to the game scene through Director.entry.Director.entry.addChild(scene);Next, we add an "onTapEnded" event handler to the scene, which gets called when a touch end event occurs.scene.onTapEnded(touch => {Inside the event handler, we first obtain the touch location and assign it to the location variable.const {location} = touch;Finally, we iterate over all entities in positionGroup and set each entity's target property to the touch point's location.positionGroup.each(entity => { entity.target = location; return false;});Thus, whenever a new entity adds the "scene" component, this observer is triggered, executing the above actions, adding the scene node to the game, and completing a series of initialization steps.Next, we will create additional observers to handle other "Add" and "Remove" types of entity changes, specifying the monitored components as image and sprite.Observer(EntityEvent.Add, ["image"]).watch((entity, image: string) => { sceneGroup.each(e => { const scene = e.scene as Node.Type; const sprite = Sprite(image); if (sprite) { sprite.runAction(Scale(0.5, 0, 0.5, Ease.OutBack)); sprite.addTo(scene); entity.sprite = sprite; } return true; }); return false;});Observer(EntityEvent.Remove, ["sprite"]).watch(self => { const sprite = self.oldValues.sprite as Node.Type; sprite.removeFromParent();});Then, we create an entity group with "position", "direction", "speed", and "target" components and define an observer to handle component changes within the group. On each game update frame, it iterates over a specific set of entities, updating the rotation angle and position properties based on their speed and time elapsed.Group(["position", "direction", "speed", "target"]).watch( (entity, position: Vec2.Type, _direction: number, speed: number, target: Vec2.Type) => { if (target.equals(position)) { return; } const dir = target.sub(position).normalize(); const newPos = position.add(dir.mul(speed)); entity.position = newPos.clamp(position, target); if (newPos.equals(target)) { entity.target = nil; } const angle = math.deg(math.atan(dir.x, dir.y)); entity.direction = angle;});In this code, first, we use the Group class to create an entity group object, specifying that the group contains entities with "position", "direction", "speed", and "target" components.Group(["position", "direction", "speed", "target"])Then, we use the entity group's watch method to iterate through all entities in the group each frame, executing the callback function to handle component logic..watch( (entity, position: Vec2.Type, _direction: number, speed: number, target: Vec2.Type) => {Inside the callback function, we perform a conditional check using if (target.equals(position)) to see if the entity's target position matches its current position. If they are the same, the function returns, and no further updates are performed.if (target.equals(position)) { return;}Next, we calculate the entity's direction vector dir, which is equal to the target position minus the current position, and normalize it.const dir = target.sub(position).normalize();Then, based on the entity's speed and direction vector, we calculate the entity’s new position newPos for the current frame. We multiply the direction vector dir by the speed speed, then add it to the current position position.const newPos = position.add(dir.mul(speed));Next, we adjust the position using newPos and the target position target. By clamping newPos between the current and target positions, we ensure the new position remains within this range. The final corrected position is assigned back to the entity's position component.entity.position = newPos.clamp(position, target);Next, if the new position equals the target position, we clear the entity's target.if (newPos.equals(target)) { entity.target = nil;}Finally, we calculate the entity's rotation angle angle using the math.atan function to find the angle of the direction vector dir in radians and convert it to degrees. The entity's rotation angle component direction is then updated with the calculated value.const angle = math.deg(math.atan(dir.x, dir.y));entity.direction = angle;Thus, on each game update, this code is triggered for each entity in the group, updating their current "position", "direction", "speed", or "target" components.After the data calculation and updates are completed, we need to update the rendered graphics based on these results.Observer(EntityEvent.AddOrChange, ["position", "direction", "sprite"]) .watch((entity, position: Vec2.Type, direction: number, sprite: Sprite.Type) => { // Update the display position of the sprite sprite.position = position; const lastDirection = entity.oldValues.direction as number ?? sprite.angle; // If the sprite’s rotation angle changes, play a rotation animation if (math.abs(direction - lastDirection) > 1) { sprite.runAction(Roll(0.3, lastDirection, direction)); } });Finally, we create three entities and assign different components to them. The game system will now officially start running.Entity({ scene: Node() });Entity({ image: "Image/logo.png", position: Vec2.zero, direction: 45.0, speed: 4.0});Entity({ image: "Image/logo.png", position: Vec2(-100, 200), direction: 90.0, speed: 10.0});Before writing the actual code, let’s first import the functional modules required for this YueScript tutorial._ENV = DoraFirst, we create two entity groups sceneGroup and positionGroup, used to access and manage all entities with the component names "scene" and "position."sceneGroup = Group ["scene",]positionGroup = Group ["position",]Next, we use an observer to listen for changes to entities. When developing a game using the ECS framework, you may need to trigger some actions when an entity adds a specific component. In such cases, you can use an observer to listen for entity addition events and perform the corresponding logic when they occur. Here's an example code snippet on how to use an observer to listen for entity addition events:with Observer "Add", ["scene",] \watch (_entity, scene): false -> Director.entry\addChild with scene \onTapEnded (touch) -> :location = touch positionGroup\each (entity) -> entity.target = locationFirst, create an observer object using the Observer class and specify that it monitors the "Add" event, which listens for the addition of an entity. We also pass a list containing the string "scene" as a parameter, indicating that the observer should monitor entities containing the "scene" component.with Observer "Add", ["scene",]Next, in the observer object's watch method, we define a callback function (_entity, scene)->. This function is triggered when an entity addition event occurs. The first parameter is the entity that triggered the event, and subsequent parameters correspond to the monitored component list.\watch (_entity, scene) ->Inside the callback function, we perform a series of actions. First, we add scene to the game scene through Director.entry.Director.entry\addChild with sceneNext, we add an "onTapEnded" event handler to the scene, which gets called when a touch end event occurs.\onTapEnded (touch) ->Inside the event handler, we assign the touch point's location to the location variable.:location = touchFinally, we iterate over all entities in positionGroup and set each entity's target property to the touch point's location.positionGroup\each (entity) -> entity.target = locationThus, whenever a new entity adds the "scene" component, this observer is triggered, executing the above actions, adding the scene node to the game, and completing a series of initialization steps.Next, we will create additional observers to handle other "Add" and "Remove" types of entity changes, specifying the monitored components as image and sprite.with Observer "Add", ["image",] \watch (image): false => sceneGroup\each (e) -> with @sprite = Sprite image \addTo e.scene \runAction Scale 0.5, 0, 0.5, Ease.OutBack truewith Observer "Remove", ["sprite",] \watch (): false => @oldValues.sprite\removeFromParent!Then, we create an entity group with "position", "direction", "speed", and "target" components and define an observer to handle component changes within the group. On each game update frame, it iterates over a specific set of entities, updating the rotation angle and position properties based on their speed and time elapsed.with Group ["position", "direction", "speed", "target"] \watch (entity, position, direction, speed, target): false -> return if target == position dir = target - position dir = dir\normalize! newPos = position + dir * speed newPos = newPos\clamp position, target entity.position = newPos entity.target = nil if newPos == target angle = math.deg math.atan dir.x, dir.y entity.direction = angleIn this code, first, we use the Group class to create an entity group object, specifying that the group contains entities with "position", "direction", "speed", and "target" components.with Group ["position", "direction", "speed", "target"]Then, we use the entity group's watch method to iterate through all entities in the group each frame, executing the callback function to handle component logic.\watch (entity, position, direction, speed, target): false ->Inside the callback function, we perform a conditional check using return if target == position to see if the entity's target position matches its current position. If they are the same, the function returns, and no further updates are performed.return if target == positionNext, we calculate the entity's direction vector dir, which is equal to the target position minus the current position, and normalize it.dir = target - positiondir = dir\normalize!Then, based on the entity's speed and direction vector, we calculate the entity’s new position newPos for the current frame. We multiply the direction vector dir by the speed speed, then add it to the current position position.newPos = position + dir * speedNext, we adjust the position using newPos and the target position target. By clamping newPos between the current and target positions, we ensure the new position remains within this range. The final corrected position is assigned back to the entity's position component.newPos = newPos\clamp position, targetentity.position = newPosNext, if the new position equals the target position, we clear the entity's target.entity.target = nil if newPos == targetFinally, we calculate the entity's rotation angle angle using the math.atan function to find the angle of the direction vector dir in radians and convert it to degrees. The entity's rotation angle component direction is then updated with the calculated value.angle = math.deg math.atan dir.x, dir.yentity.direction = angleThus, on each game update, this code is triggered for each entity in the group, updating their current "position", "direction", "speed", or "target" components.After the data calculation and updates are completed, we need to update the rendered graphics based on these results.with Observer "AddOrChange", ["position", "direction", "sprite"] \watch (position, direction, sprite): false => -- Update the display position of the sprite sprite.position = position lastDirection = @oldValues.direction or sprite.angle -- If the sprite’s rotation angle changes, play a rotation animation if math.abs(direction - lastDirection) > 1 sprite\runAction Roll 0.3, lastDirection, directionFinally, we create three entities and assign different components to them. The game system will now officially start running.Entity scene: Node!Entity image: "Image/logo.png" position: Vec2.zero direction: 45.0 speed: 4.0Entity image: "Image/logo.png" position: Vec2 -100, 200 direction: 90.0 speed: 10.0 This code example demonstrates the basic workflow for developing a game using the ECS framework. Depending on your game’s needs, you can use the framework’s provided entity, group, and observer interfaces to build game logic. In the code, you can trigger appropriate actions in response to entity component changes, add, remove, or modify entities, and manage them in groups using entity groups. By using observers, you can monitor entity change events, such as adding, modifying, or deleting specific components. In the observer’s handler function, you can perform corresponding logic operations based on entity changes, such as updating scene nodes, handling user input, or printing debug information. By properly organizing the relationship between entities and components, and leveraging the monitoring and processing capabilities of observers, you can build complex game logic and behaviors. In practice, you can design and define your own component types based on the game’s requirements and implement various functions and behaviors using the ECS framework’s interfaces. 3. Summary In summary, the basic workflow for developing a game using the ECS framework includes: Defining the entity’s component types and creating entity objects as needed. Creating entity groups and adding entities to their corresponding groups for group management. Using observers to monitor entity change events, such as adding, modifying, or deleting specific components. Performing corresponding logic operations based on entity changes in the entity group observer’s handler function, updated on each frame. Designing and implementing other components, systems, and functions based on game requirements. By following this workflow, using Dora SSR's ECS framework allows you to better organize and manage your game logic, improving the maintainability and scalability of your code, and enabling the implementation of complex game functions and behaviors.