Skip to content

Commit

Permalink
Refactor: Basic ECS architecture for the entity system
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Jan 6, 2024
1 parent 2fa63c4 commit faa8429
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 111 deletions.
12 changes: 7 additions & 5 deletions game/src/main/scala/hexacraft/game/GameScene.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,20 @@ class GameScene(
private val otherPlayer: Entity =
Entity(
null,
new EntityBaseData(CylCoords(player.position)),
Some(PlayerEntityModel.create("player")),
None,
EntityFactory.playerBounds
Seq(
TransformComponent(CylCoords(player.position)),
VelocityComponent(),
BoundsComponent(EntityFactory.playerBounds),
ModelComponent(PlayerEntityModel.create("player"))
)
)

private val worldRenderer: WorldRenderer =
new WorldRenderer(world, world.requestRenderUpdate, blockSpecRegistry, initialWindowSize.physicalSize)
world.trackEvents(worldRenderer.onWorldEvent _)

// worldRenderer.addPlayer(otherPlayer)
otherPlayer.data.position = otherPlayer.position.offset(-2, -2, -1)
otherPlayer.transform.position = otherPlayer.transform.position.offset(-2, -2, -1)

val camera: Camera = new Camera(makeCameraProjection(initialWindowSize))

Expand Down
12 changes: 6 additions & 6 deletions game/src/main/scala/hexacraft/world/World.scala
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class World(worldProvider: WorldProvider, worldInfo: WorldInfo) extends BlockRep
do ch.removeEntity(e)

private def chunkOfEntity(entity: Entity): Option[Chunk] =
getChunk(CoordUtils.approximateChunkCoords(entity.position))
getChunk(CoordUtils.approximateChunkCoords(entity.transform.position))

def getHeight(x: Int, z: Int): Int =
val coords = ColumnRelWorld(x >> 4, z >> 4)
Expand Down Expand Up @@ -181,13 +181,13 @@ class World(worldProvider: WorldProvider, worldInfo: WorldInfo) extends BlockRep

private def tickEntity(e: Entity): Unit =
e.ai.foreach: ai =>
ai.tick(this, e.data, e.boundingBox)
e.data.velocity.add(ai.acceleration())
ai.tick(this, e.transform, e.velocity, e.boundingBox)
e.velocity.velocity.add(ai.acceleration())

e.data.velocity.x *= 0.9
e.data.velocity.z *= 0.9
e.velocity.velocity.x *= 0.9
e.velocity.velocity.z *= 0.9

entityPhysicsSystem.update(e.data, e.boundingBox)
entityPhysicsSystem.update(e.transform, e.velocity, e.boundingBox)

e.model.foreach(_.tick())

Expand Down
30 changes: 20 additions & 10 deletions game/src/main/scala/hexacraft/world/entity/ai.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import com.martomate.nbt.Nbt
import org.joml.{Vector3d, Vector3dc}

trait EntityAI {
def tick(world: BlocksInWorld, entityBaseData: EntityBaseData, entityBoundingBox: HexBox): Unit
def tick(
world: BlocksInWorld,
transform: TransformComponent,
velocity: VelocityComponent,
entityBoundingBox: HexBox
): Unit
def acceleration(): Vector3dc
def toNBT: Nbt.MapTag
}
Expand All @@ -33,16 +38,21 @@ class SimpleWalkAI(using CylinderSize) extends EntityAI {

private val input: SimpleAIInput = new SimpleAIInput

def tick(world: BlocksInWorld, entityBaseData: EntityBaseData, entityBoundingBox: HexBox): Unit = {
val distSq = entityBaseData.position.distanceXZSq(target)
def tick(
world: BlocksInWorld,
transform: TransformComponent,
velocity: VelocityComponent,
entityBoundingBox: HexBox
): Unit = {
val distSq = transform.position.distanceXZSq(target)

movingForce.set(0)

if (distSq < speed * speed || timeout == 0) {
// new goal
val angle = math.random() * 2 * math.Pi
val targetX = entityBaseData.position.x + reach * math.cos(angle)
val targetZ = entityBaseData.position.z + reach * -math.sin(angle)
val targetX = transform.position.x + reach * math.cos(angle)
val targetZ = transform.position.z + reach * -math.sin(angle)
target = CylCoords(targetX, 0, targetZ)

timeout = timeLimit
Expand All @@ -51,19 +61,19 @@ class SimpleWalkAI(using CylinderSize) extends EntityAI {
val blockInFront =
input.blockInFront(
world,
entityBaseData.position,
entityBaseData.rotation,
transform.position,
transform.rotation,
entityBoundingBox.radius + speed * 4
)

if (blockInFront != Block.Air && entityBaseData.velocity.y == 0) {
if (blockInFront != Block.Air && velocity.velocity.y == 0) {
movingForce.y = 3.5
}
val angle = entityBaseData.position.angleXZ(target)
val angle = transform.position.angleXZ(target)

movingForce.x = speed * math.cos(angle)
movingForce.z = speed * math.sin(angle)
entityBaseData.rotation.y = -angle
transform.rotation.y = -angle
}

timeout -= 1
Expand Down
121 changes: 48 additions & 73 deletions game/src/main/scala/hexacraft/world/entity/base.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,42 @@ import hexacraft.world.{CylinderSize, HexBox}
import hexacraft.world.coord.CylCoords

import com.martomate.nbt.Nbt
import org.joml.{Matrix4f, Vector3d}

class Entity(
val typeName: String,
val data: EntityBaseData,
val model: Option[EntityModel],
val ai: Option[EntityAI],
val boundingBox: HexBox
) {
def position: CylCoords = data.position
def rotation: Vector3d = data.rotation
def velocity: Vector3d = data.velocity
object Entity {
def apply(typeName: String, components: Seq[EntityComponent]): Entity = new Entity(typeName, components)
}

def transform: Matrix4f = data.transform
class Entity(val typeName: String, private val components: Seq[EntityComponent] = Nil) {
val transform: TransformComponent = components
.find(_.isInstanceOf[TransformComponent])
.map(_.asInstanceOf[TransformComponent])
.orNull

def toNBT: Nbt.MapTag =
val dataNbt = data.toNBT
val velocity: VelocityComponent = components
.find(_.isInstanceOf[VelocityComponent])
.map(_.asInstanceOf[VelocityComponent])
.orNull

val boundingBox: HexBox = components
.find(_.isInstanceOf[BoundsComponent])
.map(_.asInstanceOf[BoundsComponent].bounds)
.orNull

val model: Option[EntityModel] = components
.find(_.isInstanceOf[ModelComponent])
.map(_.asInstanceOf[ModelComponent].model)

val ai: Option[EntityAI] = components
.find(_.isInstanceOf[AiComponent])
.map(_.asInstanceOf[AiComponent].ai)

def toNBT: Nbt.MapTag =
Nbt
.makeMap(
"type" -> Nbt.StringTag(typeName),
"pos" -> dataNbt.pos,
"velocity" -> dataNbt.velocity,
"rotation" -> dataNbt.rotation
"pos" -> Nbt.makeVectorTag(transform.position.toVector3d),
"velocity" -> Nbt.makeVectorTag(velocity.velocity),
"rotation" -> Nbt.makeVectorTag(transform.rotation)
)
.withOptionalField("ai", ai.map(_.toNBT))
}
Expand All @@ -39,69 +51,32 @@ object EntityFactory:
private val sheepBounds = new HexBox(0.4f, 0, 0.75f)

def atStartPos(pos: CylCoords, entityType: String)(using CylinderSize): Result[Entity, String] =
entityType match
case "player" =>
val model = PlayerEntityModel.create("player")
Ok(Entity("player", new EntityBaseData(pos), Some(model), Some(SimpleWalkAI.create), playerBounds))

case "sheep" =>
val model = SheepEntityModel.create("sheep")
Ok(Entity("sheep", new EntityBaseData(pos), Some(model), Some(SimpleWalkAI.create), sheepBounds))

case _ => Err(s"Entity-type '$entityType' not found")
fromNbt(Nbt.makeMap("type" -> Nbt.StringTag(entityType))).map: e =>
e.transform.position = pos
e

def fromNbt(tag: Nbt.MapTag)(using CylinderSize): Result[Entity, String] =
val entType = tag.getString("type", "")

entType match
case "player" =>
val model = PlayerEntityModel.create("player")
val baseData = EntityBaseData.fromNBT(tag)
val ai: EntityAI =
tag.getMap("ai") match
case Some(t) => SimpleWalkAI.fromNBT(t)
case None => SimpleWalkAI.create

Ok(Entity("player", baseData, Some(model), Some(ai), playerBounds))
val components = Seq(
TransformComponent.fromNBT(tag),
VelocityComponent.fromNBT(tag),
AiComponent.fromNBT(tag),
BoundsComponent(playerBounds),
ModelComponent(PlayerEntityModel.create("player"))
)
Ok(Entity("player", components))

case "sheep" =>
val model = SheepEntityModel.create("sheep")
val baseData = EntityBaseData.fromNBT(tag)
val ai: EntityAI =
tag.getMap("ai") match
case Some(t) => SimpleWalkAI.fromNBT(t)
case None => SimpleWalkAI.create
Ok(Entity("sheep", baseData, Some(model), Some(ai), sheepBounds))
val components = Seq(
TransformComponent.fromNBT(tag),
VelocityComponent.fromNBT(tag),
AiComponent.fromNBT(tag),
BoundsComponent(sheepBounds),
ModelComponent(SheepEntityModel.create("sheep"))
)
Ok(Entity("sheep", components))

case _ => Err(s"Entity-type '$entType' not found")

class EntityBaseData(
var position: CylCoords,
var rotation: Vector3d = new Vector3d,
var velocity: Vector3d = new Vector3d
):
def transform: Matrix4f = new Matrix4f()
.translate(position.toVector3f)
.rotateZ(rotation.z.toFloat)
.rotateX(rotation.x.toFloat)
.rotateY(rotation.y.toFloat)

def toNBT: EntityBaseData.NbtData = EntityBaseData.NbtData(
pos = Nbt.makeVectorTag(position.toVector3d),
velocity = Nbt.makeVectorTag(velocity),
rotation = Nbt.makeVectorTag(rotation)
)

object EntityBaseData:
case class NbtData(pos: Nbt.MapTag, velocity: Nbt.MapTag, rotation: Nbt.MapTag)

def fromNBT(tag: Nbt.MapTag)(using CylinderSize): EntityBaseData =
val position = tag
.getMap("pos")
.map(t => CylCoords(t.setVector(new Vector3d)))
.getOrElse(CylCoords(0, 0, 0))

val data = new EntityBaseData(position = position)
tag.getMap("velocity").foreach(t => t.setVector(data.velocity))
tag.getMap("rotation").foreach(t => t.setVector(data.rotation))
data
51 changes: 51 additions & 0 deletions game/src/main/scala/hexacraft/world/entity/components.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package hexacraft.world.entity

import hexacraft.world.{CylinderSize, HexBox}
import hexacraft.world.coord.CylCoords

import com.martomate.nbt.Nbt
import org.joml.{Matrix4f, Vector3d}

trait EntityComponent

class TransformComponent(var position: CylCoords, var rotation: Vector3d = new Vector3d) extends EntityComponent:
def transform: Matrix4f = new Matrix4f()
.translate(position.toVector3f)
.rotateZ(rotation.z.toFloat)
.rotateX(rotation.x.toFloat)
.rotateY(rotation.y.toFloat)

object TransformComponent:
def fromNBT(tag: Nbt.MapTag)(using CylinderSize): TransformComponent =
val pos = tag
.getMap("pos")
.map(t => CylCoords(t.setVector(new Vector3d)))
.getOrElse(CylCoords(0, 0, 0))

val rot = new Vector3d
tag.getMap("rotation").foreach(_.setVector(rot))

TransformComponent(pos, rot)

class VelocityComponent(var velocity: Vector3d = new Vector3d) extends EntityComponent

object VelocityComponent:
def fromNBT(tag: Nbt.MapTag)(using CylinderSize): VelocityComponent =
val vel = new Vector3d
tag.getMap("velocity").foreach(_.setVector(vel))

VelocityComponent(vel)

class ModelComponent(val model: EntityModel) extends EntityComponent

class AiComponent(val ai: EntityAI) extends EntityComponent

object AiComponent:
def fromNBT(tag: Nbt.MapTag)(using CylinderSize): AiComponent =
val ai: EntityAI =
tag.getMap("ai") match
case Some(t) => SimpleWalkAI.fromNBT(t)
case None => SimpleWalkAI.create
AiComponent(ai)

class BoundsComponent(val bounds: HexBox) extends EntityComponent
18 changes: 9 additions & 9 deletions game/src/main/scala/hexacraft/world/entity/physics.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ import org.joml.Vector3d
class EntityPhysicsSystem(world: BlocksInWorld, collisionDetector: CollisionDetector)(using
CylinderSize
) {
def update(data: EntityBaseData, boundingBox: HexBox): Unit =
applyBuoyancy(data.velocity, 75, volumeSubmergedInWater(boundingBox, data.position), Density.water)
def update(transform: TransformComponent, velocity: VelocityComponent, boundingBox: HexBox): Unit =
applyBuoyancy(velocity.velocity, 75, volumeSubmergedInWater(boundingBox, transform.position), Density.water)

data.velocity.y -= 9.82 / 60
data.velocity.div(60)
velocity.velocity.y -= 9.82 / 60
velocity.velocity.div(60)
val (pos, vel) = collisionDetector.positionAndVelocityAfterCollision(
boundingBox,
data.position.toVector3d,
data.velocity
transform.position.toVector3d,
velocity.velocity
)
data.position = CylCoords(pos)
data.velocity.set(vel)
data.velocity.mul(60)
transform.position = CylCoords(pos)
velocity.velocity.set(vel)
velocity.velocity.mul(60)

private def volumeSubmergedInWater(bounds: HexBox, position: CylCoords): Double =
val solidBounds = bounds.scaledRadially(0.7)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ object EntityRenderDataFactory {
val tr = new Matrix4f

for ent <- entities if ent.model.isDefined yield
val baseT = ent.transform
val baseT = ent.transform.transform
val model = ent.model.get

val parts = for part <- model.parts yield
Expand Down
Loading

0 comments on commit faa8429

Please sign in to comment.