Skip to content

Scene EditorΒ #43

@LyeZinho

Description

@LyeZinho

🎬 Scene Editor

Fase: 6 β€” O Olimpo
Namespace: Caffeine::Editor
Status: πŸ“… Planejado
RFs: RF6.3, RF6.4


VisΓ£o Geral

O Scene Editor Γ© o coraΓ§Γ£o do Caffeine Studio IDE. Permite criar, inspecionar e modificar entidades ECS visualmente, com suporte a drag-and-drop de assets, hierarquia de entidades, painel de inspeΓ§Γ£o de componentes e gizmos de transformaΓ§Γ£o 2D/3D.

O editor nΓ£o substitui o cΓ³digo C++ β€” ele Γ© uma ferramenta de produtividade que lΓͺ e escreve os mesmos dados ECS usados em runtime.

Componentes do editor:

Painel Responsabilidade
HierarchyPanel Árvore de todas as entidades da cena
InspectorPanel EdiΓ§Γ£o de componentes da entidade selecionada
SceneViewport Viewport com gizmos de transformaΓ§Γ£o
AssetBrowser Browser de assets do projeto com drag-and-drop
SceneEditor Orquestrador dos 4 painΓ©is acima

API Planejada

namespace Caffeine::Editor {

// ============================================================================
// @brief  Estado global de seleΓ§Γ£o no editor.
// ============================================================================
struct EditorContext {
    ECS::Entity selectedEntity   = ECS::INVALID_ENTITY;
    ECS::Entity hoveredEntity    = ECS::INVALID_ENTITY;
    bool        isDirty          = false;   // cena modificada sem salvar

    enum class GizmoMode { Translate, Rotate, Scale } gizmoMode = GizmoMode::Translate;
    enum class GizmoSpace { Local, World }             gizmoSpace = GizmoSpace::World;
};

// ============================================================================
// @brief  Painel de hierarquia β€” exibe todas as entidades da cena em Γ‘rvore.
//
//  Suporta:
//  - Hierarquia Parent/Child via componente Caffeine::Scene::Parent
//  - Renomear entidades (componente NameComponent)
//  - Criar entidade vazia (botΓ£o +)
//  - Deletar entidade selecionada (tecla Delete)
//  - Drag-and-drop para reorganizar hierarquia
// ============================================================================
class HierarchyPanel {
public:
    void render(ECS::World& world, EditorContext& ctx);

private:
    void renderEntityNode(ECS::World& world, ECS::Entity entity,
                          EditorContext& ctx);
    void renderContextMenu(ECS::World& world, ECS::Entity entity,
                           EditorContext& ctx);
};

// ============================================================================
// @brief  Painel de inspeΓ§Γ£o β€” edita componentes da entidade selecionada.
//
//  Cada tipo de componente tem um "drawer" registrado que sabe como
//  renderizar campos editΓ‘veis via ImGui.
//
//  Suporta:
//  - Adicionar/remover componentes
//  - EdiΓ§Γ£o inline de f32, Vec2, Vec3, bool, strings
//  - Reset de campos ao valor padrΓ£o
// ============================================================================
class InspectorPanel {
public:
    void render(ECS::World& world, EditorContext& ctx);

    // Registro de drawers para tipos de componentes customizados
    using ComponentDrawer = std::function<void(void* componentData)>;
    void registerDrawer(ECS::ComponentID id, ComponentDrawer drawer);

private:
    HashMap<ECS::ComponentID, ComponentDrawer> m_drawers;

    // Drawers built-in para componentes da Caffeine:
    void drawTransform(Scene::Transform& t);
    void drawSprite(Render::SpriteRenderer& s);
    void drawRigidBody2D(Physics2D::RigidBody& rb);
    void drawAudioSource(Audio::AudioSource& as);
    void drawCamera(Render::Camera2D& cam);
};

// ============================================================================
// @brief  Viewport com gizmos de transformaΓ§Γ£o.
//
//  Renderiza a cena em um framebuffer separado (offscreen) e exibe
//  dentro de uma janela ImGui. SobrepΓ΅e gizmos 2D/3D sobre o framebuffer.
//
//  Gizmos (RF6.4):
//  - Translate: arrastar eixos X/Y/Z
//  - Rotate:    girar em torno de eixo
//  - Scale:     escalar por eixo ou uniforme
//
//  Teclas de atalho (quando viewport em foco):
//  - W = Translate, E = Rotate, R = Scale
//  - Q = sem gizmo (seleΓ§Γ£o apenas)
// ============================================================================
class SceneViewport {
public:
    struct Config {
        u32   width  = 1280;
        u32   height = 720;
        bool  grid   = true;        // mostrar grade 2D
        f32   gridSpacing = 64.0f;  // pixels por cΓ©lula de grade
    };

    bool init(RHI::RenderDevice* device, Config cfg = {});
    void shutdown();

    // Renderiza cena no offscreen buffer e exibe como imagem ImGui
    void render(ECS::World& world, EditorContext& ctx,
                const Render::Camera2D& editorCamera);

    // Retorna true se o mouse estΓ‘ sobre o viewport (nΓ£o sobre painΓ©is)
    bool isHovered() const { return m_hovered; }

private:
    RHI::TextureHandle m_framebuffer;
    Config             m_config;
    bool               m_hovered = false;

    void renderGrid(RHI::CommandBuffer* cmd);
    void renderGizmos(ECS::World& world, EditorContext& ctx);
    void renderTranslateGizmo(const Scene::Transform& t, EditorContext& ctx);
    void renderRotateGizmo(const Scene::Transform& t, EditorContext& ctx);
    void renderScaleGizmo(const Scene::Transform& t, EditorContext& ctx);
    void handleGizmoInput(ECS::World& world, EditorContext& ctx);
};

// ============================================================================
// @brief  Browser de assets do projeto com drag-and-drop.
//
//  Exibe os assets em `/assets/` com thumbnails e permite:
//  - Navegar por pastas
//  - Arrastar assets para o SceneViewport β†’ cria entidade
//  - Preview de textura ao hover
//  - Renomear / deletar assets (operaΓ§Γ΅es no sistema de arquivos)
// ============================================================================
class AssetBrowser {
public:
    struct Entry {
        std::filesystem::path path;
        Assets::AssetType     type;       // Texture, Audio, Mesh, etc.
        RHI::TextureHandle    thumbnail;  // preview da textura
    };

    void init(Assets::AssetManager* assetManager, std::string_view rootPath);
    void render(EditorContext& ctx);

    // Drag payload: retorna o path do asset sendo arrastado, ou {} se nenhum.
    std::optional<std::filesystem::path> getDroppedAsset() const;

private:
    std::vector<Entry>           m_entries;
    std::filesystem::path        m_currentDir;
    Assets::AssetManager*        m_assetManager = nullptr;
    std::optional<std::filesystem::path> m_droppedAsset;

    void refreshDirectory();
    void renderEntry(const Entry& entry);
};

// ============================================================================
// @brief  Orquestrador de todos os painΓ©is do editor.
//
//  Layout padrΓ£o (docking ImGui):
//
//   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//   β”‚  [Menu Bar]  File | Edit | View | Build | Run           β”‚
//   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
//   β”‚Hierarchβ”‚                           β”‚   Inspector        β”‚
//   β”‚  Panel β”‚    Scene Viewport         β”‚   Panel            β”‚
//   β”‚        β”‚    (offscreen render)     β”‚                    β”‚
//   β”‚        β”‚    [W][E][R] gizmos       β”‚  ─ Transform ─     β”‚
//   β”‚        β”‚                           β”‚  ─ Sprite ─        β”‚
//   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
//   β”‚  Asset Browser  |  Console  |  Profiler                 β”‚
//   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// ============================================================================
class SceneEditor {
public:
    bool init(RHI::RenderDevice* device, Assets::AssetManager* assetManager,
              std::string_view assetsPath);
    void shutdown();

    // Chame no loop principal entre imgui.beginFrame() e imgui.endFrame()
    void render(ECS::World& world, Render::Camera2D& editorCamera);

    // SerializaΓ§Γ£o da cena
    bool saveScene(std::string_view path);
    bool loadScene(std::string_view path, ECS::World& world);

private:
    EditorContext  m_ctx;
    HierarchyPanel m_hierarchy;
    InspectorPanel m_inspector;
    SceneViewport  m_viewport;
    AssetBrowser   m_assetBrowser;

    bool           m_dockingSetup = false;

    void renderMenuBar(ECS::World& world);
    void setupDockingLayout();

    // Cria entidade ao soltar asset no viewport
    void handleAssetDrop(ECS::World& world, const std::filesystem::path& asset);
};

}  // namespace Caffeine::Editor

Layout Visual do Editor

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Caffeine Studio β€” Scene: "Level01.caf"               ● unsaved     β”‚
β”‚  File | Edit | View | Build | Run                                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Hierarchy  β”‚                                  β”‚  Inspector          β”‚
β”‚            β”‚         Scene Viewport            β”‚                     β”‚
β”‚ β–Έ Root     β”‚                                  β”‚  Entity: "Hero"     β”‚
β”‚   β”œβ”€ Hero  β”‚   +──────────+                  β”‚  ────────────────── β”‚
β”‚   β”œβ”€ Enemy β”‚   β”‚  Sprite  β”‚                  β”‚  β–Ύ Transform        β”‚
β”‚   └─ Floor β”‚   β”‚  (Gizmo) β”‚                  β”‚    Pos: [120, 340]  β”‚
β”‚            β”‚   β”‚   ←→     β”‚                  β”‚    Rot:  45.0Β°      β”‚
β”‚            β”‚   +──────────+                  β”‚    Scl: [1.0, 1.0]  β”‚
β”‚            β”‚                                  β”‚  β–Ύ Sprite Renderer  β”‚
β”‚            β”‚   Grid: ON    Translate [W]      β”‚    Tex: hero.caf    β”‚
β”‚            β”‚                                  β”‚  β–Ύ RigidBody2D      β”‚
β”‚            β”‚                                  β”‚    Mass: 1.0        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Assets /sprites/          β”‚  Console   β”‚  Profiler                 β”‚
β”‚  πŸ“ hero.caf  πŸ“ floor.caf β”‚  [INFO]... β”‚  Frame: 16.7ms / 60fps   β”‚
└────────────────────────────────────────────────────────────────────-β”€β”˜

IntegraΓ§Γ£o com ECS e SceneSerializer

// Salvar cena via menu File > Save:
void SceneEditor::saveScene(std::string_view path) {
    Scene::SceneSerializer serializer;
    serializer.serialize(m_world, path);    // β†’ .caf binΓ‘rio (ver fase4/scene.md)
    m_ctx.isDirty = false;
}

// Carregar cena ao abrir arquivo:
void SceneEditor::loadScene(std::string_view path, ECS::World& world) {
    world.clear();                          // destrΓ³i todas as entidades atuais
    Scene::SceneSerializer serializer;
    serializer.deserialize(world, path);    // β†’ popula o ECS
    m_ctx.selectedEntity = ECS::INVALID_ENTITY;
}

// Criar entidade ao soltar texture no viewport:
void SceneEditor::handleAssetDrop(ECS::World& world,
                                  const std::filesystem::path& asset) {
    auto entity = world.createEntity();
    world.add<Scene::NameComponent>(entity, { asset.stem().string() });
    world.add<Scene::Transform>(entity, {});    // posiΓ§Γ£o zero
    world.add<Render::SpriteRenderer>(entity, {
        .texture = m_assetManager->load<Render::Texture>(asset.string())
    });
    m_ctx.selectedEntity = entity;
    m_ctx.isDirty = true;
}

Gizmos de TransformaΓ§Γ£o (RF6.4)

Translate Gizmo (W):        Rotate Gizmo (E):         Scale Gizmo (R):
                                                        
        β–² Y                         β—―                       β–  Y
        β”‚                        β•±     β•²                    β”‚
        β”‚                       β”‚   ←   β”‚                   │──────■ X
        └────► X               β•²       β•±                    β”‚
      β•±                          β•²   β•±                    β•±
    β•± Z (3D only)                  β•²β•±                   β•± Z (3D only)

- Arrastar eixo X: move apenas em X
- Arrastar eixo Y: move apenas em Y
- Arrastar plano XY (centro): move livremente em 2D
- Shift+drag: snap de 16px (configurΓ‘vel)

Registo de Drawers Customizados

// Registar um drawer para um componente de jogo personalizado:
struct HealthComponent {
    f32 current;
    f32 maximum;
};

// No init do jogo:
editor.getInspector().registerDrawer(
    ECS::componentID<HealthComponent>(),
    [](void* data) {
        auto* health = static_cast<HealthComponent*>(data);
        ImGui::SliderFloat("HP", &health->current, 0.0f, health->maximum);
        ImGui::SliderFloat("Max HP", &health->maximum, 1.0f, 9999.0f);
    }
);

CritΓ©rio de AceitaΓ§Γ£o

  • HierarchyPanel exibe todas as entidades em Γ‘rvore com nomes
  • InspectorPanel permite editar Transform (posiΓ§Γ£o, rotaΓ§Γ£o, escala)
  • InspectorPanel permite editar SpriteRenderer (texture, cor, flip)
  • SceneViewport renderiza a cena em offscreen e exibe via ImGui::Image
  • Gizmos Translate: arrastar eixo X/Y move a entidade selecionada
  • Gizmos Rotate: girar entidade visualmente
  • Gizmos Scale: escalar entidade visualmente
  • AssetBrowser lista .caf em /assets/ com thumbnails
  • Drag-and-drop de textura do AssetBrowser cria entidade com SpriteRenderer
  • File > Save serializa a cena para .caf (round-trip com SceneSerializer)
  • File > Open deserializa a cena e popula o ECS corretamente

DependΓͺncias


ReferΓͺncias

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions