脚本更新

This commit is contained in:
Rowland 2026-06-09 15:24:38 +08:00
parent 498d202d6e
commit 84446d0060
13 changed files with 1900 additions and 47 deletions

View File

@ -133,6 +133,15 @@ void MetaCorePushGhostButtonStyle() {
}
}
[[nodiscard]] const char* MetaCoreDebugPlayModeStateName(MetaCorePlayModeState state) {
switch (state) {
case MetaCorePlayModeState::Edit: return "Edit";
case MetaCorePlayModeState::Playing: return "Playing";
case MetaCorePlayModeState::Paused: return "Paused";
default: return "Unknown";
}
}
[[nodiscard]] bool MetaCoreToolbarToggle(const char* label, bool selected, ImVec2 size = ImVec2(0.0F, 24.0F)) {
if (selected) {
MetaCorePushFlatButtonStyle(GInfernuxAccent);
@ -215,9 +224,23 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV
ImGui::BeginDisabled();
}
if (ImGui::Button(isPlaying ? "停止" : "播放", ImVec2(58.0F, 24.0F))) {
const MetaCorePlayModeState beforeState = playModeService->GetState();
std::cerr
<< "[MetaCoreDebug][Toolbar] button="
<< (isPlaying ? "Stop" : "Play")
<< " before=" << MetaCoreDebugPlayModeStateName(beforeState)
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
const bool ok = isPlaying
? playModeService->ExitPlayMode(editorContext)
: playModeService->EnterPlayMode(editorContext);
std::cerr
<< "[MetaCoreDebug][Toolbar] button="
<< (isPlaying ? "Stop" : "Play")
<< " ok=" << (ok ? "true" : "false")
<< " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState())
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
if (!ok) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "播放模式状态切换失败。");
}
@ -233,7 +256,21 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV
ImGui::BeginDisabled();
}
if (ImGui::Button(isPaused ? "继续" : "暂停", ImVec2(64.0F, 24.0F))) {
const MetaCorePlayModeState beforeState = playModeService->GetState();
std::cerr
<< "[MetaCoreDebug][Toolbar] button="
<< (isPaused ? "Resume" : "Pause")
<< " before=" << MetaCoreDebugPlayModeStateName(beforeState)
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
const bool ok = isPaused ? playModeService->Resume() : playModeService->Pause();
std::cerr
<< "[MetaCoreDebug][Toolbar] button="
<< (isPaused ? "Resume" : "Pause")
<< " ok=" << (ok ? "true" : "false")
<< " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState())
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
if (!ok) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "暂停状态切换失败。");
}
@ -249,7 +286,18 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV
ImGui::BeginDisabled();
}
if (ImGui::Button("单步", ImVec2(54.0F, 24.0F))) {
if (!playModeService->Step(editorContext)) {
std::cerr
<< "[MetaCoreDebug][Toolbar] button=Step before="
<< MetaCoreDebugPlayModeStateName(playModeService->GetState())
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
const bool ok = playModeService->Step(editorContext);
std::cerr
<< "[MetaCoreDebug][Toolbar] button=Step ok=" << (ok ? "true" : "false")
<< " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState())
<< " sceneRevision=" << editorContext.GetScene().GetRevision()
<< std::endl;
if (!ok) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "单步只在暂停状态下可用。");
}
}
@ -1136,12 +1184,9 @@ int MetaCoreEditorApp::Run() {
ImGui::Render();
// 1. 收集并更新场景数据(但不在这里渲染)
MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView();
sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId();
ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView);
// 2. 统一在一个 Frame 内渲染离屏 3D 和上屏 UI
// 视口面板在 DrawEditorFrame() 中完成本帧场景同步并把当前离屏纹理写入 ImGui。
// 这里仅提交一次 Filament 渲染,避免状态按钮切换后同一帧二次 Resize/Sync
// 让 ImGui draw data 采样到已释放的 FTexture。
ViewportRenderer_.RenderAll();
Window_.EndFrame();
@ -1422,6 +1467,23 @@ void MetaCoreEditorApp::DrawEditorFrame() {
// 7. 绘制画面 (应用 UV 翻转Filament Y轴起点在底部ImGui 在顶部)
void* texPtr = ViewportRenderer_.GetFilamentTexturePointer();
{
static void* lastDebugTexturePointer = nullptr;
static MetaCorePlayModeState lastDebugPlayModeState = MetaCorePlayModeState::Edit;
const auto playModeService = ModuleRegistry_.ResolveService<MetaCoreIPlayModeService>();
const MetaCorePlayModeState currentPlayModeState =
playModeService != nullptr ? playModeService->GetState() : MetaCorePlayModeState::Edit;
if (texPtr != lastDebugTexturePointer || currentPlayModeState != lastDebugPlayModeState) {
std::cerr
<< "[MetaCoreDebug][Viewport] ImGuiTexture pointer=" << texPtr
<< " playState=" << MetaCoreDebugPlayModeStateName(currentPlayModeState)
<< " viewport=" << viewportSize.x << "x" << viewportSize.y
<< " sceneRevision=" << Scene_.GetRevision()
<< std::endl;
lastDebugTexturePointer = texPtr;
lastDebugPlayModeState = currentPlayModeState;
}
}
if (texPtr) {
ImGui::Image(texPtr, viewportSize, ImVec2(0, 1), ImVec2(1, 0));
}

View File

@ -84,6 +84,25 @@ private:
MetaCoreNearlyEqualVec3(lhs.Scale, rhs.Scale);
}
[[nodiscard]] bool MetaCoreScriptComponentEqual(const MetaCoreScriptComponent& lhs, const MetaCoreScriptComponent& rhs) {
if (lhs.Instances.size() != rhs.Instances.size()) {
return false;
}
for (std::size_t index = 0; index < lhs.Instances.size(); ++index) {
const MetaCoreScriptInstanceData& lhsInstance = lhs.Instances[index];
const MetaCoreScriptInstanceData& rhsInstance = rhs.Instances[index];
if (lhsInstance.InstanceId != rhsInstance.InstanceId ||
lhsInstance.ScriptTypeId != rhsInstance.ScriptTypeId ||
lhsInstance.Enabled != rhsInstance.Enabled ||
lhsInstance.FieldsJson != rhsInstance.FieldsJson) {
return false;
}
}
return true;
}
[[nodiscard]] bool MetaCoreGameObjectEqual(const MetaCoreGameObjectData& lhs, const MetaCoreGameObjectData& rhs) {
if (lhs.Id != rhs.Id || lhs.ParentId != rhs.ParentId || lhs.Name != rhs.Name || !MetaCoreTransformEqual(lhs.Transform, rhs.Transform)) {
return false;
@ -134,6 +153,13 @@ private:
}
}
if (lhs.Script.has_value() != rhs.Script.has_value()) {
return false;
}
if (lhs.Script.has_value() && !MetaCoreScriptComponentEqual(*lhs.Script, *rhs.Script)) {
return false;
}
return true;
}

View File

@ -49,6 +49,38 @@ void MetaCoreIEditorService::Shutdown(MetaCoreEditorModuleRegistry& moduleRegist
(void)moduleRegistry;
}
void MetaCoreIScriptBehaviour::OnPlayStart(MetaCoreScriptExecutionContext& context) {
(void)context;
}
void MetaCoreIScriptBehaviour::FixedUpdate(
MetaCoreScriptExecutionContext& context,
double fixedDeltaSeconds
) {
(void)context;
(void)fixedDeltaSeconds;
}
void MetaCoreIScriptBehaviour::Update(
MetaCoreScriptExecutionContext& context,
double deltaSeconds
) {
(void)context;
(void)deltaSeconds;
}
void MetaCoreIScriptBehaviour::LateUpdate(
MetaCoreScriptExecutionContext& context,
double deltaSeconds
) {
(void)context;
(void)deltaSeconds;
}
void MetaCoreIScriptBehaviour::OnPlayStop(MetaCoreScriptExecutionContext& context) {
(void)context;
}
void MetaCoreIPlayModeComponentSystem::OnPlayStart(MetaCoreEditorContext& editorContext) {
(void)editorContext;
}

View File

@ -306,6 +306,57 @@ public:
[[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0;
};
enum class MetaCoreScriptFieldType {
Bool = 0,
Float,
Vec3,
String
};
struct MetaCoreScriptFieldDefinition {
std::string Name{};
std::string DisplayName{};
MetaCoreScriptFieldType Type = MetaCoreScriptFieldType::Float;
bool BoolDefault = false;
float FloatDefault = 0.0F;
glm::vec3 Vec3Default{0.0F, 0.0F, 0.0F};
std::string StringDefault{};
};
struct MetaCoreScriptExecutionContext {
MetaCoreEditorContext& EditorContext;
MetaCoreGameObject GameObject;
MetaCoreScriptInstanceData& Instance;
};
class MetaCoreIScriptBehaviour {
public:
virtual ~MetaCoreIScriptBehaviour() = default;
virtual void OnPlayStart(MetaCoreScriptExecutionContext& context);
virtual void FixedUpdate(MetaCoreScriptExecutionContext& context, double fixedDeltaSeconds);
virtual void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds);
virtual void LateUpdate(MetaCoreScriptExecutionContext& context, double deltaSeconds);
virtual void OnPlayStop(MetaCoreScriptExecutionContext& context);
};
struct MetaCoreScriptDefinition {
std::string TypeId{};
std::string DisplayName{};
std::vector<MetaCoreScriptFieldDefinition> Fields{};
std::function<std::unique_ptr<MetaCoreIScriptBehaviour>()> Factory{};
};
class MetaCoreIScriptRegistry : public MetaCoreIEditorService {
public:
virtual void RegisterScript(MetaCoreScriptDefinition definition) = 0;
[[nodiscard]] virtual const MetaCoreScriptDefinition* FindScript(std::string_view typeId) const = 0;
[[nodiscard]] virtual std::vector<MetaCoreScriptDefinition> GetScriptDefinitions() const = 0;
[[nodiscard]] virtual std::unique_ptr<MetaCoreIScriptBehaviour> CreateBehaviour(std::string_view typeId) const = 0;
[[nodiscard]] virtual std::string BuildDefaultFieldsJson(std::string_view typeId) const = 0;
[[nodiscard]] virtual std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const = 0;
};
enum class MetaCorePlayModeState {
Edit = 0,
Playing,

View File

@ -49,6 +49,7 @@
#include <unordered_set>
#include <map>
#include <string_view>
#include <sstream>
#include <vector>
#include <functional>
@ -99,6 +100,17 @@ filament::Engine::Backend MetaCoreResolveFilamentBackend() {
return filament::Engine::Backend::DEFAULT;
}
template<typename T>
[[nodiscard]] std::string MetaCoreDebugPointer(T* pointer) {
std::ostringstream stream;
stream << static_cast<const void*>(pointer);
return stream.str();
}
void MetaCoreFilamentDebugLog(const std::string& message) {
std::cerr << "[MetaCoreDebug][Filament] " << message << std::endl;
}
} // namespace
class MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridgeImpl {
@ -189,6 +201,8 @@ public:
.texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_)
.texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_)
.build(*Engine_);
++RenderTargetGeneration_;
DebugRenderTargetState("Initialize offscreen render target");
// 4. 设置到 View
View_->setRenderTarget(RenderTarget_);
@ -227,14 +241,24 @@ public:
}
if (Engine_) {
std::cout << "FilamentSceneBridge: Shutting down..." << std::endl;
DebugRenderTargetState("Shutdown begin");
// 1. 销毁所有加载的资产,切断其内部 Renderable 对克隆材质实例的占用引用
std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl;
for (auto& [id, asset] : LoadedAssets_) {
std::cout << "[DEBUG] Destroying asset for ID: " << id << ", asset pointer: " << asset << std::endl;
MetaCoreFilamentDebugLog(
"Shutdown destroyAsset begin hostRootId=" + std::to_string(id) +
" asset=" + MetaCoreDebugPointer(asset)
);
if (asset) {
Engine_->flushAndWait();
AssetLoader_->destroyAsset(asset);
}
MetaCoreFilamentDebugLog(
"Shutdown destroyAsset end hostRootId=" + std::to_string(id) +
" asset=" + MetaCoreDebugPointer(asset)
);
std::cout << "[DEBUG] Destroyed asset for ID: " << id << std::endl;
}
LoadedAssets_.clear();
@ -270,6 +294,11 @@ public:
// 3. 销毁所有克隆的材质实例,由于引用的实体都已被销毁,此时可安全释放
for (auto& [id, mats] : AssetDuplicatedMaterials_) {
MetaCoreFilamentDebugLog(
"Shutdown destroy duplicated materials hostRootId=" + std::to_string(id) +
" count=" + std::to_string(mats.size())
);
Engine_->flushAndWait();
for (auto* mat : mats) {
if (mat) {
Engine_->destroy(mat);
@ -301,19 +330,30 @@ public:
MaterialProvider_ = nullptr;
}
DestroyDeferredRenderTargetResources(true);
// 销毁离屏渲染资源
if (RenderTarget_) {
DebugRenderTargetState("Shutdown destroy current render target begin");
Engine_->flushAndWait();
Engine_->destroy(RenderTarget_);
RenderTarget_ = nullptr;
}
if (FilamentTexture_) {
MetaCoreFilamentDebugLog(
"Shutdown destroy current colorTexture=" + MetaCoreDebugPointer(FilamentTexture_)
);
Engine_->destroy(FilamentTexture_);
FilamentTexture_ = nullptr;
}
if (DepthTexture_) {
MetaCoreFilamentDebugLog(
"Shutdown destroy current depthTexture=" + MetaCoreDebugPointer(DepthTexture_)
);
Engine_->destroy(DepthTexture_);
DepthTexture_ = nullptr;
}
DebugRenderTargetState("Shutdown after current render target destroy");
if (GLTextureId_) {
glDeleteTextures(1, &GLTextureId_);
GLTextureId_ = 0;
@ -530,6 +570,18 @@ public:
if (!activeHostRootIds.contains(hostRootId) || invalidated) {
std::cout << "FilamentSceneBridge: Unloading deleted asset: " << hostRootId << std::endl;
const size_t assetEntityCount = asset != nullptr ? asset->getEntityCount() : 0;
const auto materialIt = AssetDuplicatedMaterials_.find(hostRootId);
const size_t duplicatedMaterialCount =
materialIt != AssetDuplicatedMaterials_.end() ? materialIt->second.size() : 0;
MetaCoreFilamentDebugLog(
"Unload begin hostRootId=" + std::to_string(hostRootId) +
" asset=" + MetaCoreDebugPointer(asset) +
" active=" + (activeHostRootIds.contains(hostRootId) ? std::string("true") : std::string("false")) +
" invalidated=" + (invalidated ? std::string("true") : std::string("false")) +
" entities=" + std::to_string(assetEntityCount) +
" duplicatedMaterials=" + std::to_string(duplicatedMaterialCount)
);
// 从场景中移除该资产关联的所有实体
if (Scene_ && asset) {
@ -555,9 +607,27 @@ public:
}
}
// 等待后台渲染线程消费完上一帧命令,再销毁 GLTF 资产持有的纹理/材质。
// 脚本播放会每帧推进 Transform暂停/热更新等操作可能和渲染同步落在相邻帧,
// 不等待会让 OpenGL descriptor 绑定已释放的 FTexture。
if (Engine_) {
MetaCoreFilamentDebugLog(
"Unload flush before destroyAsset hostRootId=" + std::to_string(hostRootId)
);
Engine_->flushAndWait();
}
// 销毁资产本身
if (asset) {
MetaCoreFilamentDebugLog(
"Unload destroyAsset begin hostRootId=" + std::to_string(hostRootId) +
" asset=" + MetaCoreDebugPointer(asset)
);
AssetLoader_->destroyAsset(asset);
MetaCoreFilamentDebugLog(
"Unload destroyAsset end hostRootId=" + std::to_string(hostRootId) +
" asset=" + MetaCoreDebugPointer(asset)
);
}
// 从 ObjectToFilamentEntity_ 和 ObjectWorldMatrices_ 中清除所有指向该 asset 的映射
@ -577,17 +647,32 @@ public:
// 销毁克隆的材质
auto matIt = AssetDuplicatedMaterials_.find(hostRootId);
if (matIt != AssetDuplicatedMaterials_.end()) {
MetaCoreFilamentDebugLog(
"Unload destroy duplicated materials begin hostRootId=" + std::to_string(hostRootId) +
" count=" + std::to_string(matIt->second.size())
);
if (Engine_) {
Engine_->flushAndWait();
}
for (auto* mat : matIt->second) {
if (mat) {
Engine_->destroy(mat);
}
}
MetaCoreFilamentDebugLog(
"Unload destroy duplicated materials end hostRootId=" + std::to_string(hostRootId)
);
AssetDuplicatedMaterials_.erase(matIt);
}
// 从 LoadedAssets_ 中移除
HostRootSourceModelGuids_.erase(hostRootId);
it = LoadedAssets_.erase(it);
MetaCoreFilamentDebugLog(
"Unload end hostRootId=" + std::to_string(hostRootId) +
" loadedAssets=" + std::to_string(LoadedAssets_.size()) +
" objectMappings=" + std::to_string(ObjectToFilamentEntity_.size())
);
} else {
++it;
}
@ -630,6 +715,12 @@ public:
// 加载新模型,以顶级根宿主 hostRootId 进行注册存储
std::cout << "FilamentSceneBridge: Loading GLTF: " << absolutePath << " (Host: " << hostRootName << ")" << std::endl;
MetaCoreFilamentDebugLog(
"Load begin hostRootId=" + std::to_string(hostRootId) +
" host=\"" + hostRootName + "\"" +
" modelPath=\"" + modelPath + "\"" +
" loadedAssets=" + std::to_string(LoadedAssets_.size())
);
std::ifstream file(absolutePath, std::ios::binary);
if (!file.is_open()) {
@ -648,6 +739,11 @@ public:
std::cerr << "Failed to create asset!" << std::endl;
continue;
}
MetaCoreFilamentDebugLog(
"Load createAsset hostRootId=" + std::to_string(hostRootId) +
" asset=" + MetaCoreDebugPointer(asset) +
" entities=" + std::to_string(asset->getEntityCount())
);
// 加载贴图等资源
filament::gltfio::ResourceLoader resourceLoader({ Engine_ });
@ -692,6 +788,10 @@ public:
}
}
}
MetaCoreFilamentDebugLog(
"Load duplicated materials hostRootId=" + std::to_string(hostRootId) +
" count=" + std::to_string(dupList.size())
);
}
if (scene != nullptr) {
@ -975,6 +1075,23 @@ public:
return;
}
++RenderFrameIndex_;
if (!PendingRenderTargetResources_.empty()) {
DebugRenderTargetState("RenderAll before beginFrame with pending destroy");
}
// ImGui draw data -> Filament renderable 的同步会修改 Renderable、VertexBuffer、
// MaterialInstance 参数和 descriptor 绑定。必须在 beginFrame/render pass 之前完成,
// 否则 Filament 会警告 descriptor set 在 begin/endRenderPass 中被修改,并可能在
// 后续句柄校验中触发 FTexture use-after-free/Postcondition 崩溃。
const bool hasUiPass = UIView_ != nullptr && ImGuiHelper_ != nullptr;
if (hasUiPass) {
ImGuiIO& io = ImGui::GetIO();
ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y);
UIView_->setViewport({0, 0, static_cast<uint32_t>(io.DisplaySize.x), static_cast<uint32_t>(io.DisplaySize.y)});
ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io);
}
if (Renderer_->beginFrame(SwapChain_)) {
if (RenderTarget_) {
// 1. 渲染 3D 离屏视口 (输出到 RenderTarget)
@ -985,18 +1102,11 @@ public:
Renderer_->render(View_);
if (UIView_ && ImGuiHelper_) {
if (hasUiPass) {
// 2. 准备并渲染 UI 视口 (输出到 SwapChain)
ImGuiIO& io = ImGui::GetIO();
ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y);
UIView_->setViewport({0, 0, static_cast<uint32_t>(io.DisplaySize.x), static_cast<uint32_t>(io.DisplaySize.y)});
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色
options.clear = true;
Renderer_->setClearOptions(options);
ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io);
Renderer_->render(UIView_);
}
} else {
@ -1011,11 +1121,9 @@ public:
Renderer_->endFrame();
// 在帧结束、指令提交后,安全地销毁上一帧遗留的旧纹理
for (auto* tex : OldTextures_) {
Engine_->destroy(tex);
}
OldTextures_.clear();
DestroyDeferredRenderTargetResources(true);
} else if (!PendingRenderTargetResources_.empty()) {
DebugRenderTargetState("RenderAll beginFrame failed with pending destroy");
}
}
@ -1027,6 +1135,10 @@ public:
if (width <= 0 || height <= 0) return;
if (!RenderTarget_) {
MetaCoreFilamentDebugLog(
"Resize without render target width=" + std::to_string(width) +
" height=" + std::to_string(height)
);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
return;
}
@ -1039,13 +1151,15 @@ public:
return;
}
if (RenderTarget_) Engine_->destroy(RenderTarget_);
if (DepthTexture_) Engine_->destroy(DepthTexture_);
// 【关键修复】:推迟销毁旧纹理,避免当前帧 ImGui 的 DrawList 还在引用它导致崩溃。
if (FilamentTexture_) {
OldTextures_.push_back(FilamentTexture_);
}
MetaCoreFilamentDebugLog(
"Resize recreate begin width=" + std::to_string(width) +
" height=" + std::to_string(height) +
" oldWidth=" + (FilamentTexture_ != nullptr ? std::to_string(FilamentTexture_->getWidth()) : std::string("null")) +
" oldHeight=" + (FilamentTexture_ != nullptr ? std::to_string(FilamentTexture_->getHeight()) : std::string("null"))
);
DebugRenderTargetState("Resize before queue current resources");
QueueCurrentRenderTargetResourcesForDestroy();
DestroyDeferredRenderTargetResources(true);
FilamentTexture_ = filament::Texture::Builder()
.width(static_cast<uint32_t>(width))
@ -1065,15 +1179,67 @@ public:
.texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_)
.texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_)
.build(*Engine_);
++RenderTargetGeneration_;
View_->setRenderTarget(RenderTarget_);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
DebugRenderTargetState("Resize after recreate resources");
}
void* GetFilamentTexturePointer() const {
return FilamentTexture_;
}
void QueueCurrentRenderTargetResourcesForDestroy() {
if (RenderTarget_ == nullptr && FilamentTexture_ == nullptr && DepthTexture_ == nullptr) {
return;
}
PendingRenderTargetResources_.push_back(PendingRenderTargetResource{
RenderTarget_,
FilamentTexture_,
DepthTexture_,
RenderTargetGeneration_
});
DebugRenderTargetState("Queue current render target resources for destroy");
RenderTarget_ = nullptr;
FilamentTexture_ = nullptr;
DepthTexture_ = nullptr;
}
void DestroyDeferredRenderTargetResources(bool waitForBackend) {
if (PendingRenderTargetResources_.empty() || Engine_ == nullptr) {
return;
}
if (waitForBackend) {
DebugRenderTargetState("Destroy deferred resources flush begin");
Engine_->flushAndWait();
DebugRenderTargetState("Destroy deferred resources flush end");
}
for (PendingRenderTargetResource& resource : PendingRenderTargetResources_) {
std::ostringstream stream;
stream
<< "Destroy deferred resource generation=" << resource.Generation
<< " renderTarget=" << MetaCoreDebugPointer(resource.RenderTarget)
<< " colorTexture=" << MetaCoreDebugPointer(resource.ColorTexture)
<< " depthTexture=" << MetaCoreDebugPointer(resource.DepthTexture);
MetaCoreFilamentDebugLog(stream.str());
if (resource.RenderTarget != nullptr) {
Engine_->destroy(resource.RenderTarget);
}
if (resource.ColorTexture != nullptr) {
Engine_->destroy(resource.ColorTexture);
}
if (resource.DepthTexture != nullptr) {
Engine_->destroy(resource.DepthTexture);
}
}
PendingRenderTargetResources_.clear();
DebugRenderTargetState("Destroy deferred resources end");
}
bool TryGetObjectWorldMatrix(MetaCoreId objectId, glm::mat4& worldMatrix) const {
const auto worldIt = ObjectWorldMatrices_.find(objectId);
if (worldIt == ObjectWorldMatrices_.end()) {
@ -1338,6 +1504,21 @@ private:
}
}
void DebugRenderTargetState(const char* event) const {
std::ostringstream stream;
stream
<< event
<< " frame=" << RenderFrameIndex_
<< " generation=" << RenderTargetGeneration_
<< " renderTarget=" << MetaCoreDebugPointer(RenderTarget_)
<< " colorTexture=" << MetaCoreDebugPointer(FilamentTexture_)
<< " depthTexture=" << MetaCoreDebugPointer(DepthTexture_)
<< " pending=" << PendingRenderTargetResources_.size()
<< " loadedAssets=" << LoadedAssets_.size()
<< " objectMappings=" << ObjectToFilamentEntity_.size();
MetaCoreFilamentDebugLog(stream.str());
}
filament::Engine* Engine_ = nullptr;
filament::SwapChain* SwapChain_ = nullptr;
filament::Renderer* Renderer_ = nullptr;
@ -1364,10 +1545,18 @@ private:
MetaCoreImGuiHelper* ImGuiHelper_ = nullptr;
GLuint GLTextureId_ = 0;
struct PendingRenderTargetResource {
filament::RenderTarget* RenderTarget = nullptr;
filament::Texture* ColorTexture = nullptr;
filament::Texture* DepthTexture = nullptr;
std::uint64_t Generation = 0;
};
filament::Texture* FilamentTexture_ = nullptr;
filament::Texture* DepthTexture_ = nullptr;
filament::RenderTarget* RenderTarget_ = nullptr;
std::vector<filament::Texture*> OldTextures_;
std::vector<PendingRenderTargetResource> PendingRenderTargetResources_;
std::uint64_t RenderFrameIndex_ = 0;
std::uint64_t RenderTargetGeneration_ = 0;
utils::Entity Light_;
std::filesystem::path ProjectRootPath_{};

View File

@ -90,7 +90,9 @@ MetaCoreImGuiHelper::MetaCoreImGuiHelper(Engine* engine, filament::View* view, c
void MetaCoreImGuiHelper::createAtlasTexture(Engine* engine) {
if (mTexture) {
mImGuiTextures.erase(mTexture);
engine->destroy(mTexture);
mTexture = nullptr;
}
ImGuiIO& io = ImGui::GetIO();
@ -132,25 +134,51 @@ MetaCoreImGuiHelper::~MetaCoreImGuiHelper() {
ImGui::SetCurrentContext(mImGuiContext);
ImGui::GetIO().BackendFlags &= ~ImGuiBackendFlags_RendererHasTextures;
mEngine->destroy(mScene);
mEngine->destroy(mRenderable);
auto& rcm = mEngine->getRenderableManager();
if (rcm.getInstance(mRenderable)) {
rcm.destroy(mRenderable);
}
mRenderableBuilt = false;
if (mScene) {
mScene->remove(mRenderable);
}
mEngine->destroyCameraComponent(mCameraEntity);
for (auto& mi : mMaterial2dInstances) {
mEngine->destroy(mi);
if (mi != nullptr) {
mEngine->destroy(mi);
}
}
if (mMaterial2d != nullptr) {
mEngine->destroy(mMaterial2d);
}
if (mTexture != nullptr) {
mImGuiTextures.erase(mTexture);
mEngine->destroy(mTexture);
mTexture = nullptr;
}
mEngine->destroy(mMaterial2d);
mEngine->destroy(mTexture);
for (auto& vb : mVertexBuffers) {
mEngine->destroy(vb);
if (vb != nullptr) {
mEngine->destroy(vb);
}
}
for (auto& ib : mIndexBuffers) {
mEngine->destroy(ib);
if (ib != nullptr) {
mEngine->destroy(ib);
}
}
for (auto itextures: mImGuiTextures) {
mEngine->destroy(itextures);
if (itextures != nullptr) {
mEngine->destroy(itextures);
}
}
mImGuiTextures.clear();
if (mScene != nullptr) {
mEngine->destroy(mScene);
mScene = nullptr;
}
EntityManager& em = utils::EntityManager::get();
@ -218,13 +246,25 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui
size_t nPrims = 0;
for (int cmdListIndex = 0; cmdListIndex < commands->CmdListsCount; cmdListIndex++) {
const ImDrawList* cmds = commands->CmdLists[cmdListIndex];
nPrims += cmds->CmdBuffer.size();
for (const auto& pcmd : cmds->CmdBuffer) {
if (pcmd.UserCallback == nullptr && pcmd.ElemCount > 0) {
++nPrims;
}
}
}
if (rcm.getInstance(mRenderable)) {
rcm.destroy(mRenderable);
}
mRenderableBuilt = false;
if (nPrims == 0) {
return;
}
auto rbuilder = RenderableManager::Builder(nPrims);
rbuilder.boundingBox({{ 0, 0, 0 }, { 10000, 10000, 10000 }}).culling(false);
rcm.destroy(mRenderable);
int bufferIndex = 0;
int primIndex = 0;
int material2dIndex = 0;
@ -239,6 +279,9 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui
if (pcmd.UserCallback) {
pcmd.UserCallback(cmds, &pcmd);
} else {
if (pcmd.ElemCount == 0) {
continue;
}
auto texture = (Texture const*)pcmd.GetTexID();
MaterialInstance* materialInstance;
@ -279,8 +322,9 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui
}
bufferIndex++;
}
if (commands->CmdListsCount > 0) {
if (primIndex > 0) {
rbuilder.build(*mEngine, mRenderable);
mRenderableBuilt = true;
}
}
@ -370,9 +414,19 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) {
}
if (textureData->Status == ImTextureStatus_WantCreate) {
if (mTexture != nullptr) {
mEngine->destroy(mTexture);
mTexture = nullptr;
auto* oldTexture = static_cast<Texture*>(textureData->BackendUserData);
if (oldTexture == nullptr && textureData->TexID != ImTextureID_Invalid) {
oldTexture = reinterpret_cast<Texture*>(static_cast<std::uintptr_t>(textureData->TexID));
}
if (oldTexture != nullptr) {
const bool isFontTexture = oldTexture == mTexture;
const bool isOwnedTexture = isFontTexture || mImGuiTextures.erase(oldTexture) > 0;
if (isOwnedTexture) {
mEngine->destroy(oldTexture);
}
if (isFontTexture) {
mTexture = nullptr;
}
}
Texture::PixelBufferDescriptor pb(
@ -381,7 +435,7 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) {
Texture::Format::RGBA,
Texture::Type::UBYTE);
mTexture = Texture::Builder()
Texture* createdTexture = Texture::Builder()
.width(static_cast<uint32_t>(textureData->Width))
.height(static_cast<uint32_t>(textureData->Height))
.levels(static_cast<uint8_t>(1))
@ -389,9 +443,25 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) {
.sampler(Texture::Sampler::SAMPLER_2D)
.build(*mEngine);
mTexture->setImage(*mEngine, 0, std::move(pb));
textureData->BackendUserData = mTexture;
textureData->SetTexID(static_cast<ImTextureID>(reinterpret_cast<std::uintptr_t>(mTexture)));
createdTexture->setImage(*mEngine, 0, std::move(pb));
Texture* texture = createdTexture;
#if IMGUI_VERSION_NUM >= 19200
const bool isFontAtlasTexture =
ImGui::GetIO().Fonts != nullptr &&
ImGui::GetIO().Fonts->TexData == textureData;
if (isFontAtlasTexture) {
mTexture = createdTexture;
if (mMaterial2d) {
mMaterial2d->setDefaultParameter("albedo", mTexture, mSampler);
}
} else {
mImGuiTextures.insert(createdTexture);
}
#else
mTexture = createdTexture;
#endif
textureData->BackendUserData = texture;
textureData->SetTexID(static_cast<ImTextureID>(reinterpret_cast<std::uintptr_t>(texture)));
textureData->SetStatus(ImTextureStatus_OK);
return;
}
@ -432,9 +502,16 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) {
if (textureData->Status == ImTextureStatus_WantDestroy && textureData->UnusedFrames > 0) {
auto* texture = static_cast<Texture*>(textureData->BackendUserData);
if (texture != nullptr && texture != mTexture) {
const bool isFontTexture = texture != nullptr && texture == mTexture;
const bool isOwnedTexture =
texture != nullptr &&
(isFontTexture || mImGuiTextures.erase(texture) > 0);
if (isOwnedTexture) {
mEngine->destroy(texture);
}
if (isFontTexture) {
mTexture = nullptr;
}
textureData->BackendUserData = nullptr;
textureData->SetTexID(ImTextureID_Invalid);
textureData->SetStatus(ImTextureStatus_Destroyed);

View File

@ -66,6 +66,7 @@ private:
filament::TextureSampler mSampler;
bool mFlipVertical = false;
bool mOwnsImGuiContext = false;
bool mRenderableBuilt = false;
std::filesystem::path mSettingsPath;
std::unordered_set<filament::Texture*> mImGuiTextures;
};

View File

@ -271,6 +271,9 @@ std::vector<MetaCoreId> MetaCoreScene::DuplicateGameObjects(const std::vector<Me
if (sourceObject.HasComponent<MetaCoreRotatorComponent>())
clonedObject.AddComponent<MetaCoreRotatorComponent>(sourceObject.GetComponent<MetaCoreRotatorComponent>());
if (sourceObject.HasComponent<MetaCoreScriptComponent>())
clonedObject.AddComponent<MetaCoreScriptComponent>(sourceObject.GetComponent<MetaCoreScriptComponent>());
if (sourceObject.HasComponent<MetaCorePrefabInstanceMetadata>())
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(sourceObject.GetComponent<MetaCorePrefabInstanceMetadata>());
@ -421,6 +424,8 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
data.Light = obj.GetComponent<MetaCoreLightComponent>();
if (obj.HasComponent<MetaCoreRotatorComponent>())
data.Rotator = obj.GetComponent<MetaCoreRotatorComponent>();
if (obj.HasComponent<MetaCoreScriptComponent>())
data.Script = obj.GetComponent<MetaCoreScriptComponent>();
if (obj.HasComponent<MetaCorePrefabInstanceMetadata>())
data.PrefabInstance = obj.GetComponent<MetaCorePrefabInstanceMetadata>();
if (obj.HasComponent<MetaCoreModelRootTag>())
@ -449,6 +454,8 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
obj.AddComponent<MetaCoreLightComponent>(data.Light.value());
if (data.Rotator.has_value())
obj.AddComponent<MetaCoreRotatorComponent>(data.Rotator.value());
if (data.Script.has_value())
obj.AddComponent<MetaCoreScriptComponent>(data.Script.value());
if (data.PrefabInstance.has_value())
obj.AddComponent<MetaCorePrefabInstanceMetadata>(data.PrefabInstance.value());
if (data.ModelRootTag.has_value())

View File

@ -119,6 +119,19 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
}
objJson["Rotator"] = rotatorJson;
}
} else if (objField.Name == "Script" && obj.Script.has_value()) {
json scriptJson = json::object();
json instancesJson = json::array();
for (const MetaCoreScriptInstanceData& instance : obj.Script->Instances) {
json instanceJson = json::object();
instanceJson["InstanceId"] = instance.InstanceId;
instanceJson["ScriptTypeId"] = instance.ScriptTypeId;
instanceJson["Enabled"] = instance.Enabled;
instanceJson["FieldsJson"] = instance.FieldsJson;
instancesJson.push_back(std::move(instanceJson));
}
scriptJson["Instances"] = std::move(instancesJson);
objJson["Script"] = std::move(scriptJson);
} else if (objField.Name == "Camera" && obj.Camera.has_value()) {
const MetaCoreStructDescriptor* camDesc = registry.FindStruct<MetaCoreCameraComponent>();
if (camDesc) {
@ -238,6 +251,32 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
}
obj.Rotator = rotator;
}
} else if (objField.Name == "Script" && objJson.contains("Script")) {
const auto& scriptJson = objJson["Script"];
MetaCoreScriptComponent script;
if (scriptJson.contains("Instances") && scriptJson["Instances"].is_array()) {
for (const auto& instanceJson : scriptJson["Instances"]) {
MetaCoreScriptInstanceData instance;
if (instanceJson.contains("InstanceId")) {
instance.InstanceId = instanceJson["InstanceId"].get<std::uint64_t>();
}
if (instanceJson.contains("ScriptTypeId")) {
instance.ScriptTypeId = instanceJson["ScriptTypeId"].get<std::string>();
}
if (instanceJson.contains("Enabled")) {
instance.Enabled = instanceJson["Enabled"].get<bool>();
}
if (instanceJson.contains("FieldsJson")) {
instance.FieldsJson = instanceJson["FieldsJson"].get<std::string>();
}
if (!instance.ScriptTypeId.empty()) {
script.Instances.push_back(std::move(instance));
}
}
}
if (!script.Instances.empty()) {
obj.Script = std::move(script);
}
} else if (objField.Name == "Camera" && objJson.contains("Camera")) {
const auto& camJson = objJson["Camera"];
const MetaCoreStructDescriptor* camDesc = registry.FindStruct<MetaCoreCameraComponent>();

View File

@ -142,6 +142,28 @@ struct MetaCoreRotatorComponent {
float DegreesPerSecond = 90.0F;
};
MC_STRUCT()
struct MetaCoreScriptInstanceData {
MC_GENERATED_BODY()
MC_PROPERTY()
std::uint64_t InstanceId = 0;
MC_PROPERTY()
std::string ScriptTypeId{};
MC_PROPERTY()
bool Enabled = true;
MC_PROPERTY()
std::string FieldsJson = "{}";
};
MC_STRUCT()
struct MetaCoreScriptComponent {
MC_GENERATED_BODY()
MC_PROPERTY()
std::vector<MetaCoreScriptInstanceData> Instances{};
};
MC_STRUCT()
struct MetaCoreModelRootTag {
MC_GENERATED_BODY()

View File

@ -131,6 +131,8 @@ struct MetaCoreGameObjectData {
std::optional<MetaCoreModelRootTag> ModelRootTag;
MC_PROPERTY()
std::optional<MetaCoreRotatorComponent> Rotator;
MC_PROPERTY()
std::optional<MetaCoreScriptComponent> Script;
};
} // namespace MetaCore

View File

@ -32,8 +32,10 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <thread>
#include <unordered_map>
#include <utility>
#if !defined(_WIN32)
#include <arpa/inet.h>
@ -184,6 +186,43 @@ public:
double LastLateDeltaSeconds = 0.0;
};
struct MetaCoreScriptLifecycleCounters {
int StartCount = 0;
int FixedUpdateCount = 0;
int UpdateCount = 0;
int LateUpdateCount = 0;
int StopCount = 0;
};
class MetaCoreCountingNativeScript final : public MetaCore::MetaCoreIScriptBehaviour {
public:
explicit MetaCoreCountingNativeScript(std::shared_ptr<MetaCoreScriptLifecycleCounters> counters)
: Counters_(std::move(counters)) {}
void OnPlayStart(MetaCore::MetaCoreScriptExecutionContext&) override {
++Counters_->StartCount;
}
void FixedUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->FixedUpdateCount;
}
void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->UpdateCount;
}
void LateUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override {
++Counters_->LateUpdateCount;
}
void OnPlayStop(MetaCore::MetaCoreScriptExecutionContext&) override {
++Counters_->StopCount;
}
private:
std::shared_ptr<MetaCoreScriptLifecycleCounters> Counters_{};
};
void MetaCoreTestSceneEditApi() {
MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene();
const std::size_t originalCount = scene.GetGameObjects().size();
@ -303,6 +342,7 @@ void MetaCoreTestBuiltinModuleComposition() {
editorViewsModule->Shutdown(moduleRegistry);
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestLegacyBinarySceneStartupLoadCompatibility() {
@ -573,32 +613,38 @@ void MetaCoreTestComponentRegistryDescriptors() {
const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera");
const auto* lightDescriptor = componentRegistry->FindDescriptor("Light");
const auto* rotatorDescriptor = componentRegistry->FindDescriptor("Rotator");
const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script");
const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer");
std::cout << "[CHECK] transformDescriptor: " << transformDescriptor << std::endl;
std::cout << "[CHECK] cameraDescriptor: " << cameraDescriptor << std::endl;
std::cout << "[CHECK] lightDescriptor: " << lightDescriptor << std::endl;
std::cout << "[CHECK] rotatorDescriptor: " << rotatorDescriptor << std::endl;
std::cout << "[CHECK] scriptDescriptor: " << scriptDescriptor << std::endl;
std::cout << "[CHECK] meshRendererDescriptor: " << meshRendererDescriptor << std::endl;
if (transformDescriptor) std::cout << "[CHECK] transform DrawInspector: " << static_cast<bool>(transformDescriptor->DrawInspector) << std::endl;
if (cameraDescriptor) std::cout << "[CHECK] camera DrawInspector: " << static_cast<bool>(cameraDescriptor->DrawInspector) << std::endl;
if (lightDescriptor) std::cout << "[CHECK] light DrawInspector: " << static_cast<bool>(lightDescriptor->DrawInspector) << std::endl;
if (rotatorDescriptor) std::cout << "[CHECK] rotator DrawInspector: " << static_cast<bool>(rotatorDescriptor->DrawInspector) << std::endl;
if (scriptDescriptor) std::cout << "[CHECK] script DrawInspector: " << static_cast<bool>(scriptDescriptor->DrawInspector) << std::endl;
if (meshRendererDescriptor) std::cout << "[CHECK] meshRenderer DrawInspector: " << static_cast<bool>(meshRendererDescriptor->DrawInspector) << std::endl;
MetaCoreExpect(transformDescriptor != nullptr, "Transform descriptor 应存在");
MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在");
MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在");
MetaCoreExpect(rotatorDescriptor != nullptr, "Rotator descriptor 应存在");
MetaCoreExpect(scriptDescriptor != nullptr, "Script descriptor 应存在");
MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer descriptor 应存在");
MetaCoreExpect(!transformDescriptor->DrawInspector, "Transform 应继续由独立 InspectorDrawer 处理");
MetaCoreExpect(static_cast<bool>(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(rotatorDescriptor->DrawInspector), "Rotator descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(scriptDescriptor->DrawInspector), "Script descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestPlayModeStateMachineAndLifecycle() {
@ -770,6 +816,320 @@ void MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep() {
moduleRegistry.ShutdownServices();
}
void MetaCoreTestNativeScriptRegistryAndDefaults() {
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto scriptRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIScriptRegistry>();
MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry");
const auto* rotatorDefinition = scriptRegistry->FindScript("MetaCore.Script.Rotator");
const auto* moverDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover");
MetaCoreExpect(rotatorDefinition != nullptr, "应注册内置 RotatorScript");
MetaCoreExpect(moverDefinition != nullptr, "应注册内置 MoverScript");
MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Rotator") != nullptr, "RotatorScript 应能创建运行时对象");
const nlohmann::json rotatorDefaults = nlohmann::json::parse(scriptRegistry->BuildDefaultFieldsJson("MetaCore.Script.Rotator"));
MetaCoreExpect(rotatorDefaults.contains("Enabled") && rotatorDefaults["Enabled"].get<bool>(), "RotatorScript 默认应启用");
MetaCoreExpect(rotatorDefaults.contains("DegreesPerSecond"), "RotatorScript 默认字段应包含角速度");
const nlohmann::json moverEnsured =
nlohmann::json::parse(scriptRegistry->EnsureFieldsJsonDefaults("MetaCore.Script.Mover", "{}"));
MetaCoreExpect(moverEnsured.contains("Enabled"), "MoverScript 应补齐 Enabled 默认字段");
MetaCoreExpect(moverEnsured.contains("Velocity"), "MoverScript 应补齐 Velocity 默认字段");
MetaCore::MetaCoreScriptDefinition replacement;
replacement.TypeId = "MetaCore.Script.Mover";
replacement.DisplayName = "替换移动脚本";
replacement.Fields = {
MetaCore::MetaCoreScriptFieldDefinition{
"Label",
"标签",
MetaCore::MetaCoreScriptFieldType::String,
false,
0.0F,
glm::vec3(0.0F),
"demo"
}
};
replacement.Factory = []() { return std::make_unique<MetaCoreCountingNativeScript>(std::make_shared<MetaCoreScriptLifecycleCounters>()); };
scriptRegistry->RegisterScript(std::move(replacement));
const auto* replacedDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover");
MetaCoreExpect(replacedDefinition != nullptr && replacedDefinition->DisplayName == "替换移动脚本", "重复 TypeId 注册应替换旧定义");
MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Mover") != nullptr, "替换后的脚本应能创建运行时对象");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestScriptComponentSerializationSnapshotAndJson() {
MetaCore::MetaCoreTypeRegistry registry;
MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry);
MetaCore::MetaCoreScriptComponent component;
component.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
1001,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":45.0}"
});
const auto serializedComponent = MetaCore::MetaCoreSerializeToBytes(component, registry);
MetaCoreExpect(serializedComponent.has_value(), "Script 组件应可二进制序列化");
MetaCore::MetaCoreScriptComponent roundTripComponent;
MetaCoreExpect(
MetaCore::MetaCoreDeserializeFromBytes(*serializedComponent, roundTripComponent, registry),
"Script 组件应可二进制反序列化"
);
MetaCoreExpect(roundTripComponent.Instances.size() == 1, "Script 组件 round-trip 后实例数量应保留");
MetaCoreExpect(roundTripComponent.Instances.front().ScriptTypeId == "MetaCore.Script.Rotator", "Script TypeId 应保留");
MetaCoreExpect(roundTripComponent.Instances.front().FieldsJson == component.Instances.front().FieldsJson, "Script FieldsJson 应保留");
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptObject");
object.AddComponent<MetaCore::MetaCoreScriptComponent>(component);
const MetaCore::MetaCoreId objectId = object.GetId();
const MetaCore::MetaCoreSceneSnapshot snapshot = scene.CaptureSnapshot();
MetaCoreExpect(snapshot.GameObjects.size() == 1, "快照应包含脚本对象");
MetaCoreExpect(snapshot.GameObjects.front().Script.has_value(), "快照应保存 Script 组件");
scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{});
MetaCoreExpect(!scene.FindGameObject(objectId), "恢复空快照后对象应不存在");
scene.RestoreSnapshot(snapshot);
MetaCore::MetaCoreGameObject restoredObject = scene.FindGameObject(objectId);
MetaCoreExpect(restoredObject && restoredObject.HasComponent<MetaCore::MetaCoreScriptComponent>(), "恢复快照后 Script 组件应保留");
MetaCoreExpect(
restoredObject.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.front().InstanceId == 1001,
"恢复快照后脚本 InstanceId 应保留"
);
const std::filesystem::path tempDirectory =
std::filesystem::temp_directory_path() / "MetaCoreScriptSerialization";
std::filesystem::remove_all(tempDirectory);
std::filesystem::create_directories(tempDirectory);
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "ScriptScene";
sceneDocument.GameObjects = scene.CaptureSnapshot().GameObjects;
const std::filesystem::path scenePath = tempDirectory / "ScriptScene.mcscene.json";
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry),
"包含 Script 的场景 JSON 应可保存"
);
const auto loadedSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
MetaCoreExpect(loadedSceneDocument.has_value(), "包含 Script 的场景 JSON 应可读取");
MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "读取后场景对象应保留");
MetaCoreExpect(loadedSceneDocument->GameObjects.front().Script.has_value(), "读取后 Script 组件应保留");
MetaCoreExpect(
loadedSceneDocument->GameObjects.front().Script->Instances.front().FieldsJson == component.Instances.front().FieldsJson,
"读取后 Script FieldsJson 应保留"
);
const std::filesystem::path oldScenePath = tempDirectory / "OldSceneNoScript.mcscene.json";
{
std::ofstream oldSceneFile(oldScenePath, std::ios::trunc);
oldSceneFile << "{\n"
<< " \"SceneName\": \"OldScene\",\n"
<< " \"GameObjects\": [\n"
<< " {\"Id\": 42, \"ParentId\": 0, \"Name\": \"OldObject\", \"Transform\": {"
<< "\"Position\": [0, 0, 0], \"RotationEulerDegrees\": [0, 0, 0], \"Scale\": [1, 1, 1]}}\n"
<< " ]\n"
<< "}\n";
}
const auto oldSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(oldScenePath, registry);
MetaCoreExpect(oldSceneDocument.has_value(), "旧场景缺少 Script 字段时仍应可读取");
MetaCoreExpect(!oldSceneDocument->GameObjects.front().Script.has_value(), "旧场景缺少 Script 字段时不应生成脚本组件");
std::filesystem::remove_all(tempDirectory);
}
void MetaCoreTestNativeScriptLifecycle() {
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptLifecycleObject");
auto& scriptComponent = object.AddComponent<MetaCore::MetaCoreScriptComponent>();
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
2001,
"MetaCore.Tests.CountingScript",
true,
"{}"
});
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto counters = std::make_shared<MetaCoreScriptLifecycleCounters>();
const auto scriptRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIScriptRegistry>();
MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry");
MetaCore::MetaCoreScriptDefinition countingDefinition;
countingDefinition.TypeId = "MetaCore.Tests.CountingScript";
countingDefinition.DisplayName = "计数脚本";
countingDefinition.Factory = [counters]() { return std::make_unique<MetaCoreCountingNativeScript>(counters); };
scriptRegistry->RegisterScript(std::move(countingDefinition));
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本生命周期测试应能进入播放模式");
MetaCoreExpect(counters->StartCount == 1, "脚本 OnPlayStart 应只调用一次");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(counters->FixedUpdateCount == 1, "脚本播放 Tick 应执行一次 FixedUpdate");
MetaCoreExpect(counters->UpdateCount == 1, "脚本播放 Tick 应执行一次 Update");
MetaCoreExpect(counters->LateUpdateCount == 1, "脚本播放 Tick 应执行一次 LateUpdate");
MetaCoreExpect(playMode->Pause(), "脚本生命周期测试应能暂停");
playMode->Tick(editorContext, 1.0 / 60.0);
MetaCoreExpect(counters->FixedUpdateCount == 1, "暂停时脚本 FixedUpdate 不应继续调用");
MetaCoreExpect(counters->UpdateCount == 1, "暂停时脚本 Update 不应继续调用");
MetaCoreExpect(counters->LateUpdateCount == 1, "暂停时脚本 LateUpdate 不应继续调用");
MetaCoreExpect(playMode->Step(editorContext), "暂停时脚本应能单步");
MetaCoreExpect(counters->FixedUpdateCount == 2, "脚本单步应执行一次 FixedUpdate");
MetaCoreExpect(counters->UpdateCount == 2, "脚本单步应执行一次 Update");
MetaCoreExpect(counters->LateUpdateCount == 2, "脚本单步应执行一次 LateUpdate");
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本生命周期测试应能退出播放模式");
MetaCoreExpect(counters->StopCount == 1, "脚本 OnPlayStop 应调用一次并清理运行时实例");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep() {
_putenv_s("METACORE_PROJECT_PATH", "");
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptActor");
auto& scriptComponent = object.AddComponent<MetaCore::MetaCoreScriptComponent>();
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
3001,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":60.0}"
});
scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
3002,
"MetaCore.Script.Mover",
true,
"{\"Enabled\":true,\"Velocity\":[2.0,0.0,0.0]}"
});
const MetaCore::MetaCoreId objectId = object.GetId();
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto playMode = moduleRegistry.ResolveService<MetaCore::MetaCoreIPlayModeService>();
const auto scenePersistence = moduleRegistry.ResolveService<MetaCore::MetaCoreIScenePersistenceService>();
MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService");
MetaCoreExpect(scenePersistence != nullptr, "应解析到 ScenePersistenceService");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "脚本播放前 Undo 栈应为空");
MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本行为测试应能进入播放模式");
for (int frameIndex = 0; frameIndex < 5; ++frameIndex) {
playMode->Tick(editorContext, 0.1);
}
MetaCore::MetaCoreGameObject updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"RotatorScript 播放 0.5 秒后应旋转 30 度"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F, 0.0F, 0.0F),
"MoverScript 播放 0.5 秒后应移动 1 个单位"
);
MetaCoreExpect(playMode->Pause(), "脚本行为测试应能暂停");
playMode->Tick(editorContext, 0.5);
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 30.0F, 0.0F),
"暂停期间 RotatorScript 不应继续旋转"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F, 0.0F, 0.0F),
"暂停期间 MoverScript 不应继续移动"
);
MetaCoreExpect(playMode->Step(editorContext), "暂停后脚本行为应能单步");
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 31.0F, 0.0F),
"RotatorScript 单步应按固定步长旋转"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(1.0F + (2.0F / 60.0F), 0.0F, 0.0F),
"MoverScript 单步应按固定步长移动"
);
MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本行为测试应能退出播放模式");
MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 1, "脚本播放期差异应合并为一个 Undo 步");
MetaCoreExpect(scenePersistence->IsSceneDirty(), "脚本播放结果保留后场景应标记为脏");
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpect(updatedObject.HasComponent<MetaCore::MetaCoreScriptComponent>(), "停止后 Script 组件应保留");
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 31.0F, 0.0F),
"停止后 RotatorScript 结果应保留"
);
MetaCoreExpect(editorContext.UndoCommand(), "Undo 应能回到脚本播放前快照");
updatedObject = scene.FindGameObject(objectId);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().RotationEulerDegrees,
glm::vec3(0.0F, 0.0F, 0.0F),
"Undo 后 RotatorScript 旋转应回到播放前"
);
MetaCoreExpectVec3Near(
updatedObject.GetComponent<MetaCore::MetaCoreTransformComponent>().Position,
glm::vec3(0.0F, 0.0F, 0.0F),
"Undo 后 MoverScript 位移应回到播放前"
);
MetaCoreExpect(updatedObject.HasComponent<MetaCore::MetaCoreScriptComponent>(), "Undo 后 Script 组件应保留");
MetaCoreExpect(updatedObject.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 2, "Undo 后脚本实例列表应保留");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
}
void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() {
MetaCore::MetaCoreScene scene;
@ -1855,6 +2215,13 @@ void MetaCoreTestPrefabWorkflow() {
MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot");
root.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
auto& rootScript = root.AddComponent<MetaCore::MetaCoreScriptComponent>();
rootScript.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
4101,
"MetaCore.Script.Mover",
true,
"{\"Enabled\":true,\"Velocity\":[1.0,2.0,3.0]}"
});
root.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(1.0F, 2.0F, 3.0F);
const MetaCore::MetaCoreId rootId = root.GetId();
MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId);
@ -1887,6 +2254,11 @@ void MetaCoreTestPrefabWorkflow() {
MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId);
MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCorePrefabInstanceMetadata>(), "实例根应带有 prefab 元数据");
MetaCoreExpect(instantiatedRoot.HasComponent<MetaCore::MetaCoreScriptComponent>(), "实例根应保留 Script 组件");
MetaCoreExpect(
instantiatedRoot.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 1,
"Prefab 实例应保留脚本实例"
);
instantiatedRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(8.0F, 9.0F, 10.0F);
editorContext.SelectOnly(instantiatedRoot.GetId());
@ -1895,6 +2267,11 @@ void MetaCoreTestPrefabWorkflow() {
const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析");
MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据");
MetaCoreExpect(appliedPrefabDocument->GameObjects.front().Script.has_value(), "Apply 后 prefab 资源应保留 Script 组件");
MetaCoreExpect(
appliedPrefabDocument->GameObjects.front().Script->Instances.front().ScriptTypeId == "MetaCore.Script.Mover",
"Apply 后 prefab 资源应保留脚本 TypeId"
);
MetaCoreExpectVec3Near(
appliedPrefabDocument->GameObjects.front().Transform.Position,
glm::vec3(8.0F, 9.0F, 10.0F),
@ -1905,6 +2282,7 @@ void MetaCoreTestPrefabWorkflow() {
MetaCoreExpect(secondInstanceRootId.has_value(), "应用后应仍能再次实例化 prefab");
MetaCore::MetaCoreGameObject secondInstanceRoot = scene.FindGameObject(*secondInstanceRootId);
MetaCoreExpect(secondInstanceRoot, "第二个实例应存在");
MetaCoreExpect(secondInstanceRoot.HasComponent<MetaCore::MetaCoreScriptComponent>(), "第二个实例应保留 Script 组件");
MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform");
secondInstanceRoot.GetComponent<MetaCore::MetaCoreTransformComponent>().Position = glm::vec3(-1.0F, -2.0F, -3.0F);
@ -2067,8 +2445,10 @@ void MetaCoreTestComponentRegistryOperations() {
const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera");
const auto* lightDescriptor = componentRegistry->FindDescriptor("Light");
const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script");
MetaCoreExpect(cameraDescriptor != nullptr, "应能找到 Camera 描述符");
MetaCoreExpect(lightDescriptor != nullptr, "应能找到 Light 描述符");
MetaCoreExpect(scriptDescriptor != nullptr, "应能找到 Script 描述符");
MetaCoreExpect(!cameraDescriptor->HasComponent(object), "初始时不应有 Camera");
const bool addedCamera = editorContext.ExecuteSnapshotCommand("添加 Camera", [&]() {
@ -2083,6 +2463,18 @@ void MetaCoreTestComponentRegistryOperations() {
MetaCoreExpect(addedLight, "应能通过注册表添加 Light");
MetaCoreExpect(object.HasComponent<MetaCore::MetaCoreLightComponent>(), "添加后对象应拥有 Light");
const bool addedScript = editorContext.ExecuteSnapshotCommand("添加 Script", [&]() {
return scriptDescriptor->AddComponent(object);
});
MetaCoreExpect(addedScript, "应能通过注册表添加 Script");
MetaCoreExpect(object.HasComponent<MetaCore::MetaCoreScriptComponent>(), "添加后对象应拥有 Script");
object.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.push_back(MetaCore::MetaCoreScriptInstanceData{
5101,
"MetaCore.Script.Rotator",
true,
"{\"Enabled\":true,\"DegreesPerSecond\":120.0}"
});
const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() {
return cameraDescriptor->RemoveComponent(object);
});
@ -2114,10 +2506,26 @@ void MetaCoreTestComponentRegistryOperations() {
MetaCoreExpect(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().IsPrimary, "粘贴后 IsPrimary 应一致");
MetaCoreExpect(std::abs(secondPasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致");
MetaCoreExpect(componentRegistry->CopyComponent("Script", object), "应能复制 Script 组件值");
MetaCoreExpect(componentRegistry->CanPasteComponent("Script"), "复制后应可粘贴 Script");
MetaCoreExpect(componentRegistry->PasteComponent("Script", pasteTarget), "应能粘贴 Script 组件值");
MetaCoreExpect(pasteTarget.HasComponent<MetaCore::MetaCoreScriptComponent>(), "粘贴后目标对象应拥有 Script");
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.size() == 1,
"粘贴后 Script 实例数量应一致"
);
MetaCoreExpect(
pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.front().ScriptTypeId == "MetaCore.Script.Rotator",
"粘贴后 Script TypeId 应一致"
);
MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset");
pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees = 10.0F;
MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera");
MetaCoreExpect(std::abs(pasteTarget.GetComponent<MetaCore::MetaCoreCameraComponent>().FieldOfViewDegrees - 60.0F) <= 0.0001F, "重置后 Camera 应恢复默认值");
MetaCoreExpect(scriptDescriptor->ResetComponent != nullptr, "Script descriptor 应提供 Reset");
MetaCoreExpect(scriptDescriptor->ResetComponent(pasteTarget), "应能重置 Script");
MetaCoreExpect(pasteTarget.GetComponent<MetaCore::MetaCoreScriptComponent>().Instances.empty(), "重置后 Script 实例列表应清空");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
@ -3597,6 +4005,14 @@ int main() {
MetaCoreTestPlayModeStateMachineAndLifecycle();
std::cout << "[RUN] MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep..." << std::endl;
MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep();
std::cout << "[RUN] MetaCoreTestNativeScriptRegistryAndDefaults..." << std::endl;
MetaCoreTestNativeScriptRegistryAndDefaults();
std::cout << "[RUN] MetaCoreTestScriptComponentSerializationSnapshotAndJson..." << std::endl;
MetaCoreTestScriptComponentSerializationSnapshotAndJson();
std::cout << "[RUN] MetaCoreTestNativeScriptLifecycle..." << std::endl;
MetaCoreTestNativeScriptLifecycle();
std::cout << "[RUN] MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep..." << std::endl;
MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep();
std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl;
MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot();
std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl;