Scene module overhaul: per-node script entities (SceneObject/SceneEntity/NodeRef), script ECS view-cache perf, generic multi-link system (renderable/physics/camera/audio), API completeness (transform getters, hierarchy ops, quaternions, tags/layer), incremental transforms, id index, reconcile moves, node events, JSON serialization, ray/frustum picking, spatial-index sync and SceneLoader auto AABB. Includes Cmdline/PhysFS argv0 robustness fix for unit tests.
365 lines
10 KiB
C++
365 lines
10 KiB
C++
#include "ScriptTest.h"
|
|
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
static const char* kScriptEcsContent = R"SQ(
|
|
function testEcsReady() {
|
|
return eve.ecsReady()
|
|
}
|
|
|
|
function testDefineClassAndComponent() {
|
|
class Position extends eve.Component {
|
|
x = 0.0
|
|
y = 0.0
|
|
}
|
|
class Velocity extends eve.Component {
|
|
x = 1.0
|
|
y = 2.0
|
|
}
|
|
class Moveable extends eve.Entity {
|
|
pos = Position
|
|
vel = Velocity
|
|
}
|
|
|
|
local e = Moveable.create()
|
|
if (e == null) return false
|
|
if (!e.isAlive()) return false
|
|
if (e.getId() <= 0) return false
|
|
if (e.pos.x != 0.0 || e.pos.y != 0.0) return false
|
|
if (e.vel.x != 1.0 || e.vel.y != 2.0) return false
|
|
|
|
e.pos.x = 10.0
|
|
e.vel.x = 3.0
|
|
if (e.pos.x != 10.0) return false
|
|
if (e.getComponent(Position).x != 10.0) return false
|
|
if (!e.hasComponent(Velocity)) return false
|
|
return true
|
|
}
|
|
|
|
function testViewIncludesSubclass() {
|
|
class Health extends eve.Component {
|
|
hp = 100
|
|
}
|
|
class Actor extends eve.Entity {
|
|
hp = Health
|
|
}
|
|
class Enemy extends Actor {
|
|
}
|
|
|
|
local a = Actor.create()
|
|
local b = Enemy.create()
|
|
a.hp.hp = 50
|
|
b.hp.hp = 80
|
|
|
|
local actors = eve.view(Actor)
|
|
if (actors.len() < 2) return false
|
|
|
|
local enemies = eve.view(Enemy)
|
|
if (enemies.len() < 1) return false
|
|
|
|
local foundEnemy = false
|
|
foreach (e in actors) {
|
|
if (e == b) foundEnemy = true
|
|
}
|
|
if (!foundEnemy) return false
|
|
return true
|
|
}
|
|
|
|
function testSystemUpdate() {
|
|
class Position extends eve.Component {
|
|
x = 0.0
|
|
}
|
|
class Velocity extends eve.Component {
|
|
x = 5.0
|
|
}
|
|
class Mover extends eve.Entity {
|
|
pos = Position
|
|
vel = Velocity
|
|
}
|
|
class MoveSys extends eve.System {
|
|
constructor() {
|
|
base.constructor(Mover)
|
|
}
|
|
function update(dt) {
|
|
foreach (e in entities()) {
|
|
e.pos.x += e.vel.x * dt
|
|
}
|
|
}
|
|
}
|
|
|
|
local e = Mover.create()
|
|
local sys = MoveSys()
|
|
sys.update(2.0)
|
|
if (e.pos.x != 10.0) return false
|
|
e.destroy()
|
|
if (e.isAlive()) return false
|
|
// destroyed entities leave the view
|
|
foreach (ent in eve.view(Mover)) {
|
|
if (ent == e) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
function testSystemDynamicSpawnDespawn() {
|
|
class Position extends eve.Component { x = 0.0 }
|
|
class Velocity extends eve.Component { x = 1.0 }
|
|
class Mover extends eve.Entity {
|
|
pos = Position
|
|
vel = Velocity
|
|
}
|
|
class MoveSys extends eve.System {
|
|
constructor() { base.constructor(Mover) }
|
|
function update(dt) {
|
|
local dying = []
|
|
foreach (e in entities()) {
|
|
e.pos.x += e.vel.x * dt
|
|
if (e.pos.x >= 2.0) dying.push(e)
|
|
}
|
|
foreach (e in dying) e.destroy()
|
|
}
|
|
}
|
|
|
|
local sys = MoveSys()
|
|
local a = Mover.create()
|
|
local b = Mover.create()
|
|
a.vel.x = 1.0
|
|
b.vel.x = 1.0
|
|
if (eve.view(Mover).len() != 2) return false
|
|
|
|
sys.update(1.0)
|
|
if (a.pos.x != 1.0 || b.pos.x != 1.0) return false
|
|
if (eve.view(Mover).len() != 2) return false
|
|
|
|
sys.update(1.0)
|
|
if (a.isAlive() || b.isAlive()) return false
|
|
if (eve.view(Mover).len() != 0) return false
|
|
|
|
local c = Mover.create()
|
|
c.vel.x = 1.0
|
|
sys.update(0.5)
|
|
if (!c.isAlive() || c.pos.x != 0.5) return false
|
|
if (eve.view(Mover).len() != 1) return false
|
|
|
|
// A later entities() call in the same update sees newly created instances.
|
|
class SpawnSys extends eve.System {
|
|
snapshot = 0
|
|
constructor() { base.constructor(Mover) }
|
|
function update(dt) {
|
|
snapshot = entities().len()
|
|
local extra = Mover.create()
|
|
extra.pos.x = 99.0
|
|
if (entities().len() != snapshot + 1) return false
|
|
foreach (e in entities()) {
|
|
if (e.pos.x >= 2.0) e.destroy()
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
local spawnSys = SpawnSys()
|
|
if (!spawnSys.update(0.0)) return false
|
|
if (spawnSys.snapshot != 1) return false
|
|
if (eve.view(Mover).len() != 1) return false
|
|
foreach (e in eve.view(Mover)) {
|
|
if (e != c) return false
|
|
e.destroy()
|
|
}
|
|
return eve.view(Mover).len() == 0
|
|
}
|
|
|
|
function testSystemQueryArrayAndSubclass() {
|
|
class Hp extends eve.Component { v = 1 }
|
|
class Actor extends eve.Entity { hp = Hp }
|
|
class Enemy extends Actor {}
|
|
class Prop extends eve.Entity { hp = Hp }
|
|
|
|
class TickSys extends eve.System {
|
|
constructor() { base.constructor([Actor, Prop]) }
|
|
function update(dt) {
|
|
foreach (e in entities()) e.hp.v += 1
|
|
}
|
|
}
|
|
|
|
local actor = Actor.create()
|
|
local enemy = Enemy.create()
|
|
local prop = Prop.create()
|
|
local sys = TickSys()
|
|
sys.update(0.0)
|
|
if (actor.hp.v != 2 || enemy.hp.v != 2 || prop.hp.v != 2) return false
|
|
|
|
sys.setQuery(Enemy)
|
|
sys.update(0.0)
|
|
if (actor.hp.v != 2 || enemy.hp.v != 3 || prop.hp.v != 2) return false
|
|
|
|
local emptySys = eve.System()
|
|
if (emptySys.entities().len() != 0) return false
|
|
|
|
actor.destroy()
|
|
enemy.destroy()
|
|
prop.destroy()
|
|
return true
|
|
}
|
|
|
|
function testTypeMarkersAndEntityContainer() {
|
|
class Status extends eve.Component {
|
|
alive = eve.Boolean
|
|
name = eve.String
|
|
score = eve.Number
|
|
}
|
|
class Thing extends eve.EntityContainer {
|
|
status = Status
|
|
}
|
|
local t = Thing.create()
|
|
if (t.status.alive != false) return false
|
|
if (t.status.name != "") return false
|
|
if (t.status.score != 0.0) return false
|
|
t.status.alive = true
|
|
t.status.name = "orb"
|
|
t.status.score = 3.5
|
|
if (!t.status.alive || t.status.name != "orb" || t.status.score != 3.5) return false
|
|
return eve.EntityContainer == eve.Entity
|
|
}
|
|
|
|
function testStaticComponentsTable() {
|
|
class Pos extends eve.Component {
|
|
x = 1.0
|
|
}
|
|
class Node extends eve.Entity {
|
|
static components = { pos = Pos }
|
|
pos = null
|
|
}
|
|
local n = Node.create()
|
|
if (n.pos == null) return false
|
|
if (n.pos.x != 1.0) return false
|
|
return true
|
|
}
|
|
|
|
function testShaderSystemClassExists() {
|
|
if (!("ShaderSystem" in eve)) return false
|
|
// Construction without GPU is allowed; update() no-ops until setGpgpu.
|
|
class Pos extends eve.Component { x = 0.0; y = 0.0 }
|
|
class Vel extends eve.Component { x = 1.0; y = 0.0 }
|
|
class Mover extends eve.Entity { pos = Pos; vel = Vel }
|
|
local sys = eve.ShaderSystem(Mover)
|
|
sys.bindFields(0, "pos", ["x", "y"])
|
|
sys.bindFields(1, "vel", ["x", "y"])
|
|
sys.update(0.016)
|
|
return true
|
|
}
|
|
|
|
function testViewCacheStableAndInvalidated() {
|
|
class Pos extends eve.Component { x = 0.0 }
|
|
class Node extends eve.Entity { pos = Pos }
|
|
class Sub extends Node {}
|
|
|
|
local a = Node.create()
|
|
local b = Sub.create()
|
|
|
|
// No changes between calls → same cached array identity (zero allocation
|
|
// per frame in System.entities()).
|
|
local v1 = eve.view(Node)
|
|
if (v1.len() != 2) return false
|
|
local v2 = eve.view(Node)
|
|
if (v1 != v2) return false
|
|
|
|
local sv1 = eve.view(Sub)
|
|
if (sv1.len() != 1) return false
|
|
|
|
// create() invalidates the cached view (and subclass views via the chain)
|
|
local c = Node.create()
|
|
if (eve.view(Node).len() != 3) return false
|
|
if (eve.view(Sub).len() != 1) return false
|
|
|
|
// destroy() invalidates too; dead entities leave the view immediately
|
|
a.destroy()
|
|
b.destroy()
|
|
c.destroy()
|
|
if (eve.view(Node).len() != 0) return false
|
|
return true
|
|
}
|
|
|
|
function testViewCacheBaseClassIncludesSubclasses() {
|
|
class Pos extends eve.Component { x = 0.0 }
|
|
class Node extends eve.Entity { pos = Pos }
|
|
class Sub extends Node {}
|
|
|
|
local n = Node.create()
|
|
local s = Sub.create()
|
|
if (eve.view(Node).len() != 2) return false
|
|
if (eve.view(eve.Entity).len() < 2) return false
|
|
n.destroy()
|
|
s.destroy()
|
|
return true
|
|
}
|
|
|
|
function testComponentDefaultsAndSlotsCached() {
|
|
class C extends eve.Component { v = eve.Number; s = eve.String }
|
|
class E extends eve.Entity { c = C }
|
|
|
|
local a = E.create()
|
|
if (a.c.v != 0.0 || a.c.s != "") return false
|
|
a.c.v = 5.0
|
|
|
|
// Second instance gets fresh resolved defaults, not the mutated values.
|
|
local b = E.create()
|
|
if (b.c.v != 0.0 || b.c.s != "") return false
|
|
if (a.c.v != 5.0) return false
|
|
if (a.getComponent(C) != a.c) return false
|
|
if (!a.hasComponent(C)) return false
|
|
|
|
a.destroy()
|
|
b.destroy()
|
|
return true
|
|
}
|
|
)SQ";
|
|
|
|
UnitSciptTest(ScriptEcsTest, kScriptEcsContent);
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.ready") {
|
|
CHECK(vm.callFunc(vm.findFunc("testEcsReady"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.defineClassAndComponent") {
|
|
CHECK(vm.callFunc(vm.findFunc("testDefineClassAndComponent"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewIncludesSubclass") {
|
|
CHECK(vm.callFunc(vm.findFunc("testViewIncludesSubclass"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemUpdate") {
|
|
CHECK(vm.callFunc(vm.findFunc("testSystemUpdate"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemDynamicSpawnDespawn") {
|
|
CHECK(vm.callFunc(vm.findFunc("testSystemDynamicSpawnDespawn"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemQueryArrayAndSubclass") {
|
|
CHECK(vm.callFunc(vm.findFunc("testSystemQueryArrayAndSubclass"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.typeMarkersAndEntityContainer") {
|
|
CHECK(vm.callFunc(vm.findFunc("testTypeMarkersAndEntityContainer"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.staticComponentsTable") {
|
|
CHECK(vm.callFunc(vm.findFunc("testStaticComponentsTable"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemClassExists") {
|
|
CHECK(vm.callFunc(vm.findFunc("testShaderSystemClassExists"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheStableAndInvalidated") {
|
|
CHECK(vm.callFunc(vm.findFunc("testViewCacheStableAndInvalidated"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheBaseClassIncludesSubclasses") {
|
|
CHECK(vm.callFunc(vm.findFunc("testViewCacheBaseClassIncludesSubclasses"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.componentDefaultsAndSlotsCached") {
|
|
CHECK(vm.callFunc(vm.findFunc("testComponentDefaultsAndSlotsCached"), vm).toBool());
|
|
}
|