Skip to content

Commit

Permalink
Lots of upgrades and changes. Included exercises as comments
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielBelmes committed Nov 17, 2023
1 parent 54ae399 commit 20275e1
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 66 deletions.
46 changes: 11 additions & 35 deletions src/components/BubbleComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,72 +8,48 @@ import { NameComponent } from "@etherealengine/engine/src/scene/components/NameC
import { EntityUUID } from "@etherealengine/common/src/interfaces/EntityUUID"
import { EntityTreeComponent } from "@etherealengine/engine/src/ecs/functions/EntityTree"
import { VisibleComponent } from "@etherealengine/engine/src/scene/components/VisibleComponent"
import { LocalTransformComponent } from "@etherealengine/engine/src/transform/components/TransformComponent"
import matches from "ts-matches"

export const BubbleComponent = defineComponent({
//name: The human-readable label for the component. This will be displayed in the editor and debugging tools.
name: "Bubble Component",
//jsonID: The serialized name of the component. This is used to identify the component in the serialized scene data
jsonID: "bubble",

//onInit: Initializer function that is called when the component is added to an entity. The return type of this function defines the
// schema of the component's runtime data.
onInit: (entity) => {
return {
color: new Color(0xFFFFFF),
direction: new Vector3(0, 1, 0),
speed: .1,
bubble: null as Mesh | null,
bubbleEntity: null as Entity | null
age: 0 as number
}
},
//onSet: Set function that is called whenever the component's data is updated via the setComponent function. This is where deserialize logic should
// be applied.
onSet: (entity, component, json) => {
if (!json) return
if (json.color?.isColor) {
component.color.set(json.color)
}
matches.number.test(json.age) && component.age.set(json.age)
},
//toJSON: Serializer function that is called when the component is being saved to a scene file or snapshot. This is where serialize logic should
// be applied to convert the component's runtime data into a JSON object.
toJSON: (entity, component) => {
return {
color: component.color.value,
direction: component.direction.value,
speed: component.speed.value
age: component.age.value
}
},
//reactor: The reactor function is where async reactive logic is defined. Any side-effects that depend upon component data should be defined here.
reactor: () => {
//get the entity using useEntityContext
const entity = useEntityContext()

//get a reactive component state with useComponent
const bubbleComponent = useComponent(entity, BubbleComponent)

//a useEffect with no dependencies will only run once, when the component is first initialized
useEffect(() => {
const bubbleEntity = createEntity()
bubbleComponent.bubbleEntity.set(bubbleEntity)
setComponent(bubbleEntity, VisibleComponent)
setComponent(bubbleEntity, NameComponent, "Bubble")
setComponent(bubbleEntity, EntityTreeComponent, {
parentEntity: entity,
uuid: MathUtils.generateUUID() as EntityUUID
})
setComponent(entity, VisibleComponent) // Set if the entity is visible
setComponent(entity, NameComponent, "Bubble") // Give the entity a name
setComponent(entity, LocalTransformComponent) // Give the entity a local transform
const bubbleMesh = new Mesh(new SphereGeometry(), new MeshStandardMaterial())
bubbleMesh.material.color = bubbleComponent.color.value
addObjectToGroup(bubbleComponent.bubbleEntity.value!, bubbleMesh)
bubbleComponent.bubble.set(bubbleMesh)
bubbleMesh.material.color = new Color(0xFFFFFF)
addObjectToGroup(entity, bubbleMesh) // Add GroupComponent and add mesh to Group
}, [])

//a useEffect with dependencies will run whenever the dependencies change
useEffect(() => {
const bubbleMesh = bubbleComponent.bubble.value
if (!bubbleMesh) return
const material = bubbleMesh.material as MeshStandardMaterial
material.color.copy(bubbleComponent.color.value)
}, [bubbleComponent.color])

return null
}
})
140 changes: 140 additions & 0 deletions src/components/BubbleEmitterComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { defineComponent, getComponent, getMutableComponent, setComponent, useComponent } from "@etherealengine/engine/src/ecs/functions/ComponentFunctions"
import { createEntity, removeEntity, useEntityContext } from "@etherealengine/engine/src/ecs/functions/EntityFunctions"
import { BufferGeometry, Color, MathUtils, Mesh, MeshStandardMaterial, SphereGeometry, Vector3 } from "three"
import { useEffect } from "react"
import { GroupComponent, addObjectToGroup } from "@etherealengine/engine/src/scene/components/GroupComponent"
import { Entity } from "@etherealengine/engine/src/ecs/classes/Entity"
import { NameComponent } from "@etherealengine/engine/src/scene/components/NameComponent"
import { EntityUUID } from "@etherealengine/common/src/interfaces/EntityUUID"
import { EntityTreeComponent } from "@etherealengine/engine/src/ecs/functions/EntityTree"
import { VisibleComponent } from "@etherealengine/engine/src/scene/components/VisibleComponent"
import { BubbleComponent } from "./BubbleComponent"
import { NO_PROXY, getState } from "@etherealengine/hyperflux"
import { useExecute } from "@etherealengine/engine/src/ecs/functions/SystemFunctions"
import { SimulationSystemGroup } from "@etherealengine/engine/src/ecs/functions/EngineFunctions"
import { EngineState } from "@etherealengine/engine/src/ecs/classes/EngineState"

let collectedtime = 0 //Assign out of system so scope persists

export const BubbleEmitterComponent = defineComponent({
//name: The human-readable label for the component. This will be displayed in the editor and debugging tools.
name: "Bubble Emitter Component",
//jsonID: The serialized name of the component. This is used to identify the component in the serialized scene data
jsonID: "bubbleEmitter",
//onInit: Initializer function that is called when the component is added to an entity. The return type of this function defines the
// schema of the component's runtime data.
onInit: (entity) => {
return {
color: new Color(0xFFFFFF),
direction: new Vector3(0, 1, 0),
speed: .1,
bubbleEntities: [] as Entity[] | null
}
},
//onSet: Set function that is called whenever the component's data is updated via the setComponent function. This is where deserialize logic should
// be applied.
onSet: (entity, component, json) => {
if (!json) return
if (json.color?.isColor) {
component.color.set(json.color)
}
},
//toJSON: Serializer function that is called when the component is being saved to a scene file or snapshot. This is where serialize logic should
// be applied to convert the component's runtime data into a JSON object.
toJSON: (entity, component) => {
return {
color: component.color.value,
direction: component.direction.value,
speed: component.speed.value
}
},

//reactor: The reactor function is where async reactive logic is defined. Any side-effects that depend upon component data should be defined here.
//reactive:
reactor: () => {
//get the entity using useEntityContext
const entity = useEntityContext()

//get a reactive component state with useComponent
const emitterComponent = useComponent(entity, BubbleEmitterComponent)

//a useEffect with no dependencies will only run once, when the component is first initialized
// it's return statement will run when the component is unmounted
useEffect(() => {
return () => {
for(let ent of emitterComponent.bubbleEntities.value!) {
removeEntity(ent)
}
}
}, [])

// a useEffect with dependencies will run whenever the dependencies change
// Whenever the color is changed this will rerun and update all child bubble materials
useEffect(() => {
for(let ent of emitterComponent.bubbleEntities.value!) {
const groupComponent = getComponent(ent, GroupComponent)
const obj = groupComponent[0]
obj.traverse((obj: Mesh<BufferGeometry, MeshStandardMaterial>) => {
if(obj.isMesh) {
const material = obj.material as MeshStandardMaterial
material.color.copy(emitterComponent.color.value)
}
})
}
}, [emitterComponent.color])

// useExecute is a way we can define a System within a reactive context
useExecute(() => {
const { elapsedSeconds, deltaSeconds } = getState(EngineState)
ageEmitterBubbles(entity, deltaSeconds)
// Spawning a single bubble as an example
// Exercise: Using this system. Spawn multiple bubbles with varying x,z Localtransform positons
// Remove them if they are too old(bubble.age > 10 seconds)
if(emitterComponent.bubbleEntities.value!.length < 1) { //For example ensuring there is only one bubble being added
const bubbleEntity = createEntity()
setComponent(bubbleEntity, BubbleComponent)
setComponent(bubbleEntity, EntityTreeComponent, {
parentEntity: entity,
uuid: MathUtils.generateUUID() as EntityUUID
})
const currEntities = emitterComponent.bubbleEntities.get(NO_PROXY)
emitterComponent.bubbleEntities.set([...currEntities!,bubbleEntity])
}

if(collectedtime >= 5) { // Delete one bubble and collectTime(each bubble is collecting an age independantly for your convience)
collectedtime = 0
removeBubble(entity,emitterComponent.bubbleEntities![0])
} else {
collectedtime += deltaSeconds // Collect elapsed seconds since System has been ran
}
}, { after: SimulationSystemGroup })

return null
},
})

/**
* Remove bubble entity from emitter
*/
export function removeBubble(emitterEntity: Entity, bubbleEntity: Entity): void {
const emitter = getMutableComponent(emitterEntity, BubbleEmitterComponent)
const currEntities = emitter.bubbleEntities.get(NO_PROXY)!
const index = currEntities.indexOf(bubbleEntity);
if (index > -1) { // only splice array when item is found
currEntities.splice(index, 1)
emitter.bubbleEntities.set(currEntities)// 2nd parameter means remove one item only
removeEntity(bubbleEntity)
}
}

/**
* increment age of bubbles in an emitter
*/
export function ageEmitterBubbles(emitterEntity: Entity, deltaSeconds: number): void {
const emitter = getComponent(emitterEntity, BubbleEmitterComponent)
for(const bubbleEntity of emitter.bubbleEntities!) {
const bubble = getMutableComponent(bubbleEntity, BubbleComponent)
const currAge = bubble.age.get(NO_PROXY)
bubble.age.set(currAge+deltaSeconds)
}
}
22 changes: 11 additions & 11 deletions src/editors/BubbleComponentNodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'

import { EditorComponentType, commitProperty, updateProperty } from '@etherealengine/editor/src/components/properties/Util'
import { getComponent, useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions'
import { BubbleComponent } from '../components/BubbleComponent'
import { BubbleEmitterComponent } from '../components/BubbleEmitterComponent'
import NodeEditor from '@etherealengine/editor/src/components/properties/NodeEditor'
import InputGroup from '@etherealengine/editor/src/components/inputs/InputGroup'
import { ColorInput } from '@etherealengine/editor/src/components/inputs/ColorInput'
Expand All @@ -12,27 +12,27 @@ import Vector3Input from '@etherealengine/editor/src/components/inputs/Vector3In


export const BubbleNodeEditor: EditorComponentType = (props) => {
const bubbleComponent = useComponent(props.entity, BubbleComponent)
const emitterComponent = useComponent(props.entity, BubbleEmitterComponent)
return <NodeEditor description={'Description'} {...props}>
<InputGroup name="Color" label="Bubble Color">
<ColorInput
value={bubbleComponent.color.value}
onChange={updateProperty(BubbleComponent, 'color')}
onRelease={commitProperty(BubbleComponent, 'color')}
value={emitterComponent.color.value}
onChange={updateProperty(BubbleEmitterComponent, 'color')}
onRelease={commitProperty(BubbleEmitterComponent, 'color')}
/>
</InputGroup>
<InputGroup name="Speed" label="Bubble Speed">
<NumericInput
value={bubbleComponent.speed.value}
onChange={updateProperty(BubbleComponent, 'speed')}
onRelease={commitProperty(BubbleComponent, 'speed')}
value={emitterComponent.speed.value}
onChange={updateProperty(BubbleEmitterComponent, 'speed')}
onRelease={commitProperty(BubbleEmitterComponent, 'speed')}
/>
</InputGroup>
<InputGroup name="Direction" label="Bubble Direction">
<Vector3Input
value={bubbleComponent.direction.value}
onChange={updateProperty(BubbleComponent, 'direction')}
onRelease={commitProperty(BubbleComponent, 'direction')}
value={emitterComponent.direction.value}
onChange={updateProperty(BubbleEmitterComponent, 'direction')}
onRelease={commitProperty(BubbleEmitterComponent, 'direction')}
/>
</InputGroup>
</NodeEditor>
Expand Down
26 changes: 9 additions & 17 deletions src/systems/BubbleSystem.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import { defineQuery, getComponent, getMutableComponent } from "@etherealengine/engine/src/ecs/functions/ComponentFunctions";
import { defineSystem } from "@etherealengine/engine/src/ecs/functions/SystemFunctions";
import { BubbleComponent } from "../components/BubbleComponent";
import { BubbleEmitterComponent, removeBubble } from "../components/BubbleEmitterComponent";
import { LocalTransformComponent } from "@etherealengine/engine/src/transform/components/TransformComponent";
import { NO_PROXY, getState } from "@etherealengine/hyperflux";
import { EngineState } from "@etherealengine/engine/src/ecs/classes/EngineState";
import { Vector3 } from "three";

const bubbleQuery = defineQuery([BubbleComponent])

let collectedtime = 0 //Assign out of system so scope persists

const bubbleEmitterQuery = defineQuery([BubbleEmitterComponent])

export const BubbleSystem = defineSystem({
uuid: "BubbleSystem",
execute: () => {
const { elapsedSeconds, deltaSeconds } = getState(EngineState)
for (const entity of bubbleQuery()) {
for (const entity of bubbleEmitterQuery()) {
// [Exercise 2]: Using the below basic setup. Move every bubble not just the first one
const tempvector = new Vector3(0,0,0)
const bubbleComponent = getComponent(entity, BubbleComponent)
const localTransform = getMutableComponent(bubbleComponent.bubbleEntity!, LocalTransformComponent)
tempvector.addVectors(localTransform.position.value, bubbleComponent.direction.clone().multiplyScalar(bubbleComponent.speed))
const emitterComponent = getComponent(entity, BubbleEmitterComponent)
const localTransform = getMutableComponent(emitterComponent.bubbleEntities![0], LocalTransformComponent)
tempvector.addVectors(localTransform.position.value, emitterComponent.direction.clone().multiplyScalar(emitterComponent.speed))
localTransform.position.get(NO_PROXY).copy(tempvector)

if(collectedtime >= 5) { //Reset Position and collectedTime
tempvector.set(0,0,0)
localTransform.position.get(NO_PROXY).copy(tempvector)
collectedtime = 0
} else {
collectedtime += deltaSeconds //CollectElapsed seconds since System has been ran
}
// [Exercise 3]: Utilizing an AvatarComponent Query, TransformComponent positions of bubble entities, and Vector3.distanceTo
// Detect if the player is near a bubble and remove it
}
}
})
6 changes: 3 additions & 3 deletions src/worldInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ComponentShelfCategories } from '@etherealengine/editor/src/components/
import { EntityNodeEditor } from '@etherealengine/editor/src/functions/ComponentEditors'
import { getState } from "@etherealengine/hyperflux";
import { EngineState } from "@etherealengine/engine/src/ecs/classes/EngineState";
import { BubbleComponent } from "./components/BubbleComponent";
import { BubbleEmitterComponent } from "./components/BubbleEmitterComponent";
import { BubbleNodeEditor } from "./editors/BubbleComponentNodeEditor";
import { startSystem } from "@etherealengine/engine/src/ecs/functions/SystemFunctions";
import { BubbleSystem } from "./systems/BubbleSystem";
Expand All @@ -13,8 +13,8 @@ export default async function worldInjection() {
if (isClient) {
if (getState(EngineState).isEditing)
{
EntityNodeEditor.set(BubbleComponent, BubbleNodeEditor)
ComponentShelfCategories.Misc.push(BubbleComponent)
EntityNodeEditor.set(BubbleEmitterComponent, BubbleNodeEditor)
ComponentShelfCategories.Misc.push(BubbleEmitterComponent)
}
startSystem(BubbleSystem, { after: SimulationSystemGroup })
}
Expand Down

0 comments on commit 20275e1

Please sign in to comment.