载入中...
搜索中...
未找到
ECS.cpp
浏览该文件的文档.
1#include "common/ECS.h"
3
4#include <simplesquirrel/simplesquirrel.hpp>
5
6#include <cstdio>
7#include <cstring>
8#include <string>
9#include <unordered_map>
10#include <vector>
11
12namespace eve {
13namespace {
14
15// ---------------------------------------------------------------------------
16// Script component registry (name → Squirrel class)
17// ---------------------------------------------------------------------------
18
19struct ScriptComponentReg {
20 HSQUIRRELVM vm = nullptr;
21 HSQOBJECT cls;
22 ScriptComponentReg() { sq_resetobject(&cls); }
23 ScriptComponentReg(HSQUIRRELVM v, HSQOBJECT o) : vm(v), cls(o) {}
24};
25
26std::unordered_map<std::string, ScriptComponentReg>& scriptComponents() {
27 static std::unordered_map<std::string, ScriptComponentReg> m;
28 return m;
29}
30
31int& nextEntityId() {
32 static int id = 1;
33 return id;
34}
35
36int allocEntityId() { return nextEntityId()++; }
37
38// 当前 VM 的 Squirrel 类对象 → 脚本 view() 收集函数。
39std::unordered_map<const void*, CppEntityViewFn>& cppEntityViews() {
40 static std::unordered_map<const void*, CppEntityViewFn> views;
41 return views;
42}
43
44std::vector<std::function<void(ssq::Table&)>>& postEcsHooks() {
45 static std::vector<std::function<void(ssq::Table&)>> hooks;
46 return hooks;
47}
48
49bool& ecsScriptInjected() {
50 static bool injected = false;
51 return injected;
52}
53
54size_t& postEcsHooksFlushed() {
55 static size_t n = 0;
56 return n;
57}
58
59// ---------------------------------------------------------------------------
60// Class helpers
61// ---------------------------------------------------------------------------
62
63bool isSubclassOf(HSQUIRRELVM vm, HSQOBJECT child, HSQOBJECT base) {
64 if (child._type != OT_CLASS || base._type != OT_CLASS) return false;
65 const SQInteger top = sq_gettop(vm);
66 sq_pushobject(vm, child);
67 for (int guard = 0; guard < 64; ++guard) {
68 if (sq_gettype(vm, -1) != OT_CLASS) break;
69 HSQOBJECT cur;
70 sq_getstackobj(vm, -1, &cur);
71 if (cur._unVal.pClass == base._unVal.pClass) {
72 sq_settop(vm, top);
73 return true;
74 }
75 if (SQ_FAILED(sq_getbase(vm, -1))) break;
76 sq_remove(vm, -2); // drop current, keep base
77 if (sq_gettype(vm, -1) != OT_CLASS) break;
78 }
79 sq_settop(vm, top);
80 return false;
81}
82
84void collectClassFields(HSQUIRRELVM vm, HSQOBJECT clsObj, HSQOBJECT outTable) {
85 const SQInteger top = sq_gettop(vm);
86 sq_pushobject(vm, outTable); // out
87 sq_pushobject(vm, clsObj); // out, cls
88 sq_pushnull(vm); // out, cls, iter
89 while (SQ_SUCCEEDED(sq_next(vm, -2))) {
90 // out, cls, iter, key, value
91 if (sq_gettype(vm, -2) == OT_STRING) {
92 const SQChar* key = nullptr;
93 sq_getstring(vm, -2, &key);
94 if (key && key[0] != '_') {
95 const SQObjectType vt = sq_gettype(vm, -1);
96 if (vt != OT_CLOSURE && vt != OT_NATIVECLOSURE && vt != OT_GENERATOR) {
97 sq_push(vm, -5); // out
98 sq_push(vm, -3); // key
99 sq_push(vm, -3); // value
100 sq_rawset(vm, -3);
101 sq_pop(vm, 1); // pop out
102 }
103 }
104 }
105 sq_pop(vm, 2); // key, value
106 }
107 sq_settop(vm, top);
108}
109
110ssq::Table inspectClassFields(ssq::Object clsObj) {
111 HSQUIRRELVM vm = clsObj.getHandle();
112 ssq::Table out(vm);
113 if (clsObj.getType() != ssq::Type::CLASS) return out;
114 collectClassFields(vm, clsObj.getRaw(), out.getRaw());
115 return out;
116}
117
118bool scriptIsSubclass(ssq::Object child, ssq::Object base) {
119 if (child.getType() != ssq::Type::CLASS || base.getType() != ssq::Type::CLASS)
120 return false;
121 return isSubclassOf(child.getHandle(), child.getRaw(), base.getRaw());
122}
123
124ssq::Object getClassBase(ssq::Object cls) {
125 HSQUIRRELVM vm = cls.getHandle();
126 ssq::Object ret(vm);
127 if (cls.getType() != ssq::Type::CLASS) return ret;
128 const SQInteger top = sq_gettop(vm);
129 sq_pushobject(vm, cls.getRaw());
130 if (SQ_FAILED(sq_getbase(vm, -1)) || sq_gettype(vm, -1) != OT_CLASS) {
131 sq_settop(vm, top);
132 return ret;
133 }
134 sq_getstackobj(vm, -1, &ret.getRaw());
135 sq_addref(vm, &ret.getRaw());
136 sq_settop(vm, top);
137 return ret;
138}
139
140void registerScriptComponent(const std::string& name, ssq::Object cls) {
141 if (cls.getType() != ssq::Type::CLASS) return;
142 HSQUIRRELVM vm = cls.getHandle();
143 HSQOBJECT o = cls.getRaw();
144 sq_addref(vm, &o);
145 auto& m = scriptComponents();
146 auto it = m.find(name);
147 if (it != m.end()) {
148 if (it->second.vm) sq_release(it->second.vm, &it->second.cls);
149 it->second = ScriptComponentReg(vm, o);
150 } else {
151 m.emplace(name, ScriptComponentReg(vm, o));
152 }
153}
154
155// ---------------------------------------------------------------------------
156// Injected Squirrel runtime
157// ---------------------------------------------------------------------------
158
159const char* kEcsScript = R"SQ(
160// Script ECS: define Component / Entity classes, create instances, view & System.
161// Mirrors ECS.hpp's class + component model for Squirrel game logic.
162
163if (!("Number" in eve)) eve.Number <- "number"
164if (!("Boolean" in eve)) eve.Boolean <- "boolean"
165if (!("String" in eve)) eve.String <- "string"
166
167eve._ecsTypes <- {} // EntityClass -> { cls, instances }
168
169// --- 性能缓存(万级实体稳态零分配) ---
170// view 结果缓存:create/destroy 时按类链失效;C++ 桥接类(_cppHasView=true)不缓存。
171eve._ecsViewCache <- {} // EntityClass -> array
172eve._ecsSlotsCache <- {} // EntityClass -> { fieldName = ComponentClass }
173eve._ecsCompDefaults <- {} // ComponentClass -> { fieldName = resolvedDefault }
174eve._ecsRevision <- 0 // increments on script entity structural changes
175
176function eve::_ecsResolveMarker(v) {
177 if (v == this.Number || v == "number") return 0.0
178 if (v == this.Boolean || v == "boolean") return false
179 if (v == this.String || v == "string") return ""
180 return v
181}
182
183function eve::_ecsIsComponentClass(obj) {
184 if (typeof obj != "class") return false
185 return this.isSubclass(obj, this.Component)
186}
187
188function eve::_ecsInstantiateComponent(compClass) {
189 local c = compClass()
190 if (compClass in this._ecsCompDefaults) {
191 foreach (name, val in this._ecsCompDefaults[compClass]) c[name] = val
192 } else {
193 local fields = this.inspectClassFields(compClass)
194 local resolved = {}
195 foreach (name, val in fields) {
196 local cur = null
197 try { cur = c[name] } catch (e) { cur = null }
198 if (cur == this.Number || cur == this.Boolean || cur == this.String
199 || cur == "number" || cur == "boolean" || cur == "string") {
200 c[name] = this._ecsResolveMarker(cur)
201 resolved[name] <- c[name]
202 }
203 }
204 this._ecsCompDefaults[compClass] <- resolved
205 }
206 return c
207}
208
209function eve::_ecsCollectSlots(cls) {
210 if (cls in this._ecsSlotsCache) return this._ecsSlotsCache[cls]
211 // { fieldName = ComponentClass } walking base classes (Entity → … → cls).
212 local slots = {}
213 local cur = cls
214 local guard = 0
215 while (cur != null && guard < 64) {
216 if (!this.isSubclass(cur, this.Entity)) break
217
218 local declared = null
219 try { declared = cur.components } catch (e) { declared = null }
220 if (typeof declared == "table") {
221 foreach (k, v in declared) {
222 if (!(k in slots) && this._ecsIsComponentClass(v))
223 slots[k] <- v
224 }
225 }
226
227 local fields = this.inspectClassFields(cur)
228 foreach (k, v in fields) {
229 if (k == "components") continue
230 if (!(k in slots) && this._ecsIsComponentClass(v))
231 slots[k] <- v
232 }
233
234 cur = this.getClassBase(cur)
235 guard += 1
236 }
237 this._ecsSlotsCache[cls] <- slots
238 return slots
239}
240
241function eve::_ecsInvalidateViews(cls) {
242 local cur = cls
243 local guard = 0
244 while (cur != null && guard < 64) {
245 if (cur in this._ecsViewCache) delete this._ecsViewCache[cur]
246 if (!this.isSubclass(cur, this.Entity)) break
247 cur = this.getClassBase(cur)
248 guard += 1
249 }
250}
251
252function eve::_ecsEnsureType(cls) {
253 if (cls in this._ecsTypes) return this._ecsTypes[cls]
254 local info = { cls = cls, instances = [] }
255 this._ecsTypes[cls] <- info
256 return info
257}
258
259function eve::_ecsRegisterInstance(entity, cls) {
260 this._ecsEnsureType(cls).instances.push(entity)
261 this._ecsRevision += 1
262 this._ecsInvalidateViews(cls)
263}
264
265function eve::_ecsUnregisterInstance(entity, cls) {
266 if (!(cls in this._ecsTypes)) return
267 local arr = this._ecsTypes[cls].instances
268 for (local i = arr.len() - 1; i >= 0; --i) {
269 if (arr[i] == entity) {
270 // O(1) swap-remove(顺序不保证);视图缓存随后失效重建
271 local last = arr.len() - 1
272 if (i != last) arr[i] = arr[last]
273 arr.pop()
274 this._ecsRevision += 1
275 break
276 }
277 }
278 this._ecsInvalidateViews(cls)
279}
280
281function eve::_ecsCollectInstances(cls, out) {
282 foreach (key, info in this._ecsTypes) {
283 if (info.cls == cls || this.isSubclass(info.cls, cls)) {
284 foreach (e in info.instances) {
285 if (e != null && e.isAlive()) out.push(e)
286 }
287 }
288 }
289}
290
291eve.Component <- class {
292 constructor() {}
293}
294
295eve.Entity <- class {
296 _eid = 0
297 _alive = false
298 _etype = null
299 _slots = null
300 _comps = null
301
302 constructor() {
303 _eid = 0
304 _alive = false
305 _etype = null
306 _slots = null
307 _comps = null
308 }
309
310 static function create() {
311 local e = this()
312 e._etype = this
313 e._eid = eve.allocEntityId()
314 e._alive = true
315 e._slots = eve._ecsCollectSlots(this)
316 e._comps = {}
317 foreach (name, compClass in e._slots) {
318 local c = eve._ecsInstantiateComponent(compClass)
319 e._comps[name] <- c
320 try { e[name] = c } catch (ex) {}
321 }
322 eve._ecsRegisterInstance(e, this)
323 return e
324 }
325
326 // Declare a component slot on this entity class (optional sugar).
327 static function component(name, compClass) {
328 if (!("components" in this) || typeof this.components != "table")
329 this.components <- {}
330 this.components[name] <- compClass
331 return this
332 }
333
334 function getId() { return _eid }
335 function isAlive() { return _alive }
336
337 function getComponent(compClass) {
338 if (_slots == null || _comps == null) return null
339 foreach (name, cls in _slots) {
340 if (cls == compClass) {
341 if (name in _comps) return _comps[name]
342 return null
343 }
344 }
345 return null
346 }
347
348 function hasComponent(compClass) {
349 return getComponent(compClass) != null
350 }
351
352 function destroy() {
353 if (!_alive) return
354 _alive = false
355 if (_etype != null) eve._ecsUnregisterInstance(this, _etype)
356 }
357}
358
359// Historical name from early game examples
360eve.EntityContainer <- eve.Entity
361
362eve.System <- class {
363 _query = null
364
365 constructor(query = null) {
366 _query = query
367 }
368
369 function setQuery(query) { _query = query }
370
371 function entities() {
372 if (_query == null) return []
373 if (typeof _query == "array") {
374 local out = []
375 foreach (q in _query) {
376 foreach (e in eve.view(q)) out.push(e)
377 }
378 return out
379 }
380 return eve.view(_query)
381 }
382
383 function update(dt) {}
384}
385
386// GPU-backed System: pack float component fields → SSBO → compute shader → unpack.
387// Requires eve.Gpgpu / eve.EcsShaderSystem (gpgpu module). Push constants:
388// pc.data[0] = dt, pc.data[1] = entityCount. Each bindFields() maps one binding.
389eve.ShaderSystem <- class extends eve.System {
390 _gpu = null
391 _backend = null
392 _bindings = null
393 _readback = true
394 _localSize = 64
395
396 constructor(query = null, gpu = null, glsl = null, localSize = 64) {
397 base.constructor(query)
398 _bindings = []
399 _readback = true
400 _localSize = localSize
401 if (gpu != null) setGpgpu(gpu)
402 if (gpu != null && glsl != null) setShaderSource(glsl)
403 }
404
405 function setGpgpu(gpu) {
406 _gpu = gpu
407 if (_backend == null) {
408 if (!("EcsShaderSystem" in eve))
409 throw "eve.ShaderSystem requires gpgpu module (eve.EcsShaderSystem)"
410 _backend = eve.EcsShaderSystem()
411 }
412 _backend.setGpgpu(gpu)
413 _backend.setLocalSize(_localSize)
414 return this
415 }
416
417 function setShaderSource(glsl) {
418 if (_backend == null) throw "eve.ShaderSystem.setShaderSource: call setGpgpu first"
419 _backend.setShaderSource(glsl)
420 return this
421 }
422
423 function setLocalSize(n) {
424 _localSize = n
425 if (_backend != null) _backend.setLocalSize(n)
426 return this
427 }
428
429 function setReadback(enabled) {
430 _readback = enabled
431 return this
432 }
433
434 // binding: SSBO index; slot: entity component field name; fields: ["x","y",...]
435 // uploadEachUpdate=false keeps the buffer GPU-resident after its initial upload.
436 // readback=false leaves results resident until requestReadback() is called.
437 function bindFields(binding, slot, fields, uploadEachUpdate = true, readback = null) {
438 _bindings.push({ binding = binding, slot = slot, fields = fields,
439 uploadEachUpdate = uploadEachUpdate,
440 readback = readback, uploadedRevision = -1,
441 uploadRequested = false, readbackRequested = false,
442 uploadFirst = 0, uploadCount = 0,
443 readbackFirst = 0, readbackCount = 0,
444 source = null, sourceBinding = -1,
445 initializeFromCpu = true, schema = null })
446 return this
447 }
448
449 // Typed binding schema: { slot, fields, scalar="f32", access="read_write" }.
450 // write-only bindings allocate storage without an unnecessary CPU upload.
451 function bindSchema(binding, schema, uploadEachUpdate = false, readback = false) {
452 if (typeof schema != "table" || !("slot" in schema) || !("fields" in schema))
453 throw "eve.ShaderSystem.bindSchema: schema requires slot and fields"
454 if (typeof schema.slot != "string" || typeof schema.fields != "array" ||
455 schema.fields.len() <= 0)
456 throw "eve.ShaderSystem.bindSchema: invalid slot or fields"
457 foreach (field in schema.fields)
458 if (typeof field != "string")
459 throw "eve.ShaderSystem.bindSchema: every field must be a string"
460 local scalar = "scalar" in schema ? schema.scalar : "f32"
461 if (scalar != "f32")
462 throw "eve.ShaderSystem.bindSchema: only f32 is currently supported"
463 local access = "access" in schema ? schema.access : "read_write"
464 if (access != "read" && access != "write" && access != "read_write")
465 throw "eve.ShaderSystem.bindSchema: access must be read, write, or read_write"
466 local normalized = { slot = schema.slot, fields = schema.fields,
467 scalar = scalar, access = access,
468 strideFloats = schema.fields.len() }
469 _bindings.push({ binding = binding, slot = schema.slot, fields = schema.fields,
470 uploadEachUpdate = access == "write" ? false : uploadEachUpdate,
471 readback = readback, uploadedRevision = -1,
472 uploadRequested = false, readbackRequested = false,
473 uploadFirst = 0, uploadCount = 0,
474 readbackFirst = 0, readbackCount = 0,
475 source = null, sourceBinding = -1,
476 initializeFromCpu = access != "write", schema = normalized })
477 return this
478 }
479
480 function getBindingSchema(binding) {
481 foreach (b in _bindings) if (b.binding == binding) return b.schema
482 return null
483 }
484
485 // Generate backend declaration boilerplate; the kernel body remains explicit.
486 function shaderDeclarations(language = "glsl") {
487 local out = ""
488 foreach (b in _bindings) {
489 if (b.schema == null) continue
490 if (language == "glsl") {
491 local qualifier = b.schema.access == "read" ? "readonly " :
492 b.schema.access == "write" ? "writeonly " : ""
493 out += "layout(set = 0, binding = " + b.binding + ") " + qualifier +
494 "buffer EcsBinding" + b.binding + " { float data[]; } ecs" +
495 b.binding + ";\n"
496 } else if (language == "wgsl") {
497 local access = b.schema.access == "read" ? "read" : "read_write"
498 out += "@group(0) @binding(" + b.binding + ") var<storage, " + access +
499 "> ecs" + b.binding + " : array<f32>;\n"
500 } else {
501 throw "eve.ShaderSystem.shaderDeclarations: language must be glsl or wgsl"
502 }
503 }
504 return out
505 }
506
507 // Reuse another ShaderSystem's resident buffer without a CPU round trip.
508 // The source system is retained and must be prepared before this consumer.
509 function bindSharedFields(binding, sourceSystem, sourceBinding, slot, fields,
510 readback = false) {
511 if (sourceSystem == null)
512 throw "eve.ShaderSystem.bindSharedFields: source system is null"
513 _bindings.push({ binding = binding, slot = slot, fields = fields,
514 uploadEachUpdate = false, readback = readback,
515 uploadedRevision = -1, uploadRequested = false,
516 readbackRequested = false, uploadFirst = 0, uploadCount = 0,
517 readbackFirst = 0, readbackCount = 0,
518 source = sourceSystem, sourceBinding = sourceBinding,
519 initializeFromCpu = false, schema = null })
520 return this
521 }
522
523 function setBindingUpload(binding, enabled) {
524 foreach (b in _bindings) if (b.binding == binding) b.uploadEachUpdate = enabled
525 return this
526 }
527
528 function setBindingReadback(binding, enabled) {
529 foreach (b in _bindings) if (b.binding == binding) b.readback = enabled
530 return this
531 }
532
533 // Re-upload CPU component values on the next update. binding < 0 selects all.
534 function requestUpload(binding = -1) {
535 foreach (b in _bindings) {
536 if ((binding < 0 || b.binding == binding) && b.source == null) {
537 b.uploadRequested = true
538 b.uploadFirst = 0
539 b.uploadCount = -1
540 }
541 }
542 return this
543 }
544
545 // Re-upload a stable contiguous entity range. Repeated requests are coalesced.
546 function requestUploadRange(binding, firstEntity, entityCount) {
547 if (firstEntity < 0 || entityCount <= 0)
548 throw "eve.ShaderSystem.requestUploadRange: invalid entity range"
549 foreach (b in _bindings) {
550 if (b.binding != binding) continue
551 if (b.source != null)
552 throw "eve.ShaderSystem.requestUploadRange: shared binding is GPU-owned"
553 if (!b.uploadRequested || b.uploadCount == 0) {
554 b.uploadFirst = firstEntity
555 b.uploadCount = entityCount
556 } else if (b.uploadCount >= 0) {
557 local oldEnd = b.uploadFirst + b.uploadCount
558 local newEnd = firstEntity + entityCount
559 local end = oldEnd > newEnd ? oldEnd : newEnd
560 b.uploadFirst = b.uploadFirst < firstEntity ? b.uploadFirst : firstEntity
561 b.uploadCount = end - b.uploadFirst
562 }
563 b.uploadRequested = true
564 }
565 return this
566 }
567
568 // Read GPU values back after the next update. binding < 0 selects all.
569 function requestReadback(binding = -1) {
570 foreach (b in _bindings) {
571 if (binding < 0 || b.binding == binding) {
572 b.readbackRequested = true
573 b.readbackFirst = 0
574 b.readbackCount = -1
575 }
576 }
577 return this
578 }
579
580 // Read back a stable contiguous entity range. Repeated requests are coalesced.
581 function requestReadbackRange(binding, firstEntity, entityCount) {
582 if (firstEntity < 0 || entityCount <= 0)
583 throw "eve.ShaderSystem.requestReadbackRange: invalid entity range"
584 foreach (b in _bindings) {
585 if (b.binding != binding) continue
586 if (!b.readbackRequested || b.readbackCount == 0) {
587 b.readbackFirst = firstEntity
588 b.readbackCount = entityCount
589 } else if (b.readbackCount >= 0) {
590 local oldEnd = b.readbackFirst + b.readbackCount
591 local newEnd = firstEntity + entityCount
592 local end = oldEnd > newEnd ? oldEnd : newEnd
593 b.readbackFirst = b.readbackFirst < firstEntity ? b.readbackFirst : firstEntity
594 b.readbackCount = end - b.readbackFirst
595 }
596 b.readbackRequested = true
597 }
598 return this
599 }
600
601 function setFloat(index, value) {
602 if (_backend != null) _backend.setFloat(index, value)
603 return this
604 }
605
606 function getBackend() { return _backend }
607
608 // Register renderer resources and submit a standard 80-byte GpuInstance binding
609 // without copying the instance stream back through the CPU.
610 function setGpuDrivenEnabled(enabled) {
611 if (_gpu == null) throw "eve.ShaderSystem.setGpuDrivenEnabled: call setGpgpu first"
612 _gpu.setGpuDrivenEnabled(enabled)
613 return this
614 }
615
616 function gpuDrivenMeshRecord(mesh) {
617 if (_gpu == null) throw "eve.ShaderSystem.gpuDrivenMeshRecord: call setGpgpu first"
618 return _gpu.gpuDrivenMeshRecord(mesh)
619 }
620
621 function gpuDrivenMaterialRecord(material) {
622 if (_gpu == null)
623 throw "eve.ShaderSystem.gpuDrivenMaterialRecord: call setGpgpu first"
624 return _gpu.gpuDrivenMaterialRecord(material)
625 }
626
627 function submitResidentInstances(binding, buckets, instanceCount = -1,
628 offsetBytes = 0) {
629 if (_gpu == null || _backend == null)
630 throw "eve.ShaderSystem.submitResidentInstances: call setGpgpu first"
631 local buffer = _backend.getBuffer(binding)
632 if (buffer == null)
633 throw "eve.ShaderSystem.submitResidentInstances: binding has no buffer"
634 if (instanceCount < 0) instanceCount = entities().len()
635 return _gpu.submitResidentInstances(buffer, buckets, instanceCount, offsetBytes)
636 }
637
638 function _prepareGpuBatch() {
639 if (_backend == null || _gpu == null) return
640 if (!("packEcsFloats" in eve) || !("unpackEcsFloats" in eve)) return
641
642 local ents = entities()
643 local n = ents.len()
644 if (n <= 0) return null
645
646 foreach (b in _bindings) {
647 local floatsPer = b.fields.len()
648 if (floatsPer <= 0) continue
649 if (b.source != null) {
650 local sourceBackend = b.source.getBackend()
651 local sourceBuffer = sourceBackend == null ? null :
652 sourceBackend.getBuffer(b.sourceBinding)
653 if (sourceBuffer == null)
654 throw "eve.ShaderSystem shared source must be prepared before consumer"
655 if (sourceBuffer.getSize() < n * floatsPer * 4)
656 throw "eve.ShaderSystem shared buffer is smaller than consumer view"
657 _backend.attachBuffer(b.binding, sourceBuffer)
658 continue
659 }
660 local buf = _backend.ensureBuffer(b.binding, n * floatsPer)
661 local structureChanged = b.uploadedRevision != eve._ecsRevision
662 local fullUpload = (structureChanged && (b.initializeFromCpu || b.uploadRequested)) ||
663 b.uploadEachUpdate ||
664 (b.uploadRequested && b.uploadCount < 0)
665 if (fullUpload) {
666 eve.packEcsFloats(ents, b.slot, b.fields, buf)
667 b.uploadedRevision = eve._ecsRevision
668 b.uploadRequested = false
669 b.uploadCount = 0
670 } else if (structureChanged) {
671 // Output-only schema: capacity/order changed, but the shader is
672 // authoritative and promises to initialize every accessed record.
673 b.uploadedRevision = eve._ecsRevision
674 } else if (b.uploadRequested) {
675 if (!("packEcsFloatsRange" in eve))
676 throw "eve.ShaderSystem.requestUploadRange requires gpgpu range packing"
677 eve.packEcsFloatsRange(ents, b.slot, b.fields, buf,
678 b.uploadFirst, b.uploadCount)
679 b.uploadRequested = false
680 b.uploadCount = 0
681 }
682 }
683 return { entities = ents, count = n }
684 }
685
686 function _completeGpuBatch(batch) {
687 if (batch == null) return
688 local ents = batch.entities
689 local n = batch.count
690 foreach (b in _bindings) {
691 local shouldReadback = b.readback == null ? _readback : b.readback
692 if (shouldReadback || (b.readbackRequested && b.readbackCount < 0)) {
693 local floatsPer = b.fields.len()
694 if (floatsPer <= 0) continue
695 local buf = _backend.getBuffer(b.binding)
696 eve.unpackEcsFloats(ents, b.slot, b.fields, buf, n)
697 b.readbackRequested = false
698 b.readbackCount = 0
699 } else if (b.readbackRequested) {
700 if (!("unpackEcsFloatsRange" in eve))
701 throw "eve.ShaderSystem.requestReadbackRange requires gpgpu range unpacking"
702 local buf = _backend.getBuffer(b.binding)
703 eve.unpackEcsFloatsRange(ents, b.slot, b.fields, buf,
704 b.readbackFirst, b.readbackCount)
705 b.readbackRequested = false
706 b.readbackCount = 0
707 }
708 }
709 }
710
711 // Record into a shared GpuSequence. Call completeRecorded(batch) after submit().
712 function record(sequence, dt) {
713 if (sequence == null) throw "eve.ShaderSystem.record: sequence is null"
714 local batch = _prepareGpuBatch()
715 if (batch != null) _backend.recordDispatch(sequence, batch.count, dt)
716 return batch
717 }
718
719 function completeRecorded(batch) {
720 _completeGpuBatch(batch)
721 return this
722 }
723
724 function update(dt) {
725 local batch = _prepareGpuBatch()
726 if (batch == null) return
727 _backend.dispatch(batch.count, dt)
728 _completeGpuBatch(batch)
729 }
730}
731
732function eve::view(entityClass) {
733 if (entityClass == null) return []
734 // C++ 桥接类的实体在脚本外增删(无法通知脚本),不缓存,每次新鲜收集
735 local cppBacked = false
736 if ("_cppHasView" in this) cppBacked = this._cppHasView(entityClass)
737 if (!cppBacked && (entityClass in this._ecsViewCache)) return this._ecsViewCache[entityClass]
738 local out = []
739 this._ecsCollectInstances(entityClass, out)
740 if ("_cppCollect" in this) this._cppCollect(entityClass, out)
741 if (!cppBacked) this._ecsViewCache[entityClass] <- out
742 return out
743}
744
745function eve::ecsReady() {
746 // This function is installed only after the complete ECS script has run.
747 // Avoid probing the delegated eve table here: Squirrel's `in` lookup may
748 // invoke its lazy _get handler and turn a readiness query into an error.
749 return true
750}
751)SQ";
752
753void injectEcsScript(ssq::Table& eveTable) {
754 HSQUIRRELVM vm = eveTable.getHandle();
755 const SQInteger top = sq_gettop(vm);
756 if (SQ_FAILED(sq_compilebuffer(vm, kEcsScript,
757 static_cast<SQInteger>(std::strlen(kEcsScript)),
758 "ecs.nut", SQTrue))) {
759 sq_settop(vm, top);
760 const script::ScriptErrorContext ctx = script::captureCompileError(vm);
761 std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to compile: %s\n",
762 ctx.empty() ? "unknown error" : ctx.message.c_str());
763 return;
764 }
765 sq_pushroottable(vm);
766 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
767 const script::ScriptErrorContext ctx = script::takeLastScriptError(vm);
768 std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to run: %s\n",
769 ctx.empty() ? "unknown error"
770 : script::formatScriptError(ctx).c_str());
771 }
772 sq_settop(vm, top);
773}
774
780void cppCollect(ssq::Class cls, ssq::Array out) {
781 HSQUIRRELVM vm = cls.getHandle();
782 const SQInteger top = sq_gettop(vm);
783 sq_pushobject(vm, cls.getRaw());
784 for (int guard = 0; guard < 64; ++guard) {
785 if (sq_gettype(vm, -1) != OT_CLASS)
786 break;
787 HSQOBJECT cur;
788 sq_getstackobj(vm, -1, &cur);
789 auto it = cppEntityViews().find(cur._unVal.pClass);
790 if (it != cppEntityViews().end()) {
791 sq_settop(vm, top);
792 it->second(out);
793 return;
794 }
795 if (SQ_FAILED(sq_getbase(vm, -1)))
796 break;
797 sq_remove(vm, -2); // drop current class, keep base
798 }
799 sq_settop(vm, top);
800}
801
807bool cppHasView(ssq::Object cls) {
808 if (cls.getType() != ssq::Type::CLASS) return false;
809 HSQUIRRELVM vm = cls.getHandle();
810 const SQInteger top = sq_gettop(vm);
811 sq_pushobject(vm, cls.getRaw());
812 for (int guard = 0; guard < 64; ++guard) {
813 if (sq_gettype(vm, -1) != OT_CLASS) break;
814 HSQOBJECT cur;
815 sq_getstackobj(vm, -1, &cur);
816 if (cppEntityViews().find(cur._unVal.pClass) != cppEntityViews().end()) {
817 sq_settop(vm, top);
818 return true;
819 }
820 if (SQ_FAILED(sq_getbase(vm, -1))) break;
821 sq_remove(vm, -2); // drop current class, keep base
822 }
823 sq_settop(vm, top);
824 return false;
825}
826
827} // namespace
828
829void exposeECS(ssq::Table& table) {
830 table.set("Number", std::string("number"));
831 table.set("Boolean", std::string("boolean"));
832 table.set("String", std::string("string"));
833 // ecsReady / view / Entity / Component are provided by the injected script.
834
835 table.addFunc("inspectClassFields", inspectClassFields);
836 table.addFunc("isSubclass", scriptIsSubclass);
837 table.addFunc("getClassBase", getClassBase);
838 table.addFunc("allocEntityId", allocEntityId);
839 table.addFunc("_cppHasView", cppHasView);
840 table.addFunc("component", [](std::string name, ssq::Object cls) {
841 registerScriptComponent(name, cls);
842 });
843 table.addFunc("_cppCollect", std::function<void(ssq::Class, ssq::Array)>(cppCollect));
844
845 injectEcsScript(table);
846
847 ecsScriptInjected() = true;
848 // A new VM re-runs every hook (SceneEntity class, …) against this table.
849 postEcsHooksFlushed() = 0;
850 flushPostEcsHooks(table);
851}
852
854 // Class identities belong to one VM. Clear them when a new eve table is
855 // prepared, before any lazy/native exposer registers the next VM's classes.
856 cppEntityViews().clear();
857 ecsScriptInjected() = false;
858 postEcsHooksFlushed() = 0;
859}
860
861void registerCppEntityView(const ssq::Class& cls, CppEntityViewFn fn) {
862 cppEntityViews()[cls.getRaw()._unVal.pClass] = std::move(fn);
863}
864
866 postEcsHooks().push_back(std::move(fn));
867}
868
869void flushPostEcsHooks(ssq::Table& table) {
870 if (!ecsScriptInjected()) return;
871 auto& hooks = postEcsHooks();
872 auto& n = postEcsHooksFlushed();
873 while (n < hooks.size()) {
874 try {
875 hooks[n](table);
876 } catch (...) {
877 // A failing module hook must not break VM exposure.
878 }
879 ++n;
880 }
881}
882
883void exposeECSToVM(ssq::VM& vm) {
884 try {
885 ssq::Table eveTable(vm.find("eve"));
886 exposeECS(eveTable);
887 } catch (...) {
888 }
889}
890
891} // namespace eve
SQInteger top
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::string message
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
float v
std::string name
std::string script
Definition PlayHost.cpp:116
std::string id
Definition PlayHost.cpp:108
float m[16]
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
void prepareEcsScriptInjection()
Resets per-VM script injection bookkeeping before a new VM is exposed.
Definition ECS.cpp:853
void flushPostEcsHooks(ssq::Table &table)
Run post-ECS hooks that were registered after exposeECS already ran.
Definition ECS.cpp:869
void exposeECS(ssq::Table &table)
脚本侧 ECS:Entity / Component / System / ShaderSystem / view(见 ECS.cpp)。 C++ 游戏实体仍直接用 ECS....
Definition ECS.cpp:829
void registerPostEcsHook(PostEcsHook fn)
Registers post ecs hook.
Definition ECS.cpp:865
std::function< void(ssq::Table &table)> PostEcsHook
在脚本 ECS 基类(eve.Component / eve.Entity / eve.System)注入之后执行的回调。 模块用它注入"extends eve.Entity"的脚本基类(例如 eve....
Definition ECS.h:48
std::function< void(ssq::Array &out)> CppEntityViewFn
C++ 实体 → 脚本 eve.view() 桥接。 用 registerCppEntityView(cls, fn) 登记脚本类的收集函数; 脚本 eve.view(cls) 沿类链找到登记的 C++...
Definition ECS.h:39
void exposeECSToVM(ssq::VM &vm)
在 ModuleManager::expose 之后调用;保证 eve.Component 等不被其它模块覆盖。
Definition ECS.cpp:883
void registerCppEntityView(const ssq::Class &cls, CppEntityViewFn fn)
Registers cpp entity view.
Definition ECS.cpp:861
SettlementPipeline::Stage fn