Compare commits

...

10 Commits

Author SHA1 Message Date
db0fd3a17e feat(渲染): 实现NPC和地图层的渲染优化
重构NPC渲染逻辑,将交互高亮同步移至Render方法
为NpcAnimation添加帧激活检查以避免无效纹理刷新
为GameMapLayer添加调试覆盖层画布,优化可行区域和移动区域渲染
更新测试场景地图路径和相机控制器设置
2026-04-08 00:17:03 +08:00
5af657c5c9 feat(npc): 添加NPC交互高亮效果及复合纹理支持
实现NPC交互时的绿色高亮效果,通过InteractionHighlightSprite类实现
重构NpcAnimation支持复合纹理渲染,优化高亮效果的性能
添加ShaderManager获取所有加载Shader的方法,优化渲染器uniform更新逻辑
2026-04-07 23:17:34 +08:00
caad22cca7 feat(npc): 添加NPC数据加载、动画和对象功能
实现NPC系统的核心功能,包括:
1. 新增NpcDataLoader用于加载NPC索引和配置数据
2. 添加NpcAnimation处理NPC动画显示
3. 创建NpcObject实现NPC交互和显示逻辑
4. 在GameMapTestScene中集成测试NPC功能
2026-04-07 23:02:03 +08:00
6855860d64 feat: 添加.editorconfig文件并调整调试模式设置
添加.editorconfig文件以统一代码风格配置
将GameMap.h中的debugMode_设为true并禁用movableAreaCheckEnabled_以方便调试
更新AGENTS.md文档,添加代理偏好设置说明
2026-04-07 21:45:56 +08:00
e570fec599 feat(角色): 添加角色阴影渲染功能
新增 CharacterShadowActor 类用于处理角色阴影的渲染
在 CharacterObject 中实现阴影的同步和渲染逻辑
移除 GameDebugActor 中不再使用的合成纹理预览代码
添加 EnsureCompositeTextureReady 方法确保纹理准备就绪
2026-04-07 07:08:53 +08:00
808431f92c feat(动画): 添加角色动作合成纹理功能
实现角色动作的离屏渲染合成功能,支持获取合成纹理及其相关信息:
1. 新增CanvasActor用于离屏渲染
2. 新增RenderTexture封装FBO和纹理
3. 扩展Renderer支持离屏渲染到纹理
4. 为CharacterAnimation添加合成纹理生成逻辑
5. 在调试界面添加合成纹理预览功能
2026-04-07 06:17:49 +08:00
6684abd131 docs(graphics): 添加渲染分辨率缩放模式的注释说明
为 ResolutionScaleMode 枚举添加详细的中文注释,说明每种缩放模式的具体行为
2026-04-07 00:16:02 +08:00
875af43f88 feat(渲染): 实现2D渲染风格系统
添加渲染风格预设配置,支持像素风、平滑2D和混合模式
新增纹理采样控制、顶点像素对齐和UV收缩优化
为相机和场景添加渲染风格覆盖功能
2026-04-07 00:15:48 +08:00
62b0f6dafd feat(渲染): 添加虚拟分辨率支持并重构相机系统
实现虚拟分辨率渲染系统,支持不同缩放模式
重构相机控制器以使用虚拟分辨率计算可见区域
移除硬编码的屏幕尺寸,改为动态获取
添加分辨率状态管理及相关工具函数
更新窗口和渲染器以处理分辨率变化
2026-04-06 23:17:26 +08:00
35c80247b3 feat: 添加游戏数学工具类并重构相关代码
refactor: 将数学工具函数移至GameMath类
feat(音频): 实现地图音频控制器
feat(调试): 添加游戏调试UI组件
feat(地图): 增加移动区域边界获取方法
fix(角色): 修复角色移动区域抑制逻辑
refactor(世界): 重构游戏世界场景初始化
docs(音频): 完善音频数据库注释
2026-04-06 22:22:40 +08:00
71 changed files with 4495 additions and 671 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.{c,cc,cpp,cxx,h,hh,hpp,hxx,ipp,ixx,lua}]
indent_style = space
indent_size = 2

View File

@@ -2,6 +2,11 @@
This guide helps agentic coding agents work effectively with the Frostbite2D 2D game engine codebase. This guide helps agentic coding agents work effectively with the Frostbite2D 2D game engine codebase.
## Agent Preferences
- Communicate with the user in Chinese by default unless they explicitly ask for another language.
- Treat UTF-8 as the default encoding for new or edited text and source files; do not introduce ANSI or GBK encoded files.
## Build Commands ## Build Commands
### Basic Build ### Basic Build

View File

@@ -66,6 +66,10 @@ public:
void SetScale(const Vec2& scale); void SetScale(const Vec2& scale);
void SetScale(float scale); void SetScale(float scale);
const Vec2& GetSkew() const { return skew_; }
void SetSkew(const Vec2& skew);
void SetSkew(float skewX, float skewY);
const Transform2D& GetLocalTransform() const; const Transform2D& GetLocalTransform() const;
const Transform2D& GetWorldTransform() const; const Transform2D& GetWorldTransform() const;
@@ -176,6 +180,7 @@ private:
Vec2 position_; Vec2 position_;
float rotation_; float rotation_;
Vec2 scale_ = Vec2(1.0f, 1.0f); Vec2 scale_ = Vec2(1.0f, 1.0f);
Vec2 skew_;
Vec2 size_; Vec2 size_;
Vec2 anchor_; Vec2 anchor_;
bool visible_; bool visible_;

View File

@@ -0,0 +1,64 @@
#pragma once
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/render_texture.h>
#include <functional>
namespace frostbite2D {
/// @brief 离屏画布节点。
///
/// 子节点通过 `AddCanvasChild()` 挂到内部画布树,只会在重绘到离屏纹理时参与渲染,
/// 不会像普通场景子节点那样直接显示到屏幕。
class CanvasActor : public Sprite {
public:
CanvasActor();
~CanvasActor() override = default;
bool Init(int width, int height);
bool SetCanvasSize(int width, int height);
Vec2 GetCanvasSize() const {
return Vec2(static_cast<float>(canvasWidth_), static_cast<float>(canvasHeight_));
}
void SetClearColor(const Color& color);
const Color& GetClearColor() const { return clearColor_; }
void SetDirty();
bool IsDirty() const { return dirty_; }
bool Redraw();
void SetCustomDrawCallback(std::function<void()> callback);
void ClearCustomDrawCallback();
void AddCanvasChild(RefPtr<Actor> child);
void RemoveCanvasChild(RefPtr<Actor> child);
void RemoveAllCanvasChildren();
Actor* GetCanvasRoot() const { return canvasRoot_.Get(); }
ActorList& GetCanvasChildren() { return canvasRoot_->GetChildren(); }
const ActorList& GetCanvasChildren() const { return canvasRoot_->GetChildren(); }
Ptr<Texture> GetOutputTexture() const;
Ptr<RenderTexture> GetRenderTexture() const { return renderTexture_; }
bool IsCanvasReady() const { return renderTexture_ && renderTexture_->IsValid(); }
Camera& GetCanvasCamera() { return canvasCamera_; }
const Camera& GetCanvasCamera() const { return canvasCamera_; }
void OnUpdate(float deltaTime) override;
void Render() override;
private:
bool redrawInternal();
void syncCanvasResources();
RefPtr<Actor> canvasRoot_;
Ptr<RenderTexture> renderTexture_ = nullptr;
Camera canvasCamera_;
Color clearColor_ = Colors::Transparent;
int canvasWidth_ = 0;
int canvasHeight_ = 0;
bool dirty_ = true;
std::function<void()> customDrawCallback_;
};
} // namespace frostbite2D

View File

@@ -49,11 +49,13 @@ public:
void SetOffset(const Vec2& offset); void SetOffset(const Vec2& offset);
void SetOffset(float x, float y); void SetOffset(float x, float y);
private: protected:
virtual void ConfigureShader(Shader* shader) const;
void updateTransform(); void updateTransform();
Shader* getActiveShader() const; Shader* getActiveShader() const;
Quad createQuad() const; Quad createQuad() const;
private:
Ptr<Texture> texture_; Ptr<Texture> texture_;
std::string shaderName_; std::string shaderName_;
Color color_ = Color(1.0f, 1.0f, 1.0f, 1.0f); Color color_ = Color(1.0f, 1.0f, 1.0f, 1.0f);
@@ -68,4 +70,3 @@ private:
}; };
} }

View File

@@ -44,6 +44,9 @@ public:
void InterpolationLogic(); void InterpolationLogic();
Vec2 GetMaxSize() const; Vec2 GetMaxSize() const;
bool GetStaticLocalBounds(Rect& outBounds) const;
bool GetStaticLocalBounds(int direction, Rect& outBounds) const;
uint64 GetRenderSignature() const;
bool IsUsable() const { return usable_; } bool IsUsable() const { return usable_; }
void SetUsable(bool usable) { usable_ = usable; } void SetUsable(bool usable) { usable_ = usable; }

View File

@@ -19,6 +19,8 @@ public:
static Ptr<Music> loadFromFile(const std::string& path); static Ptr<Music> loadFromFile(const std::string& path);
static Ptr<Music> loadFromMemory(const uint8* data, size_t size); static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
static Ptr<Music> loadFromNpk(const std::string& audioPath); static Ptr<Music> loadFromNpk(const std::string& audioPath);
static bool isPathPlaying(const std::string& path);
static std::string getCurrentPlayingPath();
~Music(); ~Music();

View File

@@ -2,6 +2,8 @@
#include <frostbite2D/core/window.h> #include <frostbite2D/core/window.h>
#include <frostbite2D/graphics/camera.h> #include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/render_style.h>
#include <frostbite2D/graphics/render_resolution.h>
#include <frostbite2D/types/type_alias.h> #include <frostbite2D/types/type_alias.h>
#include <frostbite2D/event/event.h> #include <frostbite2D/event/event.h>
#include <string> #include <string>
@@ -37,6 +39,32 @@ struct AppConfig {
*/ */
PlatformType targetPlatform = PlatformType::Auto; PlatformType targetPlatform = PlatformType::Auto;
/**
* @brief 是否启用虚拟分辨率
*/
bool useVirtualResolution = false;
/**
* @brief 虚拟分辨率宽度
*/
int virtualWidth = 0;
/**
* @brief 虚拟分辨率高度
*/
int virtualHeight = 0;
/**
* @brief 虚拟分辨率缩放模式
*/
ResolutionScaleMode resolutionMode = ResolutionScaleMode::Fit;
/**
* @brief 默认 2D 渲染风格预设
* 启动时作为世界/UI 渲染的基础风格Scene 可按需覆盖
*/
RenderStyleProfileId renderStyleProfile = RenderStyleProfileId::Hybrid2D;
/** /**
* @brief 创建默认配置 * @brief 创建默认配置
* @return 默认的应用配置实例 * @return 默认的应用配置实例
@@ -48,6 +76,7 @@ struct AppConfig {
config.organization = "frostbite"; config.organization = "frostbite";
config.targetPlatform = PlatformType::Auto; config.targetPlatform = PlatformType::Auto;
config.windowConfig = WindowConfig(); config.windowConfig = WindowConfig();
config.renderStyleProfile = RenderStyleProfileId::Hybrid2D;
return config; return config;
} }
}; };

View File

@@ -8,30 +8,22 @@
namespace frostbite2D { namespace frostbite2D {
/**
* \~chinese
* @brief 鼠标指针类型
*/
enum class CursorType { enum class CursorType {
Arrow, ///< 指针 Arrow,
TextInput, ///< 文本 TextInput,
Hand, ///< 手 Hand,
SizeAll, ///< 指向四个方向的箭头 SizeAll,
SizeWE, ///< 指向左右方向的箭头 SizeWE,
SizeNS, ///< 指向上下方向的箭头 SizeNS,
SizeNESW, ///< 指向左下到右上方向的箭头 SizeNESW,
SizeNWSE, ///< 指向左上到右下方向的箭头 SizeNWSE,
No, ///< 禁止 No,
}; };
/**
* \~chinese
* @brief 分辨率
*/
struct Resolution { struct Resolution {
uint32_t width = 0; ///< 分辨率宽度 uint32_t width = 0;
uint32_t height = 0; ///< 分辨率高度 uint32_t height = 0;
uint32_t refresh_rate = 0; ///< 刷新率 uint32_t refresh_rate = 0;
Resolution() = default; Resolution() = default;
@@ -39,41 +31,33 @@ struct Resolution {
: width(width), height(height), refresh_rate(refresh_rate) {} : width(width), height(height), refresh_rate(refresh_rate) {}
}; };
/**
* \~chinese
* @brief 图标
*/
struct Icon { struct Icon {
Icon() = default; Icon() = default;
Icon(std::string file_path) : file_path(file_path) {} Icon(std::string file_path) : file_path(file_path) {}
std::string file_path; ///< 文件路径 std::string file_path;
#if defined(_WIN32) #if defined(_WIN32)
uint32_t resource_id = 0; ///< 资源ID仅在windows上生效 uint32_t resource_id = 0;
Icon(uint32_t resource_id) : resource_id(resource_id) {} Icon(uint32_t resource_id) : resource_id(resource_id) {}
#endif #endif
}; };
/**
* \~chinese
* @brief 窗口设置
*/
struct WindowConfig { struct WindowConfig {
uint32_t width = 640; ///< 窗口宽度 uint32_t width = 640;
uint32_t height = 480; ///< 窗口高度 uint32_t height = 480;
std::string title = "frostbite2D Game"; ///< 窗口标题 std::string title = "frostbite2D Game";
Icon icon; ///< 窗口图标 Icon icon;
bool resizable = false; ///< 窗口大小可调整 bool resizable = false;
bool fullscreen = false; ///< 窗口全屏 bool fullscreen = false;
bool borderless = false; ///< 无边框窗口 bool borderless = false;
bool decorated = true; ///< 窗口装饰 bool decorated = true;
int multisamples = 0; ///< 多重采样数 int multisamples = 0;
bool centered = true; ///< 窗口是否居中 bool centered = true;
bool vsync = true; ///< 是否启用垂直同步 bool vsync = true;
bool showCursor = true; ///< 是否显示光标 bool showCursor = true;
}; };
class Window { class Window {
@@ -81,171 +65,45 @@ public:
Window() = default; Window() = default;
~Window() = default; ~Window() = default;
/**
* @brief 创建窗口
* @param cfg 窗口配置
* @return 创建是否成功
*/
virtual bool create(const WindowConfig& cfg); virtual bool create(const WindowConfig& cfg);
/**
* @brief 销毁窗口
*/
virtual void destroy(); virtual void destroy();
/**
* @brief 轮询事件
*/
virtual void poll(); virtual void poll();
/**
* @brief 交换缓冲区
*/
virtual void swap(); virtual void swap();
/**
* @brief 设置窗口关闭标志
*/
virtual void close(); virtual void close();
/**
* @brief 设置窗口标题
*/
virtual void setTitle(const std::string& title); virtual void setTitle(const std::string& title);
/**
* @brief 设置窗口大小
*/
virtual void setSize(int w, int h); virtual void setSize(int w, int h);
/**
* @brief 设置窗口位置
*/
virtual void setPos(int x, int y); virtual void setPos(int x, int y);
/**
* @brief 设置全屏模式
*/
virtual void setFullscreen(bool fs); virtual void setFullscreen(bool fs);
/**
* @brief 设置垂直同步
*/
virtual void setVSync(bool vsync); virtual void setVSync(bool vsync);
/**
* @brief 设置窗口可见性
*/
virtual void setVisible(bool visible); virtual void setVisible(bool visible);
/**
* @brief 获取窗口宽度
*/
virtual int width() const; virtual int width() const;
/**
* @brief 获取窗口高度
*/
virtual int height() const; virtual int height() const;
virtual int drawableWidth() const;
/** virtual int drawableHeight() const;
* @brief 获取窗口大小
*/
virtual Size size() const; virtual Size size() const;
/**
* @brief 获取窗口位置
*/
virtual Vec2 pos() const; virtual Vec2 pos() const;
/**
* @brief 是否全屏
*/
virtual bool fullscreen() const; virtual bool fullscreen() const;
/**
* @brief 是否启用垂直同步
*/
virtual bool vsync() const; virtual bool vsync() const;
/**
* @brief 窗口是否获得焦点
*/
virtual bool focused() const; virtual bool focused() const;
/**
* @brief 窗口是否最小化
*/
virtual bool minimized() const; virtual bool minimized() const;
/**
* @brief 获取内容缩放X
*/
virtual float scaleX() const; virtual float scaleX() const;
/**
* @brief 获取内容缩放Y
*/
virtual float scaleY() const; virtual float scaleY() const;
virtual bool refreshMetrics(bool emitResize = false);
/**
* @brief 设置光标形状
*/
virtual void setCursor(CursorType cursor); virtual void setCursor(CursorType cursor);
/**
* @brief 显示/隐藏光标
*/
virtual void showCursor(bool show); virtual void showCursor(bool show);
/**
* @brief 锁定/解锁光标
*/
virtual void lockCursor(bool lock); virtual void lockCursor(bool lock);
/**
* @brief 窗口大小改变回调
*/
using ResizeCb = std::function<void(int, int)>; using ResizeCb = std::function<void(int, int)>;
/**
* @brief 窗口关闭回调
*/
using CloseCb = std::function<void()>; using CloseCb = std::function<void()>;
/**
* @brief 窗口焦点改变回调
*/
using FocusCb = std::function<void(bool)>; using FocusCb = std::function<void(bool)>;
/**
* @brief 设置大小改变回调
*/
virtual void onResize(ResizeCb cb); virtual void onResize(ResizeCb cb);
/**
* @brief 设置关闭回调
*/
virtual void onClose(CloseCb cb); virtual void onClose(CloseCb cb);
/**
* @brief 设置焦点改变回调
*/
virtual void onFocus(FocusCb cb); virtual void onFocus(FocusCb cb);
/**
* @brief 获取原生窗口句柄
*/
virtual void* native() const; virtual void* native() const;
/**
* @brief 获取 SDL 窗口句柄
*/
SDL_Window* sdlWindow() const { return sdlWindow_; } SDL_Window* sdlWindow() const { return sdlWindow_; }
/**
* @brief 获取 OpenGL 上下文
*/
SDL_GLContext glContext() const { return glContext_; } SDL_GLContext glContext() const { return glContext_; }
private: private:
@@ -254,6 +112,8 @@ private:
int width_ = 1280; int width_ = 1280;
int height_ = 720; int height_ = 720;
int drawableWidth_ = 1280;
int drawableHeight_ = 720;
bool fullscreen_ = false; bool fullscreen_ = false;
bool vsync_ = true; bool vsync_ = true;
bool focused_ = true; bool focused_ = true;

View File

@@ -11,12 +11,18 @@ public:
void setPosition(const Vec2& pos); void setPosition(const Vec2& pos);
void setZoom(float zoom); void setZoom(float zoom);
void setViewport(int width, int height); void setViewport(int width, int height);
void setFlipY(bool flip) { flipY_ = flip; } // 调试用Y轴翻转开关 void setFlipY(bool flip) { flipY_ = flip; } // Debug Y-axis flip.
void setPixelSnapEnabled(bool enabled) { pixelSnapEnabled_ = enabled; }
const Vec2& getPosition() const { return position_; } const Vec2& getPosition() const { return position_; }
Vec2 getRenderPosition() const;
Vec2 snapWorldPosition(const Vec2& position) const;
float getZoom() const { return zoom_; } float getZoom() const { return zoom_; }
int getViewportWidth() const { return viewportWidth_; } int getViewportWidth() const { return viewportWidth_; }
int getViewportHeight() const { return viewportHeight_; } int getViewportHeight() const { return viewportHeight_; }
float getVisibleWidth() const;
float getVisibleHeight() const;
bool isPixelSnapEnabled() const { return pixelSnapEnabled_; }
void lookAt(const Vec2& target); void lookAt(const Vec2& target);
void move(const Vec2& delta); void move(const Vec2& delta);
@@ -30,7 +36,8 @@ public:
float zoom_ = 1.0f; float zoom_ = 1.0f;
int viewportWidth_ = 1280; int viewportWidth_ = 1280;
int viewportHeight_ = 720; int viewportHeight_ = 720;
bool flipY_ = false; // 调试用Y轴翻转开关 bool flipY_ = false; // Debug Y-axis flip.
bool pixelSnapEnabled_ = false;
}; };
} } // namespace frostbite2D

View File

@@ -0,0 +1,46 @@
#pragma once
#include <frostbite2D/types/type_math.h>
namespace frostbite2D {
/**
* @brief 虚拟分辨率缩放模式
*/
enum class ResolutionScaleMode {
Disabled, ///< 不保持纵横比,直接按窗口宽高分别缩放
Fit, ///< 保持纵横比并完整显示,按较小缩放因子适配
FitHeight, ///< 以高度优先适配,宽度不足时回退为 Fit
};
struct RenderResolutionState {
int windowWidth = 1280;
int windowHeight = 720;
int drawableWidth = 1280;
int drawableHeight = 720;
int windowViewportX = 0;
int windowViewportY = 0;
int windowViewportWidth = 1280;
int windowViewportHeight = 720;
int viewportX = 0;
int viewportY = 0;
int viewportWidth = 1280;
int viewportHeight = 720;
float contentScaleX = 1.0f;
float contentScaleY = 1.0f;
float scaleX = 1.0f;
float scaleY = 1.0f;
Rect getWindowViewportRect() const {
return Rect(static_cast<float>(windowViewportX),
static_cast<float>(windowViewportY),
static_cast<float>(windowViewportWidth),
static_cast<float>(windowViewportHeight));
}
};
} // namespace frostbite2D

View File

@@ -0,0 +1,72 @@
#pragma once
namespace frostbite2D {
/**
* @brief 2D 渲染风格预设 ID
*/
enum class RenderStyleProfileId {
PixelArt2D, ///< 世界与 UI 都按像素风策略渲染
Smooth2D, ///< 世界与 UI 都按平滑 2D 策略渲染
Hybrid2D ///< 世界走像素风UI 走平滑策略
};
/**
* @brief 渲染风格作用层级
*/
enum class RenderStyleLayerRole {
World, ///< 世界层,如地图、角色、场景特效
UI ///< UI 层,如 HUD、菜单、文字
};
/**
* @brief 单个层级的渲染风格开关集合
*/
struct RenderStyleLayerSettings {
bool cameraPixelSnap = false; ///< 是否让相机位置吸附到像素网格
bool vertexPixelSnap = false; ///< 是否让世界顶点在提交前做像素对齐
bool pixelArtSampling = false; ///< 是否优先使用像素风采样(如 GL_NEAREST
bool shrinkSubTextureUVs = false; ///< 是否对子纹理 UV 做向内收缩,减轻图集边缘闪烁
};
/**
* @brief 一套完整的 2D 渲染风格设置
*/
struct RenderStyleSettings {
RenderStyleLayerSettings world; ///< 世界层使用的渲染风格
RenderStyleLayerSettings ui; ///< UI 层使用的渲染风格
/**
* @brief 根据预设 ID 生成对应的渲染风格
*/
static RenderStyleSettings FromProfile(RenderStyleProfileId profile) {
RenderStyleSettings settings;
switch (profile) {
case RenderStyleProfileId::PixelArt2D:
settings.world = {true, true, true, true};
settings.ui = {true, true, true, true};
break;
case RenderStyleProfileId::Smooth2D:
settings.world = {false, false, false, false};
settings.ui = {false, false, false, false};
break;
case RenderStyleProfileId::Hybrid2D:
default:
settings.world = {true, true, true, true};
settings.ui = {false, false, false, false};
break;
}
return settings;
}
/**
* @brief 获取指定层级对应的风格设置
*/
const RenderStyleLayerSettings& layer(RenderStyleLayerRole role) const {
return role == RenderStyleLayerRole::UI ? ui : world;
}
};
} // namespace frostbite2D

View File

@@ -0,0 +1,34 @@
#pragma once
#include <frostbite2D/base/RefObject.h>
#include <frostbite2D/graphics/texture.h>
namespace frostbite2D {
class Renderer;
/// @brief 2D 离屏渲染目标,封装颜色纹理和对应的 framebuffer。
class RenderTexture : public RefObject {
public:
RenderTexture() = default;
~RenderTexture() override;
bool Init(int width, int height);
bool Resize(int width, int height);
void Reset();
bool IsValid() const { return framebufferID_ != 0 && texture_ != nullptr; }
int GetWidth() const { return width_; }
int GetHeight() const { return height_; }
Ptr<Texture> GetTexture() const { return texture_; }
private:
Ptr<Texture> texture_ = nullptr;
uint32 framebufferID_ = 0;
int width_ = 0;
int height_ = 0;
friend class Renderer;
};
} // namespace frostbite2D

View File

@@ -1,15 +1,20 @@
#pragma once #pragma once
#include <frostbite2D/graphics/types.h>
#include <frostbite2D/graphics/texture.h>
#include <frostbite2D/graphics/shader.h>
#include <frostbite2D/graphics/shader_manager.h>
#include <frostbite2D/graphics/batch.h> #include <frostbite2D/graphics/batch.h>
#include <frostbite2D/graphics/camera.h> #include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/render_resolution.h>
#include <frostbite2D/graphics/render_style.h>
#include <frostbite2D/graphics/shader.h>
#include <frostbite2D/graphics/shader_manager.h>
#include <frostbite2D/graphics/texture.h>
#include <frostbite2D/graphics/types.h>
#include <string> #include <string>
#include <vector>
namespace frostbite2D { namespace frostbite2D {
class RenderTexture;
class Renderer { class Renderer {
public: public:
static Renderer& get(); static Renderer& get();
@@ -20,48 +25,130 @@ public:
void beginFrame(); void beginFrame();
void endFrame(); void endFrame();
void flush(); void flush();
bool beginRenderToTexture(RenderTexture& target, Camera* camera,
const Color& clearColor = Color(0.0f, 0.0f, 0.0f, 0.0f),
bool clear = true);
bool endRenderToTexture();
void setViewport(int x, int y, int width, int height); void setViewport(int x, int y, int width, int height);
void setWindowSize(int width, int height, float contentScaleX = 1.0f,
float contentScaleY = 1.0f);
void setVirtualResolutionEnabled(bool enabled);
void setVirtualResolution(int width, int height);
void setResolutionScaleMode(ResolutionScaleMode mode);
void setClearColor(float r, float g, float b, float a = 1.0f); void setClearColor(float r, float g, float b, float a = 1.0f);
void setClearColor(const Color& color); void setClearColor(const Color& color);
void clear(uint32_t flags); void clear(uint32_t flags);
/**
* @brief 设置应用默认渲染风格预设
*/
void setDefaultRenderStyleProfile(RenderStyleProfileId profile);
/**
* @brief 获取应用默认渲染风格预设
*/
RenderStyleProfileId getDefaultRenderStyleProfile() const {
return defaultRenderStyleProfile_;
}
/**
* @brief 切换当前正在使用的渲染风格上下文
*/
void setActiveRenderStyleProfile(RenderStyleProfileId profile,
RenderStyleLayerRole role);
/**
* @brief 将指定风格作用到某个相机
*/
void applyRenderStyleToCamera(Camera* camera, RenderStyleProfileId profile,
RenderStyleLayerRole role) const;
void setCamera(Camera* camera); void setCamera(Camera* camera);
Camera* getCamera() { return camera_; } Camera* getCamera() { return camera_; }
int getVirtualWidth() const;
int getVirtualHeight() const;
Rect getViewportRect() const;
const RenderResolutionState& getResolutionState() const {
return resolutionState_;
}
bool isInitialized() const { return initialized_; }
bool isFrameActive() const { return frameActive_; }
bool isRenderingToTexture() const { return !renderTargetStack_.empty(); }
Vec2 screenToVirtual(const Vec2& screenPos) const;
Vec2 virtualToScreen(const Vec2& virtualPos) const;
void drawQuad(const Vec2& pos, const Size& size, float cr = 1.0f, float cg = 1.0f, void drawQuad(const Vec2& pos, const Size& size, float cr = 1.0f,
float cb = 1.0f, float ca = 1.0f); float cg = 1.0f, float cb = 1.0f, float ca = 1.0f);
void drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture); void drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture);
void drawQuad(const Rect& rect, const Color& color); void drawQuad(const Rect& rect, const Color& color);
void drawSprite(const Vec2& pos, const Size& size, Ptr<Texture> texture); void drawSprite(const Vec2& pos, const Size& size, Ptr<Texture> texture);
void drawSprite(const Vec2& pos, const Rect& srcRect, const Vec2& texSize, void drawSprite(const Vec2& pos, const Rect& srcRect, const Vec2& texSize,
Ptr<Texture> texture, const Color& color = Color(1, 1, 1, 1)); Ptr<Texture> texture,
const Color& color = Color(1, 1, 1, 1));
void setupBlendMode(BlendMode mode); void setupBlendMode(BlendMode mode);
/**
* @brief 获取当前激活的层级风格设置
*/
const RenderStyleLayerSettings& getActiveRenderStyle() const {
return activeRenderStyle_;
}
/**
* @brief 当前风格是否要求顶点像素对齐
*/
bool shouldSnapVertices() const { return activeRenderStyle_.vertexPixelSnap; }
/**
* @brief 当前风格是否优先使用像素风采样
*/
bool shouldUsePixelArtSampling() const {
return activeRenderStyle_.pixelArtSampling;
}
/**
* @brief 当前风格是否需要对子纹理 UV 做向内收缩
*/
bool shouldShrinkSubTextureUVs() const {
return activeRenderStyle_.shrinkSubTextureUVs;
}
ShaderManager& getShaderManager() { return shaderManager_; } ShaderManager& getShaderManager() { return shaderManager_; }
Batch& getBatch() { return batch_; } Batch& getBatch() { return batch_; }
private: private:
Renderer(); Renderer();
// ~Renderer() 在 shutdown() 中手动调用销毁
void updateUniforms(); void updateUniforms();
void recalculateResolutionState();
void syncCameraViewport();
struct RenderTargetState {
uint32 framebuffer = 0;
int viewportX = 0;
int viewportY = 0;
int viewportWidth = 1;
int viewportHeight = 1;
float clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Camera* camera = nullptr;
bool startedStandaloneBatch = false;
};
ShaderManager shaderManager_; ShaderManager shaderManager_;
Batch batch_; Batch batch_;
Camera* camera_ = nullptr; Camera* camera_ = nullptr;
RenderStyleProfileId defaultRenderStyleProfile_ = RenderStyleProfileId::Hybrid2D;
RenderStyleProfileId activeRenderStyleProfile_ = RenderStyleProfileId::Hybrid2D;
RenderStyleLayerRole activeRenderStyleRole_ = RenderStyleLayerRole::World;
RenderStyleLayerSettings activeRenderStyle_;
uint32_t clearColor_[4] = {0, 0, 0, 255}; uint32_t clearColor_[4] = {0, 0, 0, 255};
int viewportX_ = 0; bool useVirtualResolution_ = false;
int viewportY_ = 0; int virtualWidth_ = 1280;
int viewportWidth_ = 1280; int virtualHeight_ = 720;
int viewportHeight_ = 720; ResolutionScaleMode resolutionMode_ = ResolutionScaleMode::Fit;
RenderResolutionState resolutionState_;
bool initialized_ = false; bool initialized_ = false;
bool frameActive_ = false;
std::vector<RenderTargetState> renderTargetStack_;
Renderer(const Renderer&) = delete; Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete;
}; };
} } // namespace frostbite2D

View File

@@ -18,6 +18,9 @@ public:
Shader* getShader(const std::string& name); Shader* getShader(const std::string& name);
bool hasShader(const std::string& name) const; bool hasShader(const std::string& name) const;
const std::unordered_map<std::string, Ptr<Shader>>& getLoadedShaders() const {
return shaders_;
}
~ShaderManager() = default; ~ShaderManager() = default;

View File

@@ -8,6 +8,11 @@
namespace frostbite2D { namespace frostbite2D {
enum class TextureSampling {
Linear,
PixelArt
};
class Texture : public RefObject { class Texture : public RefObject {
public: public:
static Ptr<Texture> loadFromFile(const std::string& path); static Ptr<Texture> loadFromFile(const std::string& path);
@@ -16,11 +21,13 @@ public:
~Texture(); ~Texture();
void bind(uint32_t slot = 0); void bind(uint32_t slot = 0, bool preferPixelArtSampling = false);
void unbind(); void unbind();
void setWrapMode(uint32_t wrapS, uint32_t wrapT); void setWrapMode(uint32_t wrapS, uint32_t wrapT);
void setFilterMode(uint32_t minFilter, uint32_t magFilter); void setFilterMode(uint32_t minFilter, uint32_t magFilter);
void setSampling(TextureSampling sampling);
TextureSampling getSampling() const { return sampling_; }
int getWidth() const { return width_; } int getWidth() const { return width_; }
int getHeight() const { return height_; } int getHeight() const { return height_; }
@@ -29,12 +36,17 @@ public:
private: private:
Texture(int width, int height, uint32_t id); Texture(int width, int height, uint32_t id);
void applyFilter(uint32_t minFilter, uint32_t magFilter);
void applySampling(bool preferPixelArtSampling);
uint32_t textureID_ = 0; uint32_t textureID_ = 0;
int width_ = 0; int width_ = 0;
int height_ = 0; int height_ = 0;
int channels_ = 0; int channels_ = 0;
std::string path_; std::string path_;
TextureSampling sampling_ = TextureSampling::Linear;
uint32_t appliedMinFilter_ = 0;
uint32_t appliedMagFilter_ = 0;
Texture() = default; Texture() = default;
Texture(const Texture&) = delete; Texture(const Texture&) = delete;

View File

@@ -57,7 +57,8 @@ struct Quad {
static Quad createTextured(const Rect& destRect, const Rect& srcRect, static Quad createTextured(const Rect& destRect, const Rect& srcRect,
const Vec2& texSize, float cr = 1.0f, const Vec2& texSize, float cr = 1.0f,
float cg = 1.0f, float cb = 1.0f, float ca = 1.0f) { float cg = 1.0f, float cb = 1.0f, float ca = 1.0f,
bool shrinkSubTextureUVs = false) {
Quad q; Quad q;
Vec2 bl(destRect.left(), destRect.bottom()); Vec2 bl(destRect.left(), destRect.bottom());
@@ -65,10 +66,38 @@ struct Quad {
Vec2 tl(destRect.left(), destRect.top()); Vec2 tl(destRect.left(), destRect.top());
Vec2 tr(destRect.right(), destRect.top()); Vec2 tr(destRect.right(), destRect.top());
Vec2 uvBL(srcRect.left() / texSize.x, srcRect.bottom() / texSize.y); auto resolveUvX = [&srcRect, &texSize, shrinkSubTextureUVs](bool useRightEdge) {
Vec2 uvBR(srcRect.right() / texSize.x, srcRect.bottom() / texSize.y); float pixelInset = 0.0f;
Vec2 uvTL(srcRect.left() / texSize.x, srcRect.top() / texSize.y); if (!shrinkSubTextureUVs) {
Vec2 uvTR(srcRect.right() / texSize.x, srcRect.top() / texSize.y); return (useRightEdge ? srcRect.right() : srcRect.left()) / texSize.x;
}
if (useRightEdge && srcRect.right() < texSize.x) {
pixelInset = -0.5f;
} else if (!useRightEdge && srcRect.left() > 0.0f) {
pixelInset = 0.5f;
}
return (useRightEdge ? srcRect.right() : srcRect.left()) / texSize.x +
pixelInset / texSize.x;
};
auto resolveUvY = [&srcRect, &texSize, shrinkSubTextureUVs](bool useBottomEdge) {
float pixelInset = 0.0f;
if (!shrinkSubTextureUVs) {
return (useBottomEdge ? srcRect.bottom() : srcRect.top()) / texSize.y;
}
if (useBottomEdge && srcRect.bottom() < texSize.y) {
pixelInset = -0.5f;
} else if (!useBottomEdge && srcRect.top() > 0.0f) {
pixelInset = 0.5f;
}
return (useBottomEdge ? srcRect.bottom() : srcRect.top()) / texSize.y +
pixelInset / texSize.y;
};
Vec2 uvBL(resolveUvX(false), resolveUvY(true));
Vec2 uvBR(resolveUvX(true), resolveUvY(true));
Vec2 uvTL(resolveUvX(false), resolveUvY(false));
Vec2 uvTR(resolveUvX(true), resolveUvY(false));
q.vertices[0] = Vertex(bl, uvBL, cr, cg, cb, ca); q.vertices[0] = Vertex(bl, uvBL, cr, cg, cb, ca);
q.vertices[1] = Vertex(br, uvBR, cr, cg, cb, ca); q.vertices[1] = Vertex(br, uvBR, cr, cg, cb, ca);

View File

@@ -13,10 +13,11 @@ namespace frostbite2D {
* @brief 音频项类型枚举 * @brief 音频项类型枚举
*/ */
enum class AudioEntryType { enum class AudioEntryType {
None, ///< 无效 None, ///< invalid
Effect, ///< 音效 Effect, ///< one-shot sound effect
Music, ///< 背景音乐 Ambient, ///< looping ambient track
Random ///< 随机音效组 Music, ///< background music
Random ///< random sound group
}; };
/** /**
@@ -39,6 +40,16 @@ struct AudioMusic {
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置) int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
}; };
/**
* @brief ?????????
*/
struct AudioAmbient {
std::string id; ///< ?????ID
std::string file; ///< ??????
int32 loopDelay = -1; ///< ????????1 ?????????
int32 loopDelayRange = -1;///< ???????????1 ?????????
};
/** /**
* @brief 随机音效项 * @brief 随机音效项
*/ */
@@ -62,6 +73,7 @@ struct AudioEntry {
AudioEntryType type = AudioEntryType::None; ///< 条目类型 AudioEntryType type = AudioEntryType::None; ///< 条目类型
const AudioEffect* effect = nullptr; ///< 音效指针type == Effect 时有效) const AudioEffect* effect = nullptr; ///< 音效指针type == Effect 时有效)
const AudioAmbient* ambient = nullptr;///< ???????????ype == Ambient ??????
const AudioMusic* music = nullptr; ///< 音乐指针type == Music 时有效) const AudioMusic* music = nullptr; ///< 音乐指针type == Music 时有效)
const AudioRandom* random = nullptr; ///< 随机组指针type == Random 时有效) const AudioRandom* random = nullptr; ///< 随机组指针type == Random 时有效)
@@ -71,6 +83,9 @@ struct AudioEntry {
/// @brief 判断是否为音效 /// @brief 判断是否为音效
bool isEffect() const { return type == AudioEntryType::Effect; } bool isEffect() const { return type == AudioEntryType::Effect; }
/// @brief ????????????
bool isAmbient() const { return type == AudioEntryType::Ambient; }
/// @brief 判断是否为音乐 /// @brief 判断是否为音乐
bool isMusic() const { return type == AudioEntryType::Music; } bool isMusic() const { return type == AudioEntryType::Music; }
@@ -81,6 +96,8 @@ struct AudioEntry {
std::string getFilePath() const { std::string getFilePath() const {
if (type == AudioEntryType::Effect && effect) if (type == AudioEntryType::Effect && effect)
return effect->file; return effect->file;
if (type == AudioEntryType::Ambient && ambient)
return ambient->file;
if (type == AudioEntryType::Music && music) if (type == AudioEntryType::Music && music)
return music->file; return music->file;
return ""; return "";
@@ -203,6 +220,13 @@ public:
*/ */
std::optional<const AudioEffect*> getEffect(const std::string& id); std::optional<const AudioEffect*> getEffect(const std::string& id);
/**
* @brief ????????????
* @param id ?????ID
* @return ????????????????????? std::nullopt
*/
std::optional<const AudioAmbient*> getAmbient(const std::string& id);
/** /**
* @brief 获取音乐配置 * @brief 获取音乐配置
* @param id 音乐 ID * @param id 音乐 ID
@@ -226,6 +250,11 @@ public:
*/ */
size_t effectCount() const; size_t effectCount() const;
/**
* @brief ???????????
*/
size_t ambientCount() const;
/** /**
* @brief 获取音乐数量 * @brief 获取音乐数量
*/ */
@@ -312,6 +341,7 @@ private:
std::string selectFromRandom(const AudioRandom& randomGroup); std::string selectFromRandom(const AudioRandom& randomGroup);
std::unordered_map<std::string, AudioEffect> effectMap_; ///< 音效映射 std::unordered_map<std::string, AudioEffect> effectMap_; ///< 音效映射
std::unordered_map<std::string, AudioAmbient> ambientMap_; ///< ????????
std::unordered_map<std::string, AudioMusic> musicMap_; ///< 音乐映射 std::unordered_map<std::string, AudioMusic> musicMap_; ///< 音乐映射
std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射 std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射

View File

@@ -2,6 +2,7 @@
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
#include <frostbite2D/event/event.h> #include <frostbite2D/event/event.h>
#include <frostbite2D/graphics/render_style.h>
#include <vector> #include <vector>
namespace frostbite2D { namespace frostbite2D {
@@ -18,6 +19,20 @@ public:
void Render() override; void Render() override;
bool OnEvent(const Event& event) override; bool OnEvent(const Event& event) override;
/**
* @brief 为当前场景指定渲染风格预设
*/
void SetRenderStyleProfile(RenderStyleProfileId profile);
/**
* @brief 清除场景级渲染风格覆盖,回退到应用默认值
*/
void ClearRenderStyleProfileOverride();
bool HasRenderStyleProfileOverride() const { return hasRenderStyleProfileOverride_; }
/**
* @brief 解析当前场景最终应使用的渲染风格
*/
RenderStyleProfileId ResolveRenderStyleProfile(
RenderStyleProfileId defaultProfile) const;
static Scene* GetCurrent(); static Scene* GetCurrent();
@@ -25,9 +40,10 @@ private:
bool dispatchToChildren(const Event& event); bool dispatchToChildren(const Event& event);
static Scene* current_; static Scene* current_;
bool hasRenderStyleProfileOverride_ = false;
RenderStyleProfileId renderStyleProfileOverride_ = RenderStyleProfileId::Hybrid2D;
friend class SceneManager; friend class SceneManager;
}; };
} }

View File

@@ -58,6 +58,16 @@ void Actor::SetScale(float scale) {
markTransformDirty(); markTransformDirty();
} }
void Actor::SetSkew(const Vec2& skew) {
skew_ = skew;
markTransformDirty();
}
void Actor::SetSkew(float skewX, float skewY) {
skew_ = Vec2(skewX, skewY);
markTransformDirty();
}
void Actor::SetSize(const Vec2& size) { void Actor::SetSize(const Vec2& size) {
size_ = size; size_ = size;
markTransformDirty(); markTransformDirty();
@@ -123,6 +133,7 @@ void Actor::updateLocalTransform() const {
Vec2 anchorOffset = Vec2(anchor_.x * size_.x, anchor_.y * size_.y); Vec2 anchorOffset = Vec2(anchor_.x * size_.x, anchor_.y * size_.y);
localTransform_ = Transform2D::translation(position_.x, position_.y) * localTransform_ = Transform2D::translation(position_.x, position_.y) *
Transform2D::rotation(rotation_) * Transform2D::rotation(rotation_) *
Transform2D::skewing(skew_.x, skew_.y) *
Transform2D::scaling(scale_.x, scale_.y) * Transform2D::scaling(scale_.x, scale_.y) *
Transform2D::translation(-anchorOffset.x, -anchorOffset.y); Transform2D::translation(-anchorOffset.x, -anchorOffset.y);
} }

View File

@@ -0,0 +1,141 @@
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/graphics/renderer.h>
#include <algorithm>
namespace frostbite2D {
CanvasActor::CanvasActor() {
canvasRoot_ = MakePtr<Actor>();
canvasRoot_->SetName("canvasRoot");
}
bool CanvasActor::Init(int width, int height) {
return SetCanvasSize(width, height);
}
bool CanvasActor::SetCanvasSize(int width, int height) {
width = std::max(width, 1);
height = std::max(height, 1);
if (!renderTexture_) {
renderTexture_ = MakePtr<RenderTexture>();
}
if (!renderTexture_->Resize(width, height)) {
return false;
}
canvasWidth_ = width;
canvasHeight_ = height;
canvasCamera_.setPosition(Vec2::Zero());
canvasCamera_.setZoom(1.0f);
canvasCamera_.setViewport(canvasWidth_, canvasHeight_);
syncCanvasResources();
dirty_ = true;
return true;
}
void CanvasActor::SetClearColor(const Color& color) {
clearColor_ = color;
dirty_ = true;
}
void CanvasActor::SetDirty() {
dirty_ = true;
}
bool CanvasActor::Redraw() {
dirty_ = true;
return redrawInternal();
}
void CanvasActor::SetCustomDrawCallback(std::function<void()> callback) {
customDrawCallback_ = std::move(callback);
dirty_ = true;
}
void CanvasActor::ClearCustomDrawCallback() {
if (!customDrawCallback_) {
return;
}
customDrawCallback_ = nullptr;
dirty_ = true;
}
void CanvasActor::AddCanvasChild(RefPtr<Actor> child) {
if (!child || child.Get() == this) {
return;
}
canvasRoot_->AddChild(child);
dirty_ = true;
}
void CanvasActor::RemoveCanvasChild(RefPtr<Actor> child) {
if (!child) {
return;
}
canvasRoot_->RemoveChild(child);
dirty_ = true;
}
void CanvasActor::RemoveAllCanvasChildren() {
canvasRoot_->RemoveAllChildren();
dirty_ = true;
}
Ptr<Texture> CanvasActor::GetOutputTexture() const {
return renderTexture_ ? renderTexture_->GetTexture() : nullptr;
}
void CanvasActor::OnUpdate(float deltaTime) {
if (canvasRoot_) {
canvasRoot_->Update(deltaTime);
}
}
void CanvasActor::Render() {
if (dirty_) {
redrawInternal();
}
Sprite::Render();
}
bool CanvasActor::redrawInternal() {
if (!renderTexture_ || !renderTexture_->IsValid()) {
return false;
}
Renderer& renderer = Renderer::get();
if (!renderer.beginRenderToTexture(*renderTexture_, &canvasCamera_, clearColor_)) {
dirty_ = true;
return false;
}
if (canvasRoot_) {
canvasRoot_->Render();
}
if (customDrawCallback_) {
customDrawCallback_();
}
renderer.endRenderToTexture();
dirty_ = false;
return true;
}
void CanvasActor::syncCanvasResources() {
Ptr<Texture> outputTexture = renderTexture_ ? renderTexture_->GetTexture() : nullptr;
if (outputTexture) {
outputTexture->setSampling(TextureSampling::PixelArt);
}
SetTexture(outputTexture);
SetSize(static_cast<float>(canvasWidth_), static_cast<float>(canvasHeight_));
}
} // namespace frostbite2D

View File

@@ -25,6 +25,7 @@ Ptr<Sprite> Sprite::createFromFile(const std::string &path) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str()); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str());
return nullptr; return nullptr;
} }
texture->setSampling(TextureSampling::PixelArt);
auto sprite = MakePtr<Sprite>(texture); auto sprite = MakePtr<Sprite>(texture);
sprite->SetSizeToTexture(); sprite->SetSizeToTexture();
@@ -38,6 +39,7 @@ Ptr<Sprite> Sprite::createFromMemory(const uint8* data, int width, int height, i
"Failed to create texture from memory"); "Failed to create texture from memory");
return nullptr; return nullptr;
} }
texture->setSampling(TextureSampling::PixelArt);
auto sprite = MakePtr<Sprite>(texture); auto sprite = MakePtr<Sprite>(texture);
sprite->SetSizeToTexture(); sprite->SetSizeToTexture();
@@ -56,38 +58,12 @@ void Sprite::Render() {
} }
Renderer& renderer = Renderer::get(); Renderer& renderer = Renderer::get();
Quad quad = createQuad();
Vec2 size = GetSize(); auto* shader = getActiveShader();
if (size.x == 0 || size.y == 0) {
size = Vec2((float)texture_->getWidth(), (float)texture_->getHeight());
}
// 使用完整的变换矩阵进行渲染
Rect destRect(0, 0, size.x, size.y);
Rect srcRect = srcRect_;
if (srcRect.empty()) {
srcRect = Rect(0, 0, (float)texture_->getWidth(), (float)texture_->getHeight());
}
float worldOpacity = GetWorldOpacity();
Quad quad = Quad::createTextured(
destRect, srcRect,
Vec2((float)texture_->getWidth(), (float)texture_->getHeight()),
color_.r, color_.g, color_.b, color_.a * worldOpacity
);
if (flippedX_) {
std::swap(quad.vertices[0].texCoord, quad.vertices[1].texCoord);
std::swap(quad.vertices[2].texCoord, quad.vertices[3].texCoord);
}
if (flippedY_) {
std::swap(quad.vertices[0].texCoord, quad.vertices[2].texCoord);
std::swap(quad.vertices[1].texCoord, quad.vertices[3].texCoord);
}
auto* shader = renderer.getShaderManager().getShader("sprite");
if (shader) { if (shader) {
shader->use(); shader->use();
ConfigureShader(shader);
} }
Transform2D worldTransform = GetWorldTransform(); Transform2D worldTransform = GetWorldTransform();
@@ -151,6 +127,10 @@ void Sprite::SetOffset(float x, float y) {
offset_.y = y; offset_.y = y;
} }
void Sprite::ConfigureShader(Shader* shader) const {
(void)shader;
}
void Sprite::updateTransform() { void Sprite::updateTransform() {
Vec2 pos = GetPosition(); Vec2 pos = GetPosition();
float rotation = GetRotation(); float rotation = GetRotation();
@@ -180,6 +160,7 @@ Shader* Sprite::getActiveShader() const {
} }
Quad Sprite::createQuad() const { Quad Sprite::createQuad() const {
Renderer& renderer = Renderer::get();
Rect srcRect = srcRect_; Rect srcRect = srcRect_;
if (srcRect.empty()) { if (srcRect.empty()) {
srcRect = Rect(0, 0, texture_->getWidth(), texture_->getHeight()); srcRect = Rect(0, 0, texture_->getWidth(), texture_->getHeight());
@@ -201,7 +182,8 @@ Quad Sprite::createQuad() const {
float worldOpacity = GetWorldOpacity(); float worldOpacity = GetWorldOpacity();
Quad quad = Quad::createTextured( Quad quad = Quad::createTextured(
destRect, srcRect, texSize, destRect, srcRect, texSize,
color_.r, color_.g, color_.b, color_.a * worldOpacity color_.r, color_.g, color_.b, color_.a * worldOpacity,
renderer.shouldShrinkSubTextureUVs()
); );
if (flippedX_) { if (flippedX_) {

View File

@@ -148,6 +148,7 @@ void TextSprite::RenderText() {
); );
if (texture) { if (texture) {
texture->setSampling(TextureSampling::Linear);
SetTexture(texture); SetTexture(texture);
SetSizeToTexture(); SetSizeToTexture();
} }

View File

@@ -12,6 +12,46 @@ using namespace frostbite2D::animation;
namespace frostbite2D { namespace frostbite2D {
namespace { namespace {
struct BoundsAccumulator {
bool valid = false;
float minX = 0.0f;
float minY = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
void includePoint(const Vec2& point) {
if (!valid) {
minX = maxX = point.x;
minY = maxY = point.y;
valid = true;
return;
}
minX = std::min(minX, point.x);
minY = std::min(minY, point.y);
maxX = std::max(maxX, point.x);
maxY = std::max(maxY, point.y);
}
void includeRect(const Rect& rect) {
if (rect.empty()) {
return;
}
includePoint(Vec2(rect.left(), rect.top()));
includePoint(Vec2(rect.right(), rect.top()));
includePoint(Vec2(rect.left(), rect.bottom()));
includePoint(Vec2(rect.right(), rect.bottom()));
}
Rect toRect() const {
if (!valid) {
return Rect();
}
return Rect(minX, minY, maxX - minX, maxY - minY);
}
};
Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) { Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) {
return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y); return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y);
} }
@@ -37,6 +77,64 @@ BlendMode resolveBlendMode(const AniFrame& frameInfo) {
return BlendMode::Normal; return BlendMode::Normal;
} }
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
BoundsAccumulator bounds;
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.top())));
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.top())));
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.bottom())));
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.bottom())));
return bounds.toRect();
}
bool computeAnimationFrameBounds(const Animation& animation,
int frameIndex,
int direction,
Rect& outBounds) {
if (frameIndex < 0 ||
frameIndex >= static_cast<int>(animation.frames_.size()) ||
frameIndex >= static_cast<int>(animation.spriteFrames_.size()) ||
frameIndex >= static_cast<int>(animation.spriteFrameOffsets_.size())) {
return false;
}
const AniFrame& frameInfo = animation.frames_[frameIndex];
const auto& sprite = animation.spriteFrames_[frameIndex];
if (!sprite) {
return false;
}
Vec2 spriteSize = sprite->GetSize();
if (spriteSize.x <= 0.0f || spriteSize.y <= 0.0f) {
return false;
}
Vec2 imageRate = resolveImageRate(frameInfo);
float rotation = resolveFrameRotation(frameInfo);
Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
Vec2 offset = animation.spriteFrameOffsets_[frameIndex];
float mirroredRotation = rotation;
if (direction < 0) {
float visualWidth = spriteSize.x * std::abs(imageRate.x);
framePos.x = -framePos.x;
offset.x = -offset.x - visualWidth;
mirroredRotation = -rotation;
}
Transform2D transform =
Transform2D::translation(offset) *
Transform2D::translation(framePos) *
Transform2D::rotation(mirroredRotation) *
Transform2D::scaling(imageRate.x, imageRate.y);
outBounds = transformRectBounds(Rect(0.0f, 0.0f, spriteSize.x, spriteSize.y),
transform);
return !outBounds.empty();
}
void combineHash(uint64& seed, uint64 value) {
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}
} // namespace } // namespace
Animation::Animation() { Animation::Animation() {
@@ -346,6 +444,66 @@ Vec2 Animation::GetMaxSize() const {
return maxSize_; return maxSize_;
} }
bool Animation::GetStaticLocalBounds(Rect& outBounds) const {
BoundsAccumulator bounds;
Rect directionalBounds;
if (GetStaticLocalBounds(1, directionalBounds)) {
bounds.includeRect(directionalBounds);
}
if (GetStaticLocalBounds(-1, directionalBounds)) {
bounds.includeRect(directionalBounds);
}
outBounds = bounds.toRect();
return bounds.valid;
}
bool Animation::GetStaticLocalBounds(int direction, Rect& outBounds) const {
BoundsAccumulator bounds;
for (int i = 0; i < static_cast<int>(frames_.size()); ++i) {
Rect frameBounds;
if (computeAnimationFrameBounds(*this, i, direction, frameBounds)) {
bounds.includeRect(frameBounds);
}
}
for (const auto& child : GetChildren()) {
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
if (!subAnimation) {
continue;
}
Rect childBounds;
if (!subAnimation->GetStaticLocalBounds(direction, childBounds)) {
continue;
}
bounds.includeRect(transformRectBounds(childBounds, subAnimation->GetLocalTransform()));
}
outBounds = bounds.toRect();
return bounds.valid;
}
uint64 Animation::GetRenderSignature() const {
uint64 signature = 1469598103934665603ULL;
combineHash(signature, static_cast<uint64>(usable_));
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
combineHash(signature, static_cast<uint64>(std::max(currentFrameIndex_, 0)));
combineHash(signature, static_cast<uint64>(std::max(totalFrameCount_, 0)));
for (const auto& child : GetChildren()) {
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
if (!subAnimation) {
continue;
}
combineHash(signature, subAnimation->GetRenderSignature());
}
return signature;
}
void Animation::ApplyFramePresentation(const Vec2& framePos, void Animation::ApplyFramePresentation(const Vec2& framePos,
const Vec2& imageRate, const Vec2& imageRate,
float rotation, float rotation,

View File

@@ -10,6 +10,9 @@ namespace frostbite2D {
namespace { namespace {
Mix_Music* gCurrentMusicHandle = nullptr;
std::string gCurrentMusicPath;
std::string normalizeAudioLogicalPath(const std::string& path) { std::string normalizeAudioLogicalPath(const std::string& path) {
std::string normalized = path; std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/'); std::replace(normalized.begin(), normalized.end(), '\\', '/');
@@ -24,6 +27,30 @@ bool startsWithPrefix(const std::string& value, const char* prefix) {
value.compare(0, prefixLength, prefix) == 0; value.compare(0, prefixLength, prefix) == 0;
} }
std::string canonicalizeAudioPlaybackPath(const std::string& path) {
std::string normalized = normalizeAudioLogicalPath(path);
if (startsWithPrefix(normalized, "assets/music/") ||
startsWithPrefix(normalized, "assets/sounds/")) {
return normalized.substr(7);
}
return normalized;
}
bool hasActiveMusicPlayback() {
return Mix_PlayingMusic() == 1 || Mix_PausedMusic() == 1;
}
void clearCurrentMusicState() {
gCurrentMusicHandle = nullptr;
gCurrentMusicPath.clear();
}
void clearCurrentMusicStateIfStopped() {
if (!hasActiveMusicPlayback()) {
clearCurrentMusicState();
}
}
} // namespace } // namespace
Ptr<Music> Music::loadFromPath(const std::string& path) { Ptr<Music> Music::loadFromPath(const std::string& path) {
@@ -98,18 +125,37 @@ Ptr<Music> Music::loadFromNpk(const std::string& audioPath) {
return nullptr; return nullptr;
} }
return loadFromMemory(dataOpt->data(), dataOpt->size()); Ptr<Music> music = loadFromMemory(dataOpt->data(), dataOpt->size());
if (music) {
music->path_ = canonicalizeAudioPlaybackPath(normalizedPath);
}
return music;
}
bool Music::isPathPlaying(const std::string& path) {
clearCurrentMusicStateIfStopped();
return hasActiveMusicPlayback() &&
gCurrentMusicPath == canonicalizeAudioPlaybackPath(path);
}
std::string Music::getCurrentPlayingPath() {
clearCurrentMusicStateIfStopped();
return gCurrentMusicPath;
} }
Music::Music(Mix_Music* music, const std::string& path) Music::Music(Mix_Music* music, const std::string& path)
: music_(music), path_(path) { : music_(music), path_(canonicalizeAudioPlaybackPath(path)) {
} }
Music::Music(Mix_Music* music, std::vector<uint8> data, const std::string& path) Music::Music(Mix_Music* music, std::vector<uint8> data, const std::string& path)
: music_(music), path_(path), data_(std::move(data)) { : music_(music), path_(canonicalizeAudioPlaybackPath(path)),
data_(std::move(data)) {
} }
Music::~Music() { Music::~Music() {
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
if (music_) { if (music_) {
Mix_FreeMusic(music_); Mix_FreeMusic(music_);
music_ = nullptr; music_ = nullptr;
@@ -131,8 +177,11 @@ bool Music::play(int loops) {
Mix_VolumeMusic(vol); Mix_VolumeMusic(vol);
if (Mix_PlayMusic(music_, loops) == 0) { if (Mix_PlayMusic(music_, loops) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true; return true;
} else { } else {
clearCurrentMusicStateIfStopped();
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError()); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError());
return false; return false;
} }
@@ -151,7 +200,14 @@ bool Music::fadeIn(int ms, int loops) {
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME); int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeMusic(vol); Mix_VolumeMusic(vol);
return Mix_FadeInMusic(music_, loops, ms) == 0; if (Mix_FadeInMusic(music_, loops, ms) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true;
}
clearCurrentMusicStateIfStopped();
return false;
} }
void Music::pause() { void Music::pause() {
@@ -173,13 +229,20 @@ void Music::stop() {
return; return;
} }
Mix_HaltMusic(); Mix_HaltMusic();
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
} }
bool Music::fadeOut(int ms) { bool Music::fadeOut(int ms) {
if (!music_) { if (!music_) {
return false; return false;
} }
return Mix_FadeOutMusic(ms) == 0; bool result = Mix_FadeOutMusic(ms) == 0;
if (result && gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
return result;
} }
void Music::setVolume(float volume) { void Music::setVolume(float volume) {

View File

@@ -180,9 +180,13 @@ bool Application::initCoreModules() {
} }
window_->onResize([this](int width, int height) { window_->onResize([this](int width, int height) {
renderer_->setViewport(0, 0, width, height); if (!renderer_ || !window_) {
camera_->setViewport(width, height); return;
SDL_Log("Window resized to %dx%d", width, height); }
renderer_->setWindowSize(width, height, window_->scaleX(), window_->scaleY());
SDL_Log("Window resized to %dx%d (drawable %dx%d)", width, height,
window_->drawableWidth(), window_->drawableHeight());
}); });
// 初始化渲染器 // 初始化渲染器
@@ -197,14 +201,23 @@ bool Application::initCoreModules() {
// 设置窗口清除颜色和视口 // 设置窗口清除颜色和视口
renderer_->setClearColor(0.0f, 0.0f, 0.0f); renderer_->setClearColor(0.0f, 0.0f, 0.0f);
renderer_->setViewport(0, 0, config_.windowConfig.width, config_.windowConfig.height); renderer_->setDefaultRenderStyleProfile(config_.renderStyleProfile);
renderer_->setVirtualResolutionEnabled(config_.useVirtualResolution);
renderer_->setResolutionScaleMode(config_.resolutionMode);
if (config_.virtualWidth > 0 && config_.virtualHeight > 0) {
renderer_->setVirtualResolution(config_.virtualWidth, config_.virtualHeight);
}
renderer_->setWindowSize(window_->width(), window_->height(), window_->scaleX(),
window_->scaleY());
// 创建并设置相机 // 创建并设置相机
{ {
ScopedStartupTrace stageTrace("Camera setup"); ScopedStartupTrace stageTrace("Camera setup");
camera_ = new Camera(); camera_ = new Camera();
camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height); camera_->setViewport(renderer_->getVirtualWidth(), renderer_->getVirtualHeight());
camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角 camera_->setFlipY(true); // Use top-left as (0,0).
renderer_->applyRenderStyleToCamera(
camera_, config_.renderStyleProfile, RenderStyleLayerRole::World);
renderer_->setCamera(camera_); renderer_->setCamera(camera_);
} }
@@ -282,6 +295,17 @@ void Application::mainLoop() {
break; break;
} }
if (sdlEvent.type == SDL_WINDOWEVENT && window_) {
switch (sdlEvent.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
window_->refreshMetrics(true);
break;
default:
break;
}
}
auto event = convertSDLEvent(sdlEvent); auto event = convertSDLEvent(sdlEvent);
if (event) { if (event) {
dispatchEvent(*event); dispatchEvent(*event);

View File

@@ -4,11 +4,9 @@
#include <frostbite2D/resource/asset.h> #include <frostbite2D/resource/asset.h>
#include <glad/glad.h> #include <glad/glad.h>
namespace frostbite2D { namespace frostbite2D {
bool Window::create(const WindowConfig& cfg) { bool Window::create(const WindowConfig& cfg) {
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN; Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
#ifndef __SWITCH__ #ifndef __SWITCH__
@@ -28,7 +26,6 @@ bool Window::create(const WindowConfig &cfg) {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
@@ -38,17 +35,11 @@ bool Window::create(const WindowConfig &cfg) {
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples);
} }
int x = SDL_WINDOWPOS_CENTERED; int x = cfg.centered ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED;
int y = SDL_WINDOWPOS_CENTERED; int y = cfg.centered ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED;
if (!cfg.centered) {
x = SDL_WINDOWPOS_UNDEFINED;
y = SDL_WINDOWPOS_UNDEFINED;
}
// 创建窗口
sdlWindow_ =
SDL_CreateWindow(cfg.title.c_str(), x, y, cfg.width, cfg.height, flags);
sdlWindow_ = SDL_CreateWindow(cfg.title.c_str(), x, y, cfg.width, cfg.height,
flags);
if (!sdlWindow_) { if (!sdlWindow_) {
SDL_Log("Failed to create window: %s", SDL_GetError()); SDL_Log("Failed to create window: %s", SDL_GetError());
return false; return false;
@@ -72,23 +63,12 @@ bool Window::create(const WindowConfig &cfg) {
} }
SDL_GL_SetSwapInterval(cfg.vsync ? 1 : 0); SDL_GL_SetSwapInterval(cfg.vsync ? 1 : 0);
refreshMetrics(false);
// 获取实际的可绘制大小(考虑高 DPI 和 Switch 等平台)
int drawableWidth, drawableHeight;
SDL_GL_GetDrawableSize(sdlWindow_, &drawableWidth, &drawableHeight);
width_ = drawableWidth;
height_ = drawableHeight;
// 计算缩放比例
int windowWidth, windowHeight;
SDL_GetWindowSize(sdlWindow_, &windowWidth, &windowHeight);
if (windowWidth > 0 && windowHeight > 0) {
scaleX_ = static_cast<float>(drawableWidth) / static_cast<float>(windowWidth);
scaleY_ = static_cast<float>(drawableHeight) / static_cast<float>(windowHeight);
}
fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;
vsync_ = cfg.vsync; vsync_ = cfg.vsync;
cursorVisible_ = cfg.showCursor;
showCursor(cfg.showCursor);
#ifndef __SWITCH__ #ifndef __SWITCH__
if (!cfg.icon.file_path.empty()) { if (!cfg.icon.file_path.empty()) {
@@ -113,9 +93,11 @@ bool Window::create(const WindowConfig &cfg) {
} }
void Window::destroy() { void Window::destroy() {
if (sdlWindow_) { if (glContext_) {
SDL_GL_DeleteContext(glContext_); SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr; glContext_ = nullptr;
}
if (sdlWindow_) {
SDL_DestroyWindow(sdlWindow_); SDL_DestroyWindow(sdlWindow_);
sdlWindow_ = nullptr; sdlWindow_ = nullptr;
} }
@@ -129,16 +111,32 @@ void Window::swap() {
} }
} }
void Window::close() { shouldClose_ = true; } void Window::close() {
shouldClose_ = true;
if (closeCb_) {
closeCb_();
}
}
void Window::setTitle(const std::string &title) {} void Window::setTitle(const std::string& title) {
if (sdlWindow_) {
SDL_SetWindowTitle(sdlWindow_, title.c_str());
}
}
void Window::setSize(int w, int h) { void Window::setSize(int w, int h) {
if (sdlWindow_) { if (sdlWindow_) {
SDL_SetWindowSize(sdlWindow_, w, h); SDL_SetWindowSize(sdlWindow_, w, h);
refreshMetrics(true);
return;
} }
width_ = w; width_ = w;
height_ = h; height_ = h;
drawableWidth_ = w;
drawableHeight_ = h;
scaleX_ = 1.0f;
scaleY_ = 1.0f;
if (resizeCb_) { if (resizeCb_) {
resizeCb_(w, h); resizeCb_(w, h);
} }
@@ -150,23 +148,54 @@ void Window::setPos(int x, int y) {
} }
} }
void Window::setFullscreen(bool fs) { fullscreen_ = fs; } void Window::setFullscreen(bool fs) {
fullscreen_ = fs;
if (!sdlWindow_) {
return;
}
void Window::setVSync(bool vsync) { vsync_ = vsync; } #ifndef __SWITCH__
Uint32 flags = fs ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
SDL_SetWindowFullscreen(sdlWindow_, flags);
refreshMetrics(true);
#endif
}
void Window::setVisible(bool visible) {} void Window::setVSync(bool vsync) {
vsync_ = vsync;
if (glContext_) {
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
}
}
void Window::setVisible(bool visible) {
if (!sdlWindow_) {
return;
}
if (visible) {
SDL_ShowWindow(sdlWindow_);
} else {
SDL_HideWindow(sdlWindow_);
}
}
int Window::width() const { return width_; } int Window::width() const { return width_; }
int Window::height() const { return height_; } int Window::height() const { return height_; }
int Window::drawableWidth() const { return drawableWidth_; }
int Window::drawableHeight() const { return drawableHeight_; }
Size Window::size() const { Size Window::size() const {
return Size{static_cast<float>(width_), static_cast<float>(height_)}; return Size{static_cast<float>(width_), static_cast<float>(height_)};
} }
Vec2 Window::pos() const { Vec2 Window::pos() const {
if (sdlWindow_) { if (sdlWindow_) {
int x, y; int x = 0;
int y = 0;
SDL_GetWindowPosition(sdlWindow_, &x, &y); SDL_GetWindowPosition(sdlWindow_, &x, &y);
return Vec2{static_cast<float>(x), static_cast<float>(y)}; return Vec2{static_cast<float>(x), static_cast<float>(y)};
} }
@@ -185,11 +214,90 @@ float Window::scaleX() const { return scaleX_; }
float Window::scaleY() const { return scaleY_; } float Window::scaleY() const { return scaleY_; }
void Window::setCursor(CursorType cursor) {} bool Window::refreshMetrics(bool emitResize) {
if (!sdlWindow_) {
return false;
}
void Window::showCursor(bool show) { cursorVisible_ = show; } int prevWidth = width_;
int prevHeight = height_;
int prevDrawableWidth = drawableWidth_;
int prevDrawableHeight = drawableHeight_;
void Window::lockCursor(bool lock) { cursorLocked_ = lock; } SDL_GetWindowSize(sdlWindow_, &width_, &height_);
SDL_GL_GetDrawableSize(sdlWindow_, &drawableWidth_, &drawableHeight_);
if (width_ > 0 && height_ > 0) {
scaleX_ = static_cast<float>(drawableWidth_) / static_cast<float>(width_);
scaleY_ = static_cast<float>(drawableHeight_) / static_cast<float>(height_);
} else {
scaleX_ = 1.0f;
scaleY_ = 1.0f;
}
Uint32 windowFlags = SDL_GetWindowFlags(sdlWindow_);
focused_ = (windowFlags & SDL_WINDOW_INPUT_FOCUS) != 0;
minimized_ = (windowFlags & SDL_WINDOW_MINIMIZED) != 0;
fullscreen_ = (windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 ||
(windowFlags & SDL_WINDOW_FULLSCREEN) != 0;
bool changed = prevWidth != width_ || prevHeight != height_ ||
prevDrawableWidth != drawableWidth_ ||
prevDrawableHeight != drawableHeight_;
if (emitResize && changed && resizeCb_) {
resizeCb_(width_, height_);
}
return true;
}
void Window::setCursor(CursorType cursor) {
SDL_SystemCursor systemCursor = SDL_SYSTEM_CURSOR_ARROW;
switch (cursor) {
case CursorType::Arrow:
systemCursor = SDL_SYSTEM_CURSOR_ARROW;
break;
case CursorType::TextInput:
systemCursor = SDL_SYSTEM_CURSOR_IBEAM;
break;
case CursorType::Hand:
systemCursor = SDL_SYSTEM_CURSOR_HAND;
break;
case CursorType::SizeAll:
systemCursor = SDL_SYSTEM_CURSOR_SIZEALL;
break;
case CursorType::SizeWE:
systemCursor = SDL_SYSTEM_CURSOR_SIZEWE;
break;
case CursorType::SizeNS:
systemCursor = SDL_SYSTEM_CURSOR_SIZENS;
break;
case CursorType::SizeNESW:
systemCursor = SDL_SYSTEM_CURSOR_SIZENESW;
break;
case CursorType::SizeNWSE:
systemCursor = SDL_SYSTEM_CURSOR_SIZENWSE;
break;
case CursorType::No:
systemCursor = SDL_SYSTEM_CURSOR_ARROW;
break;
}
SDL_Cursor* sdlCursor = SDL_CreateSystemCursor(systemCursor);
if (sdlCursor) {
SDL_SetCursor(sdlCursor);
}
}
void Window::showCursor(bool show) {
cursorVisible_ = show;
SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE);
}
void Window::lockCursor(bool lock) {
cursorLocked_ = lock;
SDL_SetRelativeMouseMode(lock ? SDL_TRUE : SDL_FALSE);
}
void Window::onResize(ResizeCb cb) { resizeCb_ = cb; } void Window::onResize(ResizeCb cb) { resizeCb_ = cb; }

View File

@@ -98,15 +98,25 @@ void Batch::submitQuad(const Quad& quad, const Transform2D& transform,
flushIfNeeded(newKey, shader, texture); flushIfNeeded(newKey, shader, texture);
Renderer& renderer = Renderer::get();
glm::mat4 mat = transform.matrix; glm::mat4 mat = transform.matrix;
Camera* camera = renderer.getCamera();
Vec2 snapOffset = Vec2::Zero();
if (camera && renderer.shouldSnapVertices()) {
glm::vec4 referencePos(quad.vertices[0].position.x,
quad.vertices[0].position.y, 0.0f, 1.0f);
glm::vec4 transformedReference = mat * referencePos;
Vec2 referenceWorld(transformedReference.x, transformedReference.y);
snapOffset = camera->snapWorldPosition(referenceWorld) - referenceWorld;
}
uint16_t indexOffset = static_cast<uint16_t>(currentBatch_.vertices.size()); uint16_t indexOffset = static_cast<uint16_t>(currentBatch_.vertices.size());
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
Vertex v = quad.vertices[i]; Vertex v = quad.vertices[i];
glm::vec4 pos(v.position.x, v.position.y, 0.0f, 1.0f); glm::vec4 pos(v.position.x, v.position.y, 0.0f, 1.0f);
glm::vec4 transformed = mat * pos; glm::vec4 transformed = mat * pos;
v.position.x = transformed.x; v.position.x = transformed.x + snapOffset.x;
v.position.y = transformed.y; v.position.y = transformed.y + snapOffset.y;
currentBatch_.vertices.push_back(v); currentBatch_.vertices.push_back(v);
} }
@@ -161,7 +171,9 @@ void Batch::flushCurrentBatch() {
currentBatch_.shader->setTexture("u_texture", 0); currentBatch_.shader->setTexture("u_texture", 0);
} }
if (currentBatch_.texture) { if (currentBatch_.texture) {
currentBatch_.texture->bind(0); Renderer& renderer = Renderer::get();
bool preferPixelArtSampling = renderer.shouldUsePixelArtSampling();
currentBatch_.texture->bind(0, preferPixelArtSampling);
} }
glBindBuffer(GL_ARRAY_BUFFER, vbo_); glBindBuffer(GL_ARRAY_BUFFER, vbo_);

View File

@@ -1,36 +1,80 @@
#include <frostbite2D/graphics/camera.h> #include <frostbite2D/graphics/camera.h>
#include <algorithm>
#include <cmath>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
namespace frostbite2D { namespace frostbite2D {
namespace {
constexpr float kMinZoom = 0.01f;
float clampZoom(float zoom) {
return std::max(zoom, kMinZoom);
}
Vec2 snapPositionToPixelGrid(const Vec2& position, float zoom) {
float safeZoom = clampZoom(zoom);
return Vec2(std::round(position.x * safeZoom) / safeZoom,
std::round(position.y * safeZoom) / safeZoom);
}
} // namespace
Camera::Camera() : position_(0, 0), zoom_(1.0f) {} Camera::Camera() : position_(0, 0), zoom_(1.0f) {}
void Camera::setPosition(const Vec2& pos) { position_ = pos; } void Camera::setPosition(const Vec2& pos) { position_ = pos; }
void Camera::setZoom(float zoom) { zoom_ = zoom; } void Camera::setZoom(float zoom) { zoom_ = clampZoom(zoom); }
void Camera::setViewport(int width, int height) { void Camera::setViewport(int width, int height) {
viewportWidth_ = width; viewportWidth_ = width;
viewportHeight_ = height; viewportHeight_ = height;
} }
Vec2 Camera::getRenderPosition() const {
return snapWorldPosition(position_);
}
Vec2 Camera::snapWorldPosition(const Vec2& position) const {
if (!pixelSnapEnabled_) {
return position;
}
return snapPositionToPixelGrid(position, zoom_);
}
float Camera::getVisibleWidth() const {
float safeZoom = clampZoom(zoom_);
return static_cast<float>(viewportWidth_) / safeZoom;
}
float Camera::getVisibleHeight() const {
float safeZoom = clampZoom(zoom_);
return static_cast<float>(viewportHeight_) / safeZoom;
}
void Camera::lookAt(const Vec2& target) { position_ = target; } void Camera::lookAt(const Vec2& target) { position_ = target; }
void Camera::move(const Vec2& delta) { position_ = position_ + delta; } void Camera::move(const Vec2& delta) { position_ = position_ + delta; }
void Camera::zoomAt(float factor, const Vec2& screenPos) { void Camera::zoomAt(float factor, const Vec2& screenPos) {
Vec2 worldBefore = screenPos - position_; float previousZoom = clampZoom(zoom_);
zoom_ *= factor; Vec2 worldBefore(position_.x + screenPos.x / previousZoom,
Vec2 worldAfter = screenPos - position_; position_.y + screenPos.y / previousZoom);
zoom_ = clampZoom(zoom_ * factor);
Vec2 worldAfter(position_.x + screenPos.x / zoom_,
position_.y + screenPos.y / zoom_);
position_ = position_ + (worldBefore - worldAfter); position_ = position_ + (worldBefore - worldAfter);
} }
glm::mat4 Camera::getViewMatrix() const { glm::mat4 Camera::getViewMatrix() const {
float safeZoom = clampZoom(zoom_);
Vec2 renderPosition = getRenderPosition();
glm::mat4 view = glm::mat4(1.0f); glm::mat4 view = glm::mat4(1.0f);
view[3][0] = -position_.x; view = glm::scale(view, glm::vec3(safeZoom, safeZoom, 1.0f));
view[3][1] = -position_.y; view = glm::translate(view,
view[0][0] = zoom_; glm::vec3(-renderPosition.x, -renderPosition.y, 0.0f));
view[1][1] = zoom_;
return view; return view;
} }
@@ -40,13 +84,9 @@ glm::mat4 Camera::getProjectionMatrix() const {
float bottom, top; float bottom, top;
if (flipY_) { if (flipY_) {
// Y 轴向下:(0,0) 在左上角2D 游戏常用)
// glm::ortho(left, right, bottom, top, ...)
// 这里 bottom 是屏幕底部值大top 是屏幕顶部(值小)
bottom = static_cast<float>(viewportHeight_); bottom = static_cast<float>(viewportHeight_);
top = 0.0f; top = 0.0f;
} else { } else {
// Y 轴向上:(0,0) 在左下角OpenGL 默认)
bottom = 0.0f; bottom = 0.0f;
top = static_cast<float>(viewportHeight_); top = static_cast<float>(viewportHeight_);
} }

View File

@@ -0,0 +1,67 @@
#include <frostbite2D/graphics/render_texture.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <glad/glad.h>
namespace frostbite2D {
RenderTexture::~RenderTexture() {
Reset();
}
bool RenderTexture::Init(int width, int height) {
return Resize(width, height);
}
bool RenderTexture::Resize(int width, int height) {
width = std::max(width, 1);
height = std::max(height, 1);
if (framebufferID_ != 0 && texture_ && width_ == width && height_ == height) {
return true;
}
Reset();
texture_ = Texture::createEmpty(width, height);
if (!texture_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"RenderTexture: failed to create color texture %dx%d",
width, height);
return false;
}
glGenFramebuffers(1, &framebufferID_);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID_);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
texture_->getID(), 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"RenderTexture: framebuffer incomplete (status=0x%x)",
static_cast<unsigned int>(status));
Reset();
return false;
}
width_ = width;
height_ = height;
return true;
}
void RenderTexture::Reset() {
texture_.Reset();
if (framebufferID_ != 0) {
glDeleteFramebuffers(1, &framebufferID_);
framebufferID_ = 0;
}
width_ = 0;
height_ = 0;
}
} // namespace frostbite2D

View File

@@ -1,19 +1,38 @@
#include "SDL_log.h" #include "SDL_log.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h> #include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/graphics/render_texture.h>
#include <glad/glad.h> #include <glad/glad.h>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
namespace frostbite2D { namespace frostbite2D {
namespace {
constexpr float kMinContentScale = 0.01f;
int roundToInt(float value) {
return static_cast<int>(std::lround(value));
}
RenderStyleLayerSettings resolveRenderStyleLayer(RenderStyleProfileId profile,
RenderStyleLayerRole role) {
return RenderStyleSettings::FromProfile(profile).layer(role);
}
} // namespace
Renderer& Renderer::get() { Renderer& Renderer::get() {
static Renderer instance; static Renderer instance;
return instance; return instance;
} }
Renderer::Renderer() = default; Renderer::Renderer() {
activeRenderStyle_ =
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用 resolveRenderStyleLayer(defaultRenderStyleProfile_, activeRenderStyleRole_);
}
bool Renderer::init() { bool Renderer::init() {
if (initialized_) { if (initialized_) {
@@ -21,13 +40,14 @@ bool Renderer::init() {
} }
if (!batch_.init()) { if (!batch_.init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize batch system"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize batch system");
return false; return false;
} }
//初始化着色器管理器
if (!shaderManager_.init("assets/shaders")) { if (!shaderManager_.init("assets/shaders")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize shader manager"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize shader manager");
return false; return false;
} }
@@ -36,42 +56,191 @@ bool Renderer::init() {
SDL_Log("Renderer initialized"); SDL_Log("Renderer initialized");
initialized_ = true; initialized_ = true;
recalculateResolutionState();
return true; return true;
} }
void Renderer::shutdown() { void Renderer::shutdown() {
renderTargetStack_.clear();
batch_.shutdown(); batch_.shutdown();
shaderManager_.shutdown(); shaderManager_.shutdown();
initialized_ = false; initialized_ = false;
frameActive_ = false;
} }
void Renderer::beginFrame() { void Renderer::beginFrame() {
glViewport(viewportX_, viewportY_, viewportWidth_, viewportHeight_); glViewport(resolutionState_.viewportX, resolutionState_.viewportY,
resolutionState_.viewportWidth, resolutionState_.viewportHeight);
glClearColor(clearColor_[0] / 255.0f, clearColor_[1] / 255.0f, glClearColor(clearColor_[0] / 255.0f, clearColor_[1] / 255.0f,
clearColor_[2] / 255.0f, clearColor_[3] / 255.0f); clearColor_[2] / 255.0f, clearColor_[3] / 255.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
updateUniforms(); updateUniforms();
batch_.begin(); batch_.begin();
frameActive_ = true;
} }
void Renderer::endFrame() { void Renderer::endFrame() {
while (!renderTargetStack_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: endFrame() auto-closing unfinished render target scope");
endRenderToTexture();
}
batch_.end(); batch_.end();
frameActive_ = false;
} }
void Renderer::flush() { void Renderer::flush() {
batch_.flush(); batch_.flush();
} }
void Renderer::setViewport(int x, int y, int width, int height) { bool Renderer::beginRenderToTexture(RenderTexture& target, Camera* camera,
viewportX_ = x; const Color& clearColor, bool clear) {
viewportY_ = y; if (!initialized_) {
viewportWidth_ = width; SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
viewportHeight_ = height; "Renderer: beginRenderToTexture called before renderer init");
return false;
if (camera_) {
camera_->setViewport(width, height);
} }
if (!target.IsValid()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: beginRenderToTexture called with invalid target");
return false;
}
Camera* activeCamera = camera ? camera : camera_;
if (!activeCamera) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: beginRenderToTexture requires an active camera");
return false;
}
RenderTargetState state;
GLint framebufferBinding = 0;
GLint viewport[4] = {};
GLfloat clearColorValue[4] = {};
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferBinding);
glGetIntegerv(GL_VIEWPORT, viewport);
glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColorValue);
state.framebuffer = static_cast<uint32>(framebufferBinding);
state.viewportX = viewport[0];
state.viewportY = viewport[1];
state.viewportWidth = std::max(viewport[2], 1);
state.viewportHeight = std::max(viewport[3], 1);
state.clearColor[0] = clearColorValue[0];
state.clearColor[1] = clearColorValue[1];
state.clearColor[2] = clearColorValue[2];
state.clearColor[3] = clearColorValue[3];
state.camera = camera_;
if (frameActive_ || !renderTargetStack_.empty()) {
batch_.flush();
} else {
batch_.begin();
state.startedStandaloneBatch = true;
}
renderTargetStack_.push_back(state);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebufferID_);
glViewport(0, 0, std::max(target.width_, 1), std::max(target.height_, 1));
if (clear) {
glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
glClear(GL_COLOR_BUFFER_BIT);
}
camera_ = activeCamera;
updateUniforms();
return true;
}
bool Renderer::endRenderToTexture() {
if (renderTargetStack_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: endRenderToTexture called without matching begin");
return false;
}
RenderTargetState state = renderTargetStack_.back();
renderTargetStack_.pop_back();
if (state.startedStandaloneBatch) {
batch_.end();
} else {
batch_.flush();
}
glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer);
glViewport(state.viewportX, state.viewportY, state.viewportWidth,
state.viewportHeight);
glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
state.clearColor[3]);
camera_ = state.camera;
updateUniforms();
return true;
}
void Renderer::setViewport(int x, int y, int width, int height) {
resolutionState_.viewportX = x;
resolutionState_.viewportY = y;
resolutionState_.viewportWidth = std::max(width, 1);
resolutionState_.viewportHeight = std::max(height, 1);
float safeScaleX = std::max(resolutionState_.contentScaleX, kMinContentScale);
float safeScaleY = std::max(resolutionState_.contentScaleY, kMinContentScale);
resolutionState_.windowViewportX = roundToInt(
static_cast<float>(resolutionState_.viewportX) / safeScaleX);
resolutionState_.windowViewportY = roundToInt(
static_cast<float>(resolutionState_.viewportY) / safeScaleY);
resolutionState_.windowViewportWidth = roundToInt(
static_cast<float>(resolutionState_.viewportWidth) / safeScaleX);
resolutionState_.windowViewportHeight = roundToInt(
static_cast<float>(resolutionState_.viewportHeight) / safeScaleY);
int logicalWidth = std::max(getVirtualWidth(), 1);
int logicalHeight = std::max(getVirtualHeight(), 1);
resolutionState_.scaleX =
static_cast<float>(resolutionState_.windowViewportWidth) /
static_cast<float>(logicalWidth);
resolutionState_.scaleY =
static_cast<float>(resolutionState_.windowViewportHeight) /
static_cast<float>(logicalHeight);
syncCameraViewport();
}
void Renderer::setWindowSize(int width, int height, float contentScaleX,
float contentScaleY) {
resolutionState_.windowWidth = std::max(width, 1);
resolutionState_.windowHeight = std::max(height, 1);
resolutionState_.contentScaleX = std::max(contentScaleX, kMinContentScale);
resolutionState_.contentScaleY = std::max(contentScaleY, kMinContentScale);
resolutionState_.drawableWidth = std::max(
roundToInt(static_cast<float>(resolutionState_.windowWidth) *
resolutionState_.contentScaleX),
1);
resolutionState_.drawableHeight = std::max(
roundToInt(static_cast<float>(resolutionState_.windowHeight) *
resolutionState_.contentScaleY),
1);
recalculateResolutionState();
}
void Renderer::setVirtualResolutionEnabled(bool enabled) {
useVirtualResolution_ = enabled;
recalculateResolutionState();
}
void Renderer::setVirtualResolution(int width, int height) {
virtualWidth_ = std::max(width, 1);
virtualHeight_ = std::max(height, 1);
recalculateResolutionState();
}
void Renderer::setResolutionScaleMode(ResolutionScaleMode mode) {
resolutionMode_ = mode;
recalculateResolutionState();
} }
void Renderer::setClearColor(float r, float g, float b, float a) { void Renderer::setClearColor(float r, float g, float b, float a) {
@@ -92,21 +261,203 @@ void Renderer::clear(uint32_t flags) {
glClear(flags); glClear(flags);
} }
void Renderer::setDefaultRenderStyleProfile(RenderStyleProfileId profile) {
defaultRenderStyleProfile_ = profile;
setActiveRenderStyleProfile(profile, activeRenderStyleRole_);
}
void Renderer::setActiveRenderStyleProfile(RenderStyleProfileId profile,
RenderStyleLayerRole role) {
if (initialized_ &&
(activeRenderStyleProfile_ != profile || activeRenderStyleRole_ != role)) {
batch_.flush();
}
activeRenderStyleProfile_ = profile;
activeRenderStyleRole_ = role;
activeRenderStyle_ = resolveRenderStyleLayer(profile, role);
}
void Renderer::applyRenderStyleToCamera(Camera* camera,
RenderStyleProfileId profile,
RenderStyleLayerRole role) const {
if (!camera) {
return;
}
RenderStyleLayerSettings settings = resolveRenderStyleLayer(profile, role);
camera->setPixelSnapEnabled(settings.cameraPixelSnap);
}
void Renderer::setCamera(Camera* camera) { void Renderer::setCamera(Camera* camera) {
if (initialized_ && camera_ != camera) { if (initialized_ && camera_ != camera) {
batch_.flush(); batch_.flush();
} }
camera_ = camera; camera_ = camera;
if (camera) { syncCameraViewport();
camera_->setViewport(viewportWidth_, viewportHeight_);
}
if (initialized_) { if (initialized_) {
updateUniforms(); updateUniforms();
} }
} }
int Renderer::getVirtualWidth() const {
if (useVirtualResolution_) {
return std::max(virtualWidth_, 1);
}
return std::max(resolutionState_.windowWidth, 1);
}
int Renderer::getVirtualHeight() const {
if (useVirtualResolution_) {
return std::max(virtualHeight_, 1);
}
return std::max(resolutionState_.windowHeight, 1);
}
Rect Renderer::getViewportRect() const {
return resolutionState_.getWindowViewportRect();
}
Vec2 Renderer::screenToVirtual(const Vec2& screenPos) const {
Rect viewport = getViewportRect();
if (viewport.width() <= 0.0f || viewport.height() <= 0.0f) {
return screenPos;
}
float localX = screenPos.x - viewport.left();
float localY = screenPos.y - viewport.top();
return Vec2(localX * static_cast<float>(getVirtualWidth()) / viewport.width(),
localY * static_cast<float>(getVirtualHeight()) /
viewport.height());
}
Vec2 Renderer::virtualToScreen(const Vec2& virtualPos) const {
Rect viewport = getViewportRect();
float logicalWidth = static_cast<float>(std::max(getVirtualWidth(), 1));
float logicalHeight = static_cast<float>(std::max(getVirtualHeight(), 1));
return Vec2(viewport.left() + virtualPos.x * viewport.width() / logicalWidth,
viewport.top() + virtualPos.y * viewport.height() /
logicalHeight);
}
void Renderer::recalculateResolutionState() {
resolutionState_.windowWidth = std::max(resolutionState_.windowWidth, 1);
resolutionState_.windowHeight = std::max(resolutionState_.windowHeight, 1);
resolutionState_.contentScaleX =
std::max(resolutionState_.contentScaleX, kMinContentScale);
resolutionState_.contentScaleY =
std::max(resolutionState_.contentScaleY, kMinContentScale);
resolutionState_.drawableWidth = std::max(
roundToInt(static_cast<float>(resolutionState_.windowWidth) *
resolutionState_.contentScaleX),
1);
resolutionState_.drawableHeight = std::max(
roundToInt(static_cast<float>(resolutionState_.windowHeight) *
resolutionState_.contentScaleY),
1);
int windowViewportX = 0;
int windowViewportY = 0;
int windowViewportWidth = resolutionState_.windowWidth;
int windowViewportHeight = resolutionState_.windowHeight;
if (useVirtualResolution_) {
float widthScale = static_cast<float>(resolutionState_.windowWidth) /
static_cast<float>(std::max(virtualWidth_, 1));
float heightScale = static_cast<float>(resolutionState_.windowHeight) /
static_cast<float>(std::max(virtualHeight_, 1));
switch (resolutionMode_) {
case ResolutionScaleMode::Disabled:
resolutionState_.scaleX = widthScale;
resolutionState_.scaleY = heightScale;
break;
case ResolutionScaleMode::FitHeight: {
float uniformScale = heightScale;
float fittedWidth = static_cast<float>(virtualWidth_) * uniformScale;
if (fittedWidth > static_cast<float>(resolutionState_.windowWidth)) {
uniformScale = std::min(widthScale, heightScale);
}
windowViewportWidth = std::clamp(
roundToInt(static_cast<float>(virtualWidth_) * uniformScale), 1,
resolutionState_.windowWidth);
windowViewportHeight = std::clamp(
roundToInt(static_cast<float>(virtualHeight_) * uniformScale), 1,
resolutionState_.windowHeight);
windowViewportX =
(resolutionState_.windowWidth - windowViewportWidth) / 2;
windowViewportY =
(resolutionState_.windowHeight - windowViewportHeight) / 2;
resolutionState_.scaleX =
static_cast<float>(windowViewportWidth) /
static_cast<float>(std::max(virtualWidth_, 1));
resolutionState_.scaleY =
static_cast<float>(windowViewportHeight) /
static_cast<float>(std::max(virtualHeight_, 1));
break;
}
case ResolutionScaleMode::Fit:
default: {
float uniformScale = std::min(widthScale, heightScale);
windowViewportWidth = std::clamp(
roundToInt(static_cast<float>(virtualWidth_) * uniformScale), 1,
resolutionState_.windowWidth);
windowViewportHeight = std::clamp(
roundToInt(static_cast<float>(virtualHeight_) * uniformScale), 1,
resolutionState_.windowHeight);
windowViewportX =
(resolutionState_.windowWidth - windowViewportWidth) / 2;
windowViewportY =
(resolutionState_.windowHeight - windowViewportHeight) / 2;
resolutionState_.scaleX =
static_cast<float>(windowViewportWidth) /
static_cast<float>(std::max(virtualWidth_, 1));
resolutionState_.scaleY =
static_cast<float>(windowViewportHeight) /
static_cast<float>(std::max(virtualHeight_, 1));
break;
}
}
} else {
resolutionState_.scaleX = 1.0f;
resolutionState_.scaleY = 1.0f;
}
resolutionState_.windowViewportX = windowViewportX;
resolutionState_.windowViewportY = windowViewportY;
resolutionState_.windowViewportWidth = windowViewportWidth;
resolutionState_.windowViewportHeight = windowViewportHeight;
resolutionState_.viewportX = roundToInt(
static_cast<float>(windowViewportX) * resolutionState_.contentScaleX);
resolutionState_.viewportY = roundToInt(
static_cast<float>(windowViewportY) * resolutionState_.contentScaleY);
resolutionState_.viewportWidth = std::max(
roundToInt(static_cast<float>(windowViewportWidth) *
resolutionState_.contentScaleX),
1);
resolutionState_.viewportHeight = std::max(
roundToInt(static_cast<float>(windowViewportHeight) *
resolutionState_.contentScaleY),
1);
syncCameraViewport();
if (initialized_) {
updateUniforms();
}
}
void Renderer::syncCameraViewport() {
if (!camera_) {
return;
}
camera_->setViewport(getVirtualWidth(), getVirtualHeight());
}
void Renderer::setupBlendMode(BlendMode mode) { void Renderer::setupBlendMode(BlendMode mode) {
switch (mode) { switch (mode) {
case BlendMode::None: case BlendMode::None:
@@ -116,64 +467,56 @@ void Renderer::setupBlendMode(BlendMode mode) {
case BlendMode::Normal: case BlendMode::Normal:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD); glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GL_ONE_MINUS_SRC_ALPHA);
break; break;
case BlendMode::Additive: case BlendMode::Additive:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD); glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
GL_ONE, GL_ONE);
break; break;
case BlendMode::Screen: case BlendMode::Screen:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD); glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE,
GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GL_ONE_MINUS_SRC_ALPHA);
break; break;
case BlendMode::Premultiplied: case BlendMode::Premultiplied:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD); glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GL_ONE_MINUS_SRC_ALPHA);
break; break;
case BlendMode::Subtractive: case BlendMode::Subtractive:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
break; break;
case BlendMode::Multiply: case BlendMode::Multiply:
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD); glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, glBlendFuncSeparate(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GL_ONE_MINUS_SRC_ALPHA);
break; break;
} }
} }
void Renderer::updateUniforms() { void Renderer::updateUniforms() {
if (camera_) { if (!camera_) {
auto* spriteShader = shaderManager_.getShader("sprite"); return;
if (spriteShader) {
spriteShader->use();
spriteShader->setMat4("u_view", camera_->getViewMatrix());
spriteShader->setMat4("u_projection", camera_->getProjectionMatrix());
} }
auto* coloredShader = shaderManager_.getShader("colored_quad"); for (const auto& [name, shaderPtr] : shaderManager_.getLoadedShaders()) {
if (coloredShader) { (void)name;
coloredShader->use(); Shader* shader = shaderPtr.Get();
coloredShader->setMat4("u_view", camera_->getViewMatrix()); if (!shader) {
coloredShader->setMat4("u_projection", camera_->getProjectionMatrix()); continue;
} }
auto* textShader = shaderManager_.getShader("text"); shader->use();
if (textShader) { shader->setMat4("u_view", camera_->getViewMatrix());
textShader->use(); shader->setMat4("u_projection", camera_->getProjectionMatrix());
textShader->setMat4("u_view", camera_->getViewMatrix());
textShader->setMat4("u_projection", camera_->getProjectionMatrix());
}
} }
} }
@@ -194,7 +537,9 @@ void Renderer::drawQuad(const Vec2& pos, const Size& size, float cr, float cg,
void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture) { void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture) {
Rect rect(pos.x, pos.y, size.width, size.height); Rect rect(pos.x, pos.y, size.width, size.height);
Quad quad = Quad::createTextured(rect, Rect(0, 0, size.width, size.height), Quad quad = Quad::createTextured(rect, Rect(0, 0, size.width, size.height),
Vec2(size.width, size.height)); Vec2(size.width, size.height),
1.0f, 1.0f, 1.0f, 1.0f,
shouldShrinkSubTextureUVs());
Transform2D transform = Transform2D::identity(); Transform2D transform = Transform2D::identity();
auto* shader = shaderManager_.getShader("sprite"); auto* shader = shaderManager_.getShader("sprite");
@@ -206,8 +551,7 @@ void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture)
} }
void Renderer::drawQuad(const Rect& rect, const Color& color) { void Renderer::drawQuad(const Rect& rect, const Color& color) {
drawQuad(Vec2(rect.left(), rect.top()), drawQuad(Vec2(rect.left(), rect.top()), Size(rect.width(), rect.height()),
Size(rect.width(), rect.height()),
color.r, color.g, color.b, color.a); color.r, color.g, color.b, color.a);
} }
@@ -215,11 +559,13 @@ void Renderer::drawSprite(const Vec2& pos, const Size& size, Ptr<Texture> textur
drawQuad(pos, size, texture); drawQuad(pos, size, texture);
} }
void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect, const Vec2& texSize, void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect,
Ptr<Texture> texture, const Color& color) { const Vec2& texSize, Ptr<Texture> texture,
const Color& color) {
Rect destRect(pos.x, pos.y, srcRect.width(), srcRect.height()); Rect destRect(pos.x, pos.y, srcRect.width(), srcRect.height());
Quad quad = Quad::createTextured(destRect, srcRect, texSize, Quad quad = Quad::createTextured(destRect, srcRect, texSize, color.r, color.g,
color.r, color.g, color.b, color.a); color.b, color.a,
shouldShrinkSubTextureUVs());
Transform2D transform = Transform2D::identity(); Transform2D transform = Transform2D::identity();
auto* shader = shaderManager_.getShader("sprite"); auto* shader = shaderManager_.getShader("sprite");
@@ -230,4 +576,4 @@ void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect, const Vec2& texS
batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal); batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal);
} }
} } // namespace frostbite2D

View File

@@ -84,6 +84,8 @@ Ptr<Texture> Texture::loadFromFile(const std::string& path) {
auto texture = Ptr<Texture>(new Texture(width, height, textureID)); auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->path_ = resolvedPath; texture->path_ = resolvedPath;
texture->channels_ = channels; texture->channels_ = channels;
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture; return texture;
} }
@@ -111,6 +113,8 @@ Ptr<Texture> Texture::createFromMemory(const uint8* data, int width, int height,
auto texture = Ptr<Texture>(new Texture(width, height, textureID)); auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->channels_ = channels; texture->channels_ = channels;
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture; return texture;
} }
@@ -129,12 +133,16 @@ Ptr<Texture> Texture::createEmpty(int width, int height) {
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
return Ptr<Texture>(new Texture(width, height, textureID)); auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture;
} }
void Texture::bind(uint32_t slot) { void Texture::bind(uint32_t slot, bool preferPixelArtSampling) {
glActiveTexture(GL_TEXTURE0 + slot); glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, textureID_); glBindTexture(GL_TEXTURE_2D, textureID_);
applySampling(preferPixelArtSampling);
} }
void Texture::unbind() { void Texture::unbind() {
@@ -150,9 +158,35 @@ void Texture::setWrapMode(uint32_t wrapS, uint32_t wrapT) {
void Texture::setFilterMode(uint32_t minFilter, uint32_t magFilter) { void Texture::setFilterMode(uint32_t minFilter, uint32_t magFilter) {
bind(); bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter); applyFilter(minFilter, magFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter); sampling_ = (minFilter == GL_NEAREST && magFilter == GL_NEAREST)
? TextureSampling::PixelArt
: TextureSampling::Linear;
unbind(); unbind();
} }
void Texture::setSampling(TextureSampling sampling) {
sampling_ = sampling;
appliedMinFilter_ = 0;
appliedMagFilter_ = 0;
}
void Texture::applyFilter(uint32_t minFilter, uint32_t magFilter) {
if (appliedMinFilter_ == minFilter && appliedMagFilter_ == magFilter) {
return;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
appliedMinFilter_ = minFilter;
appliedMagFilter_ = magFilter;
}
void Texture::applySampling(bool preferPixelArtSampling) {
bool useNearest =
sampling_ == TextureSampling::PixelArt && preferPixelArtSampling;
applyFilter(useNearest ? GL_NEAREST : GL_LINEAR,
useNearest ? GL_NEAREST : GL_LINEAR);
}
} }

View File

@@ -95,6 +95,42 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
} }
effectMap_[effect.id] = std::move(effect); effectMap_[effect.id] = std::move(effect);
} else if (strcmp(name, "AMBIENT") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip invalid AMBIENT entry (empty ID)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip AMBIENT entry with empty FILE: %s",
idAttr->value());
continue;
}
AudioAmbient ambient;
ambient.id = idAttr->value();
ambient.file = fileAttr->value();
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
ambient.loopDelay = std::stoi(loopDelayAttr->value());
}
rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE");
if (loopDelayRangeAttr) {
ambient.loopDelayRange = std::stoi(loopDelayRangeAttr->value());
}
ambientMap_[ambient.id] = std::move(ambient);
} else if (strcmp(name, "MUSIC") == 0) { } else if (strcmp(name, "MUSIC") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID"); rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE"); rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
@@ -163,14 +199,16 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) {
loaded_ = true; loaded_ = true;
SDL_Log("AudioDatabase: 加载完成 - %zu 个音效, %zu 个音乐, %zu 个随机组", SDL_Log("AudioDatabase: loaded - %zu effects, %zu ambients, %zu musics, %zu random groups",
effectMap_.size(), musicMap_.size(), randomMap_.size()); effectMap_.size(), ambientMap_.size(), musicMap_.size(),
randomMap_.size());
return true; return true;
} }
void AudioDatabase::clear() { void AudioDatabase::clear() {
effectMap_.clear(); effectMap_.clear();
ambientMap_.clear();
musicMap_.clear(); musicMap_.clear();
randomMap_.clear(); randomMap_.clear();
loaded_ = false; loaded_ = false;
@@ -194,6 +232,13 @@ std::optional<AudioEntry> AudioDatabase::get(const std::string& id) {
return entry; return entry;
} }
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
entry.type = AudioEntryType::Ambient;
entry.ambient = &ambientIt->second;
return entry;
}
auto musicIt = musicMap_.find(id); auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) { if (musicIt != musicMap_.end()) {
entry.type = AudioEntryType::Music; entry.type = AudioEntryType::Music;
@@ -235,6 +280,10 @@ std::optional<std::string> AudioDatabase::getFilePath(const std::string& id) {
auto soundIdOpt = getSound(id); auto soundIdOpt = getSound(id);
if (!soundIdOpt) { if (!soundIdOpt) {
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
return ambientIt->second.file;
}
auto musicIt = musicMap_.find(id); auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) { if (musicIt != musicMap_.end()) {
return musicIt->second.file; return musicIt->second.file;
@@ -263,6 +312,18 @@ std::optional<const AudioEffect*> AudioDatabase::getEffect(const std::string& id
return std::nullopt; return std::nullopt;
} }
std::optional<const AudioAmbient*> AudioDatabase::getAmbient(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = ambientMap_.find(id);
if (it != ambientMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) { std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) {
if (!loaded_) { if (!loaded_) {
return std::nullopt; return std::nullopt;
@@ -291,6 +352,10 @@ size_t AudioDatabase::effectCount() const {
return effectMap_.size(); return effectMap_.size();
} }
size_t AudioDatabase::ambientCount() const {
return ambientMap_.size();
}
size_t AudioDatabase::musicCount() const { size_t AudioDatabase::musicCount() const {
return musicMap_.size(); return musicMap_.size();
} }
@@ -364,7 +429,8 @@ bool AudioDatabase::has(const std::string& id) {
if (!loaded_) { if (!loaded_) {
return false; return false;
} }
return effectMap_.count(id) > 0 || musicMap_.count(id) > 0 || randomMap_.count(id) > 0; return effectMap_.count(id) > 0 || ambientMap_.count(id) > 0 ||
musicMap_.count(id) > 0 || randomMap_.count(id) > 0;
} }
std::string AudioDatabase::filePath(const std::string& id) { std::string AudioDatabase::filePath(const std::string& id) {
@@ -395,6 +461,9 @@ AudioEntryType AudioDatabase::typeOf(const std::string& id) {
if (effectMap_.count(id) > 0) { if (effectMap_.count(id) > 0) {
return AudioEntryType::Effect; return AudioEntryType::Effect;
} }
if (ambientMap_.count(id) > 0) {
return AudioEntryType::Ambient;
}
if (musicMap_.count(id) > 0) { if (musicMap_.count(id) > 0) {
return AudioEntryType::Music; return AudioEntryType::Music;
} }

View File

@@ -43,6 +43,21 @@ bool Scene::OnEvent(const Event& event) {
return false; return false;
} }
void Scene::SetRenderStyleProfile(RenderStyleProfileId profile) {
renderStyleProfileOverride_ = profile;
hasRenderStyleProfileOverride_ = true;
}
void Scene::ClearRenderStyleProfileOverride() {
hasRenderStyleProfileOverride_ = false;
}
RenderStyleProfileId Scene::ResolveRenderStyleProfile(
RenderStyleProfileId defaultProfile) const {
return hasRenderStyleProfileOverride_ ? renderStyleProfileOverride_
: defaultProfile;
}
bool Scene::dispatchToChildren(const Event& event) { bool Scene::dispatchToChildren(const Event& event) {
if (event.isPropagationStopped()) { if (event.isPropagationStopped()) {
return false; return false;

View File

@@ -5,6 +5,18 @@
namespace frostbite2D { namespace frostbite2D {
namespace {
RenderStyleProfileId resolveSceneRenderStyleProfile(const Scene* scene,
const Renderer& renderer) {
if (!scene) {
return renderer.getDefaultRenderStyleProfile();
}
return scene->ResolveRenderStyleProfile(renderer.getDefaultRenderStyleProfile());
}
} // namespace
SceneManager& SceneManager::get() { SceneManager& SceneManager::get() {
static SceneManager instance; static SceneManager instance;
return instance; return instance;
@@ -123,13 +135,23 @@ void SceneManager::ClearAll() {
void SceneManager::Update(float deltaTime) { void SceneManager::Update(float deltaTime) {
if (!sceneStack_.empty()) { if (!sceneStack_.empty()) {
Renderer& renderer = Renderer::get();
RenderStyleProfileId profile =
resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer);
renderer.applyRenderStyleToCamera(renderer.getCamera(), profile,
RenderStyleLayerRole::World);
sceneStack_.back()->Update(deltaTime); sceneStack_.back()->Update(deltaTime);
} }
} }
void SceneManager::UpdateUI(float deltaTime) { void SceneManager::UpdateUI(float deltaTime) {
Renderer& renderer = Renderer::get();
for (auto& scene : uiSceneStack_) { for (auto& scene : uiSceneStack_) {
if (scene) { if (scene) {
RenderStyleProfileId profile =
resolveSceneRenderStyleProfile(scene.Get(), renderer);
renderer.applyRenderStyleToCamera(scene->GetCamera(), profile,
RenderStyleLayerRole::UI);
scene->Update(deltaTime); scene->Update(deltaTime);
} }
} }
@@ -138,8 +160,15 @@ void SceneManager::UpdateUI(float deltaTime) {
void SceneManager::Render() { void SceneManager::Render() {
Renderer& renderer = Renderer::get(); Renderer& renderer = Renderer::get();
Camera* worldCamera = renderer.getCamera(); Camera* worldCamera = renderer.getCamera();
RenderStyleProfileId worldProfile = renderer.getDefaultRenderStyleProfile();
if (!sceneStack_.empty()) { if (!sceneStack_.empty()) {
worldProfile =
resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer);
renderer.applyRenderStyleToCamera(worldCamera, worldProfile,
RenderStyleLayerRole::World);
renderer.setActiveRenderStyleProfile(worldProfile,
RenderStyleLayerRole::World);
sceneStack_.back()->Render(); sceneStack_.back()->Render();
} }
@@ -148,6 +177,11 @@ void SceneManager::Render() {
continue; continue;
} }
RenderStyleProfileId uiProfile =
resolveSceneRenderStyleProfile(scene.Get(), renderer);
renderer.applyRenderStyleToCamera(scene->GetCamera(), uiProfile,
RenderStyleLayerRole::UI);
renderer.setActiveRenderStyleProfile(uiProfile, RenderStyleLayerRole::UI);
renderer.setCamera(scene->GetCamera()); renderer.setCamera(scene->GetCamera());
scene->Render(); scene->Render();
} }
@@ -155,6 +189,7 @@ void SceneManager::Render() {
if (renderer.getCamera() != worldCamera) { if (renderer.getCamera() != worldCamera) {
renderer.setCamera(worldCamera); renderer.setCamera(worldCamera);
} }
renderer.setActiveRenderStyleProfile(worldProfile, RenderStyleLayerRole::World);
} }
bool SceneManager::DispatchEvent(const Event& event) { bool SceneManager::DispatchEvent(const Event& event) {

View File

@@ -1,8 +1,11 @@
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/scene/ui_scene.h> #include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D { namespace frostbite2D {
UIScene::UIScene() { UIScene::UIScene() {
Renderer& renderer = Renderer::get();
camera_.setViewport(renderer.getVirtualWidth(), renderer.getVirtualHeight());
camera_.setFlipY(true); camera_.setFlipY(true);
camera_.setZoom(1.0f); camera_.setZoom(1.0f);
camera_.setPosition(Vec2::Zero()); camera_.setPosition(Vec2::Zero());

View File

@@ -0,0 +1,48 @@
#pragma once
#include <frostbite2D/audio/music.h>
#include <frostbite2D/audio/sound.h>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace frostbite2D {
struct MapAudioTrack {
std::string id;
std::string path;
};
struct MapAudioState {
std::optional<MapAudioTrack> bgmTrack;
std::vector<MapAudioTrack> ambientLoops;
};
class MapAudioController {
public:
static MapAudioController& get();
void ApplyMapAudio(const MapAudioState& state);
void ClearMapAudio();
private:
struct ActiveAmbientLoop {
MapAudioTrack track;
RefPtr<Sound> sound;
int channel = -1;
};
MapAudioController() = default;
void ApplyBgmTrack(const std::optional<MapAudioTrack>& bgmTrack);
void ApplyAmbientLoops(const std::vector<MapAudioTrack>& ambientLoops);
void StopAmbientLoop(ActiveAmbientLoop& loop);
static std::string NormalizeTrackKey(const MapAudioTrack& track);
std::optional<MapAudioTrack> currentBgmTrack_;
RefPtr<Music> currentBgmMusic_;
std::unordered_map<std::string, ActiveAmbientLoop> activeAmbientLoops_;
};
} // namespace frostbite2D

View File

@@ -45,7 +45,7 @@ private:
bool debugEnabled_ = false; bool debugEnabled_ = false;
bool initialized_ = false; bool initialized_ = false;
float zoom_ = 1.2f; float zoom_ = 1.0f;
float debugMoveSpeed_ = 800.0f; float debugMoveSpeed_ = 800.0f;
}; };

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "common/math/GameMath.h"
#include <frostbite2D/types/type_math.h> #include <frostbite2D/types/type_math.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -81,8 +82,8 @@ struct CharacterMotor {
float gravity = 1600.0f; float gravity = 1600.0f;
void SetGroundPosition(const Vec2& groundPosition) { void SetGroundPosition(const Vec2& groundPosition) {
position.x = RoundWorldCoordinate(groundPosition.x); position.x = gameMath::RoundWorldCoordinate(groundPosition.x);
position.y = RoundWorldCoordinate(groundPosition.y); position.y = gameMath::RoundWorldCoordinate(groundPosition.y);
positionRemainder_ = Vec2::Zero(); positionRemainder_ = Vec2::Zero();
} }
@@ -167,10 +168,6 @@ struct CharacterMotor {
} }
private: private:
static int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
static int32 ConsumeWholeUnits(float& remainder) { static int32 ConsumeWholeUnits(float& remainder) {
if (remainder >= 1.0f) { if (remainder >= 1.0f) {
int32 wholeUnits = static_cast<int32>(std::floor(remainder)); int32 wholeUnits = static_cast<int32>(std::floor(remainder));

View File

@@ -3,6 +3,7 @@
#include "character/CharacterDataLoader.h" #include "character/CharacterDataLoader.h"
#include "character/CharacterEquipmentManager.h" #include "character/CharacterEquipmentManager.h"
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/animation/animation.h> #include <frostbite2D/animation/animation.h>
#include <frostbite2D/base/RefPtr.h> #include <frostbite2D/base/RefPtr.h>
#include <functional> #include <functional>
@@ -21,9 +22,20 @@ public:
using ActionFrameFlagCallback = std::function<void(int)>; using ActionFrameFlagCallback = std::function<void(int)>;
using ActionEndCallback = std::function<void()>; using ActionEndCallback = std::function<void()>;
struct CompositeFrameInfo {
bool valid = false;
Rect localBounds;
Vec2 originInTexture = Vec2::Zero();
Vec2 groundAnchorInTexture = Vec2::Zero();
int width = 1;
int height = 1;
};
bool Init(CharacterObject* parent, bool Init(CharacterObject* parent,
const character::CharacterConfig& config, const character::CharacterConfig& config,
const CharacterEquipmentManager& equipmentManager); const CharacterEquipmentManager& equipmentManager);
void Update(float deltaTime) override;
void Render() override;
bool SetAction(const std::string& actionName); bool SetAction(const std::string& actionName);
void SetDirection(int direction); void SetDirection(int direction);
@@ -39,6 +51,13 @@ public:
animation::AniFrame GetCurrentFrameInfo() const; animation::AniFrame GetCurrentFrameInfo() const;
void SetActionFrameFlagCallback(ActionFrameFlagCallback callback); void SetActionFrameFlagCallback(ActionFrameFlagCallback callback);
void SetActionEndCallback(ActionEndCallback callback); void SetActionEndCallback(ActionEndCallback callback);
bool EnsureCompositeTextureReady();
bool HasCompositeTexture() const;
Ptr<Texture> GetCompositeTexture() const;
Vec2 GetCompositeTextureSize() const;
Vec2 GetCompositeOriginInTexture() const;
Vec2 GetCompositeGroundAnchorInTexture() const;
uint64 GetCompositeVersion() const { return compositeVersion_; }
private: private:
static std::string FormatImgPath(std::string path, Animation::ReplaceData data); static std::string FormatImgPath(std::string path, Animation::ReplaceData data);
@@ -46,6 +65,15 @@ private:
Animation* GetCurrentPrimaryAnimation() const; Animation* GetCurrentPrimaryAnimation() const;
void RefreshRuntimeCallbacks(); void RefreshRuntimeCallbacks();
bool shouldSkipMissingAnimation(const std::string& aniPath); bool shouldSkipMissingAnimation(const std::string& aniPath);
void BuildCompositeActionFrames();
CompositeFrameInfo ComputeCompositeFrameInfo(const std::vector<RefPtr<Animation>>& animations,
int direction) const;
CompositeFrameInfo GetCurrentCompositeFrameInfo() const;
uint64 CaptureCurrentRenderSignature() const;
void MarkCompositeDirty();
void UpdateCompositeCamera();
void RefreshCompositeTextureIfNeeded();
void RenderCurrentActionToCompositeCanvas();
void CreateAnimationBySlot(const std::string& actionName, void CreateAnimationBySlot(const std::string& actionName,
const std::string& slotName, const std::string& slotName,
@@ -61,6 +89,11 @@ private:
ActionEndCallback actionEndCallback_; ActionEndCallback actionEndCallback_;
std::unordered_set<std::string> missingAnimationPaths_; std::unordered_set<std::string> missingAnimationPaths_;
size_t skippedMissingAnimationCount_ = 0; size_t skippedMissingAnimationCount_ = 0;
RefPtr<CanvasActor> compositeCanvas_ = nullptr;
std::map<std::string, CompositeFrameInfo> compositeFrameInfoByAction_;
bool compositeDirty_ = true;
uint64 compositeVersion_ = 0;
uint64 lastCompositeSignature_ = 0;
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -5,6 +5,7 @@
#include "character/CharacterDataLoader.h" #include "character/CharacterDataLoader.h"
#include "character/CharacterEquipmentManager.h" #include "character/CharacterEquipmentManager.h"
#include "character/CharacterInputRouter.h" #include "character/CharacterInputRouter.h"
#include "character/CharacterShadowActor.h"
#include "character/CharacterStateMachine.h" #include "character/CharacterStateMachine.h"
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
#include <frostbite2D/event/event.h> #include <frostbite2D/event/event.h>
@@ -31,7 +32,7 @@ class GameMap;
class CharacterObject : public Actor { class CharacterObject : public Actor {
public: public:
CharacterObject() = default; CharacterObject() = default;
~CharacterObject() override = default; ~CharacterObject() override;
/// @brief 二段初始化角色。 /// @brief 二段初始化角色。
/// @param jobId 职业 id用来加载职业配置、动作定义和动画资源。 /// @param jobId 职业 id用来加载职业配置、动作定义和动画资源。
@@ -77,7 +78,10 @@ public:
/// ///
/// 顺序固定为:命令缓冲推进 -> 采样实时输入 -> 生成意图 -> 状态机更新 /// 顺序固定为:命令缓冲推进 -> 采样实时输入 -> 生成意图 -> 状态机更新
/// -> motor 推进 -> 投影到 Actor 坐标。 /// -> motor 推进 -> 投影到 Actor 坐标。
void Update(float deltaTime) override;
void OnUpdate(float deltaTime) override; void OnUpdate(float deltaTime) override;
void OnAdded(Actor* parent) override;
void PrepareRenderFrame();
/// @brief 统一输入入口。 /// @brief 统一输入入口。
/// ///
@@ -141,6 +145,12 @@ public:
int GetCurrentAnimationFrameCount() const; int GetCurrentAnimationFrameCount() const;
float GetCurrentAnimationProgressNormalized() const; float GetCurrentAnimationProgressNormalized() const;
animation::AniFrame GetCurrentAnimationFrameInfo() const; animation::AniFrame GetCurrentAnimationFrameInfo() const;
bool HasCompositeTexture() const;
Ptr<Texture> GetCompositeTexture() const;
Vec2 GetCompositeTextureSize() const;
Vec2 GetCompositeOriginInTexture() const;
Vec2 GetCompositeGroundAnchorInTexture() const;
uint64 GetCompositeTextureVersion() const;
/// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。 /// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。
void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback); void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback);
@@ -176,6 +186,9 @@ private:
void ApplyMapMovementConstraints(const CharacterWorldPosition& previousPosition); void ApplyMapMovementConstraints(const CharacterWorldPosition& previousPosition);
void QueueMapTransitionIfNeeded(); void QueueMapTransitionIfNeeded();
void SyncActorPositionFromWorld(); void SyncActorPositionFromWorld();
void SyncShadowAttachment();
void SyncShadowPresentation();
void DetachShadowActor();
bool SetActionStrict(const std::string& actionName, bool SetActionStrict(const std::string& actionName,
const char* phase, const char* phase,
const std::string& requestedActionId); const std::string& requestedActionId);
@@ -232,6 +245,10 @@ private:
/// 真正负责播放角色分层动画的 Actor 子节点。 /// 真正负责播放角色分层动画的 Actor 子节点。
RefPtr<CharacterAnimation> animationManager_ = nullptr; RefPtr<CharacterAnimation> animationManager_ = nullptr;
/// ???? normal ???????????
RefPtr<CharacterShadowActor> shadowActor_ = nullptr;
GameMap* shadowAttachedMap_ = nullptr;
/// 缓存上一帧 deltaTime方便状态和脚本系统查询。 /// 缓存上一帧 deltaTime方便状态和脚本系统查询。
float lastDeltaTime_ = 0.0f; float lastDeltaTime_ = 0.0f;

View File

@@ -0,0 +1,32 @@
#pragma once
#include <frostbite2D/2d/sprite.h>
namespace frostbite2D {
struct CharacterShadowFrameSnapshot {
bool visible = false;
Ptr<Texture> texture = nullptr;
Vec2 textureSize = Vec2::Zero();
Vec2 groundAnchorInTexture = Vec2::Zero();
Vec2 groundScreenPosition = Vec2::Zero();
int zOrder = 0;
uint64 textureVersion = 0;
};
class CharacterShadowActor : public Sprite {
public:
CharacterShadowActor();
void ApplyFrameSnapshot(const CharacterShadowFrameSnapshot& snapshot);
private:
void ApplyTextureState(const CharacterShadowFrameSnapshot& snapshot);
Ptr<Texture> appliedTexture_ = nullptr;
Vec2 appliedTextureSize_ = Vec2::Zero();
Vec2 appliedGroundAnchorInTexture_ = Vec2::Zero();
uint64 appliedTextureVersion_ = 0;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,20 @@
#pragma once
#include <frostbite2D/2d/sprite.h>
namespace frostbite2D {
class InteractionHighlightSprite : public Sprite {
public:
InteractionHighlightSprite();
void SetGroundAnchorInTexture(const Vec2& groundAnchorInTexture);
protected:
void ConfigureShader(Shader* shader) const override;
private:
Vec2 texelSize_ = Vec2::One();
};
} // namespace frostbite2D

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
namespace frostbite2D { namespace frostbite2D {
class CharacterObject; class CharacterObject;
@@ -33,6 +32,7 @@ public:
private: private:
void initOverlay(); void initOverlay();
void updateOverlay(); void updateOverlay();
void updateMapDebugHighlight();
void setOverlayVisible(bool visible); void setOverlayVisible(bool visible);
GameDebugActor(); GameDebugActor();
@@ -42,6 +42,7 @@ private:
CharacterObject* trackedCharacter_ = nullptr; CharacterObject* trackedCharacter_ = nullptr;
RefPtr<NineSliceActor> background_; RefPtr<NineSliceActor> background_;
RefPtr<TextSprite> coordText_; RefPtr<TextSprite> coordText_;
RefPtr<TextSprite> actionText_;
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -0,0 +1,19 @@
#pragma once
#include <frostbite2D/types/type_math.h>
#include <vector>
namespace frostbite2D::gameMath {
int32 RoundWorldCoordinate(float value);
Vec2 MakeIntegerWorldPoint(int x, int y);
Vec3 MakeIntegerWorldPosition(int x, int y, int z);
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end);
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point);
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y);
std::vector<Vec2> BuildRectPolygon(const Rect& rect);
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon);
} // namespace frostbite2D::gameMath

View File

@@ -30,9 +30,11 @@ public:
/// 地图进入场景后的初始化入口,目前主要负责音乐播放。 /// 地图进入场景后的初始化入口,目前主要负责音乐播放。
void Enter(); void Enter();
void Update(float deltaTime) override; void Update(float deltaTime) override;
void Render() override;
/// 将运行时对象挂到 normal 层,并按 y 值设置基础排序。 /// 将运行时对象挂到 normal 层,并按 y 值设置基础排序。
void AddObject(RefPtr<Actor> object); void AddObject(RefPtr<Actor> object);
void AddObjectToLayer(const std::string& layerName, RefPtr<Actor> object);
/// 返回地图推荐的默认相机关注点。 /// 返回地图推荐的默认相机关注点。
Vec2 GetDefaultCameraFocus() const; Vec2 GetDefaultCameraFocus() const;
@@ -49,6 +51,7 @@ public:
size_t FindMoveAreaIndex(const Vec3& curPos) const; size_t FindMoveAreaIndex(const Vec3& curPos) const;
const std::vector<MapMoveArea>& GetMoveAreaInfo() const; const std::vector<MapMoveArea>& GetMoveAreaInfo() const;
size_t GetMoveAreaCount() const { return moveArea_.size(); } size_t GetMoveAreaCount() const { return moveArea_.size(); }
game::MoveAreaBounds GetMovablePositionBounds(size_t index) const;
Rect GetMovablePositionArea(size_t index) const; Rect GetMovablePositionArea(size_t index) const;
const std::string& GetMapPath() const { return mapConfig_.mapPath; } const std::string& GetMapPath() const { return mapConfig_.mapPath; }
void SetDebugHighlightedMoveAreaIndex(size_t index); void SetDebugHighlightedMoveAreaIndex(size_t index);
@@ -74,6 +77,7 @@ private:
void InitMoveArea(); void InitMoveArea();
/// 将各层转换到屏幕空间;远景层在这里做视差滚动。 /// 将各层转换到屏幕空间;远景层在这里做视差滚动。
void updateLayerPositions(const Vec2& cameraFocus); void updateLayerPositions(const Vec2& cameraFocus);
void PrepareRuntimeObjectsForRender();
/// 原始地图配置,作为运行时装配地图内容的输入。 /// 原始地图配置,作为运行时装配地图内容的输入。
game::MapConfig mapConfig_; game::MapConfig mapConfig_;
@@ -89,9 +93,9 @@ private:
int backgroundRepeatWidth_ = 0; int backgroundRepeatWidth_ = 0;
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。 /// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
int mapOffsetY_ = 0; int mapOffsetY_ = 0;
bool debugMode_ = true; bool debugMode_ = false;
/// 硬编码调试开关:关闭后忽略可行走区域检测,允许角色自由移动。 /// 硬编码调试开关:关闭后忽略可行走区域检测,允许角色自由移动。
bool movableAreaCheckEnabled_ = false; bool movableAreaCheckEnabled_ = true;
/// 当前地图正在播放的背景音乐。 /// 当前地图正在播放的背景音乐。
Ptr<Music> currentMusic_; Ptr<Music> currentMusic_;
}; };

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/2d/actor.h> #include <frostbite2D/2d/actor.h>
#include <vector> #include <vector>
@@ -18,15 +19,29 @@ public:
void AddObject(RefPtr<Actor> obj); void AddObject(RefPtr<Actor> obj);
private: private:
void EnsureDebugOverlayCanvases();
void RefreshFeasibleAreaOverlay();
void RefreshMoveAreaOverlay();
Rect ComputeFeasibleAreaOverlayBounds() const;
Rect ComputeMoveAreaOverlayBounds() const;
void DrawFeasibleAreaOverlay() const;
void DrawMoveAreaOverlay() const;
struct DebugMoveAreaInfo { struct DebugMoveAreaInfo {
Rect rect; Rect rect;
size_t index = kInvalidMoveAreaIndex; size_t index = kInvalidMoveAreaIndex;
}; };
RefPtr<CanvasActor> feasibleAreaOverlayCanvas_ = nullptr;
RefPtr<CanvasActor> moveAreaOverlayCanvas_ = nullptr;
std::vector<Vec2> feasibleAreaPolygon_; std::vector<Vec2> feasibleAreaPolygon_;
std::vector<Rect> feasibleAreaFillRects_; std::vector<Rect> feasibleAreaFillRects_;
std::vector<DebugMoveAreaInfo> moveAreaInfoList_; std::vector<DebugMoveAreaInfo> moveAreaInfoList_;
size_t highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex; size_t highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex;
Rect feasibleAreaOverlayBounds_;
Rect moveAreaOverlayBounds_;
bool feasibleAreaOverlayDirty_ = true;
bool moveAreaOverlayDirty_ = true;
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -0,0 +1,56 @@
#pragma once
#include "npc/NpcDataLoader.h"
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/animation/animation.h>
#include <string>
namespace frostbite2D {
class NpcAnimation : public Actor {
public:
struct CompositeFrameInfo {
bool valid = false;
Rect localBounds;
Vec2 originInTexture = Vec2::Zero();
Vec2 groundAnchorInTexture = Vec2::Zero();
int width = 1;
int height = 1;
};
bool Init(const npc::NpcConfig& config);
void Update(float deltaTime) override;
void Render() override;
void SetDirection(int direction);
bool IsReady() const { return displayAnimation_ != nullptr; }
bool IsAnimationFinished() const;
const std::string& GetAnimationPath() const { return animationPath_; }
bool GetDisplayLocalBounds(Rect& outBounds) const;
bool EnsureCompositeTextureReady();
bool HasCompositeTexture() const;
Ptr<Texture> GetCompositeTexture() const;
Vec2 GetCompositeTextureSize() const;
Vec2 GetCompositeGroundAnchorInTexture() const;
uint64 GetCompositeVersion() const { return compositeVersion_; }
private:
CompositeFrameInfo ComputeCompositeFrameInfo(int direction) const;
CompositeFrameInfo GetCurrentCompositeFrameInfo() const { return compositeFrameInfo_; }
uint64 CaptureCurrentRenderSignature() const;
void MarkCompositeDirty();
void UpdateCompositeCamera();
void RefreshCompositeTextureIfNeeded();
void RenderCurrentAnimationToCompositeCanvas();
RefPtr<Animation> displayAnimation_ = nullptr;
RefPtr<CanvasActor> compositeCanvas_ = nullptr;
CompositeFrameInfo compositeFrameInfo_;
std::string animationPath_;
int direction_ = 1;
bool compositeDirty_ = true;
uint64 compositeVersion_ = 0;
uint64 lastCompositeSignature_ = 0;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,20 @@
#pragma once
#include <map>
#include <optional>
#include <string>
namespace frostbite2D::npc {
struct NpcConfig {
int id = -1;
std::string path;
std::string name;
std::string fieldName;
std::string fieldAnimationPath;
};
bool loadNpcIndex(std::map<int, std::string>& outIndex);
std::optional<NpcConfig> loadNpcConfig(int npcId);
} // namespace frostbite2D::npc

View File

@@ -0,0 +1,68 @@
#pragma once
#include "character/CharacterActionTypes.h"
#include "common/InteractionHighlightSprite.h"
#include "npc/NpcAnimation.h"
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/text_sprite.h>
#include <array>
#include <optional>
#include <string>
namespace frostbite2D {
class NpcObject : public Actor {
public:
NpcObject() = default;
~NpcObject() override = default;
bool Construction(int npcId);
void SetDirection(int direction);
void SetNpcPosition(const Vec2& pos);
void SetWorldPosition(const CharacterWorldPosition& pos);
void SetInteractable(bool interactable);
void SetInteractionHighlighted(bool highlighted);
void BeginInteract();
void EndInteract();
bool CanInteract() const;
bool IsInteracting() const { return interacting_; }
bool IsInteractionHighlighted() const { return interactionHighlighted_; }
int GetNpcId() const { return npcId_; }
int GetDirection() const { return direction_; }
const std::string& GetName() const;
const std::string& GetFieldName() const;
const std::string& GetDisplayName() const;
const CharacterWorldPosition& GetWorldPosition() const { return worldPosition_; }
bool IsAnimationFinished() const;
const npc::NpcConfig* GetConfig() const {
return config_ ? &config_.value() : nullptr;
}
void Update(float deltaTime) override;
void Render() override;
private:
void EnsureInteractionHighlight();
void EnsureNameLabel();
void RefreshNameLabel();
void SyncInteractionHighlight();
void SyncActorPositionFromWorld();
int npcId_ = -1;
int direction_ = 1;
CharacterWorldPosition worldPosition_;
std::optional<npc::NpcConfig> config_;
RefPtr<NpcAnimation> animation_ = nullptr;
RefPtr<InteractionHighlightSprite> interactionHighlight_ = nullptr;
std::array<RefPtr<TextSprite>, 8> nameOutlineLabels_;
RefPtr<TextSprite> nameLabel_ = nullptr;
bool interactable_ = true;
bool interacting_ = false;
bool interactionHighlighted_ = false;
uint64 syncedHighlightCompositeVersion_ = 0;
};
} // namespace frostbite2D

View File

@@ -3,6 +3,7 @@
#include "camera/GameCameraController.h" #include "camera/GameCameraController.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "map/GameMap.h" #include "map/GameMap.h"
#include "npc/NpcObject.h"
#include "scene/GameDebugUIScene.h" #include "scene/GameDebugUIScene.h"
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
@@ -20,6 +21,7 @@ public:
private: private:
GameCameraController cameraController_; GameCameraController cameraController_;
RefPtr<CharacterObject> character_; RefPtr<CharacterObject> character_;
RefPtr<NpcObject> npc_;
RefPtr<GameDebugUIScene> debugScene_; RefPtr<GameDebugUIScene> debugScene_;
bool initialized_ = false; bool initialized_ = false;
RefPtr<GameMap> map_; RefPtr<GameMap> map_;

View File

@@ -25,6 +25,7 @@ public:
int GetCurAreaIndex() const { return curMapIndex_; } int GetCurAreaIndex() const { return curMapIndex_; }
RefPtr<GameMap> GetArea(int index) const; RefPtr<GameMap> GetArea(int index) const;
RefPtr<GameMap> GetCurrentArea() const;
const std::vector<MapInfo>& GetAreas() const { return mapList_; } const std::vector<MapInfo>& GetAreas() const { return mapList_; }
void Update(float deltaTime) override; void Update(float deltaTime) override;

View File

@@ -7,6 +7,11 @@
namespace frostbite2D { namespace frostbite2D {
class Actor;
class CharacterObject;
class GameDebugUIScene;
class GameMap;
class GameWorld : public Scene { class GameWorld : public Scene {
public: public:
GameWorld(); GameWorld();
@@ -19,6 +24,13 @@ public:
void AddCharacter(RefPtr<Actor> actor, int townId); void AddCharacter(RefPtr<Actor> actor, int townId);
void MoveCharacter(RefPtr<Actor> actor, int townId, int area); void MoveCharacter(RefPtr<Actor> actor, int townId, int area);
void RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area); void RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area);
void UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex);
bool ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const;
Actor* GetMainActor() const;
CharacterObject* GetMainCharacter() const;
GameTown* GetCurrentTown() const;
GameMap* GetCurrentMap() const;
static GameWorld* GetWorld(); static GameWorld* GetWorld();
@@ -29,13 +41,25 @@ private:
int area = -1; int area = -1;
}; };
struct SuppressedMoveArea {
GameMap* map = nullptr;
size_t moveAreaIndex = GameMap::kInvalidMoveAreaIndex;
};
bool InitWorld(); bool InitWorld();
bool InitMainCharacter(int townId);
void ProcessPendingCharacterMove(); void ProcessPendingCharacterMove();
void EnsureDebugScene();
void RefreshDebugContext();
void SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex);
void ClearSuppressedMoveArea();
std::map<int, std::string> townPathMap_; std::map<int, std::string> townPathMap_;
std::map<int, RefPtr<GameTown>> townMap_; std::map<int, RefPtr<GameTown>> townMap_;
RefPtr<Actor> mainActor_; RefPtr<Actor> mainActor_;
RefPtr<GameDebugUIScene> debugScene_;
std::optional<PendingCharacterMove> pendingCharacterMove_; std::optional<PendingCharacterMove> pendingCharacterMove_;
std::optional<SuppressedMoveArea> suppressedMoveArea_;
int curTown_ = -1; int curTown_ = -1;
bool initialized_ = false; bool initialized_ = false;
}; };

View File

@@ -0,0 +1,172 @@
#include "audio/MapAudioController.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <unordered_map>
namespace frostbite2D {
namespace {
std::string NormalizeAudioPath(const std::string& path) {
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalized;
}
} // namespace
MapAudioController& MapAudioController::get() {
static MapAudioController instance;
return instance;
}
void MapAudioController::ApplyMapAudio(const MapAudioState& state) {
ApplyBgmTrack(state.bgmTrack);
ApplyAmbientLoops(state.ambientLoops);
}
void MapAudioController::ClearMapAudio() {
if (currentBgmMusic_) {
currentBgmMusic_->stop();
currentBgmMusic_.Reset();
}
currentBgmTrack_.reset();
for (auto& [_, loop] : activeAmbientLoops_) {
StopAmbientLoop(loop);
}
activeAmbientLoops_.clear();
}
void MapAudioController::ApplyBgmTrack(
const std::optional<MapAudioTrack>& bgmTrack) {
if (!bgmTrack || bgmTrack->path.empty()) {
if (currentBgmMusic_) {
currentBgmMusic_->stop();
currentBgmMusic_.Reset();
}
currentBgmTrack_.reset();
return;
}
const std::string desiredKey = NormalizeTrackKey(*bgmTrack);
const bool sameTrack =
currentBgmTrack_ && NormalizeTrackKey(*currentBgmTrack_) == desiredKey;
if (sameTrack && Music::isPathPlaying(bgmTrack->path)) {
return;
}
RefPtr<Music> music = Music::loadFromPath(bgmTrack->path);
if (!music) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load bgm %s",
bgmTrack->path.c_str());
return;
}
if (!music->play(-1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play bgm %s",
bgmTrack->path.c_str());
return;
}
currentBgmMusic_ = music;
currentBgmTrack_ = *bgmTrack;
}
void MapAudioController::ApplyAmbientLoops(
const std::vector<MapAudioTrack>& ambientLoops) {
std::unordered_map<std::string, MapAudioTrack> desiredLoops;
desiredLoops.reserve(ambientLoops.size());
for (const auto& loop : ambientLoops) {
if (loop.path.empty()) {
continue;
}
desiredLoops.emplace(NormalizeTrackKey(loop), loop);
}
for (auto it = activeAmbientLoops_.begin(); it != activeAmbientLoops_.end();) {
if (desiredLoops.find(it->first) != desiredLoops.end()) {
++it;
continue;
}
StopAmbientLoop(it->second);
it = activeAmbientLoops_.erase(it);
}
for (const auto& [key, track] : desiredLoops) {
auto activeIt = activeAmbientLoops_.find(key);
if (activeIt != activeAmbientLoops_.end()) {
ActiveAmbientLoop& loop = activeIt->second;
if (NormalizeAudioPath(loop.track.path) == NormalizeAudioPath(track.path) &&
Sound::isPlaying(loop.channel)) {
continue;
}
StopAmbientLoop(loop);
loop.track = track;
if (!loop.sound ||
NormalizeAudioPath(loop.sound->getPath()) !=
NormalizeAudioPath(track.path)) {
loop.sound = Sound::loadFromPath(track.path);
}
if (!loop.sound) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load ambient %s",
track.path.c_str());
continue;
}
int channel = loop.sound->playLoop();
if (channel == -1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play ambient %s",
track.path.c_str());
continue;
}
loop.channel = channel;
continue;
}
ActiveAmbientLoop loop;
loop.track = track;
loop.sound = Sound::loadFromPath(track.path);
if (!loop.sound) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to load ambient %s",
track.path.c_str());
continue;
}
loop.channel = loop.sound->playLoop();
if (loop.channel == -1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"MapAudioController: failed to play ambient %s",
track.path.c_str());
continue;
}
activeAmbientLoops_.emplace(key, std::move(loop));
}
}
void MapAudioController::StopAmbientLoop(ActiveAmbientLoop& loop) {
if (loop.channel >= 0) {
Sound::stop(loop.channel);
loop.channel = -1;
}
}
std::string MapAudioController::NormalizeTrackKey(const MapAudioTrack& track) {
if (!track.id.empty()) {
return NormalizeAudioPath(track.id);
}
return NormalizeAudioPath(track.path);
}
} // namespace frostbite2D

View File

@@ -21,7 +21,6 @@
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/utils/startup_trace.h> #include <frostbite2D/utils/startup_trace.h>
#include "scene/GameMapTestScene.h" #include "scene/GameMapTestScene.h"
#include "world/GameWorld.h"
using namespace frostbite2D; using namespace frostbite2D;
@@ -35,9 +34,19 @@ int main(int argc, char **argv) {
AppConfig config = AppConfig::createDefault(); AppConfig config = AppConfig::createDefault();
config.appName = "Frostbite2D Test App"; config.appName = "Frostbite2D Test App";
config.appVersion = "1.0.0"; config.appVersion = "1.0.0";
#ifdef SWITCH
config.windowConfig.width = 1280; config.windowConfig.width = 1280;
config.windowConfig.height = 720; config.windowConfig.height = 720;
#else
config.windowConfig.width = 1066;
config.windowConfig.height = 600;
#endif
config.windowConfig.title = "Frostbite2D - Async Init Demo"; config.windowConfig.title = "Frostbite2D - Async Init Demo";
config.useVirtualResolution = true;
config.virtualWidth = 1066;
config.virtualHeight = 600;
config.resolutionMode = ResolutionScaleMode::FitHeight;
config.renderStyleProfile = RenderStyleProfileId::PixelArt2D;
Application &app = Application::get(); Application &app = Application::get();
{ {
@@ -77,19 +86,20 @@ int main(int argc, char **argv) {
auto LoadingScene = MakePtr<Scene>(); auto LoadingScene = MakePtr<Scene>();
SceneManager::get().PushScene(LoadingScene); SceneManager::get().PushScene(LoadingScene);
auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg"); auto Background =
Background->SetSize(1280, 720); Sprite::createFromFile("assets/ImagePacks2/Loading0.png");
Background->SetSize(1066, 600);
LoadingScene->AddChild(Background); LoadingScene->AddChild(Background);
auto BackgroundBar = auto BackgroundBar =
Sprite::createFromFile("assets/ImagePacks2/Loading1.png"); Sprite::createFromFile("assets/ImagePacks2/Loading1.png");
BackgroundBar->SetPosition(0, 686); BackgroundBar->SetPosition(0 - 107, 566);
LoadingScene->AddChild(BackgroundBar); LoadingScene->AddChild(BackgroundBar);
auto LoadCircleSp = auto LoadCircleSp =
Sprite::createFromFile("assets/ImagePacks2/Loading2.png"); Sprite::createFromFile("assets/ImagePacks2/Loading2.png");
LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f)); LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f));
LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60); LoadCircleSp->SetPosition(1066 / 2.0f, 566 - 60);
LoadCircleSp->SetBlendMode(BlendMode::Additive); LoadCircleSp->SetBlendMode(BlendMode::Additive);
LoadCircleSp->AddUpdateListener([](Actor &self, float dt) { LoadCircleSp->AddUpdateListener([](Actor &self, float dt) {
auto rotation = self.GetRotation(); auto rotation = self.GetRotation();
@@ -146,8 +156,8 @@ int main(int argc, char **argv) {
{ {
ScopedStartupTrace stageTrace( ScopedStartupTrace stageTrace(
"SceneManager::ReplaceScene(GameMapTestScene)"); "SceneManager::ReplaceScene(GameMapTestScene)");
auto testMapScene = MakePtr<GameMapTestScene>(); auto gameMapTestScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(testMapScene); SceneManager::get().ReplaceScene(gameMapTestScene);
} }
StartupTrace::mark("after SceneManager::ReplaceScene"); StartupTrace::mark("after SceneManager::ReplaceScene");

View File

@@ -10,8 +10,6 @@ namespace frostbite2D {
namespace { namespace {
constexpr float kScreenWidth = 1280.0f;
constexpr float kScreenHeight = 720.0f;
constexpr int kDebugStickDeadzone = 8000; constexpr int kDebugStickDeadzone = 8000;
float normalizeControllerAxis(int16 value) { float normalizeControllerAxis(int16 value) {
@@ -180,10 +178,10 @@ void GameCameraController::applyFocus() const {
return; return;
} }
float halfWidth = kScreenWidth * 0.5f / zoom_;
float halfHeight = kScreenHeight * 0.5f / zoom_;
Vec2 cameraPos(focus_.x - halfWidth, focus_.y - halfHeight);
camera->setZoom(zoom_); camera->setZoom(zoom_);
float halfWidth = camera->getVisibleWidth() * 0.5f;
float halfHeight = camera->getVisibleHeight() * 0.5f;
Vec2 cameraPos(focus_.x - halfWidth, focus_.y - halfHeight);
camera->setPosition(cameraPos); camera->setPosition(cameraPos);
map_->ApplyCameraFocus(focus_); map_->ApplyCameraFocus(focus_);
} }

View File

@@ -1,9 +1,11 @@
#include "character/CharacterAnimation.h" #include "character/CharacterAnimation.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/pvf_archive.h> #include <frostbite2D/resource/pvf_archive.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cmath>
#include <cstdio> #include <cstdio>
#include <sstream> #include <sstream>
#include <utility> #include <utility>
@@ -32,6 +34,82 @@ std::string actionTail(const std::string& path) {
return path.substr(slashPos); return path.substr(slashPos);
} }
struct BoundsAccumulator {
bool valid = false;
float minX = 0.0f;
float minY = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
void includeRect(const Rect& rect) {
if (rect.empty()) {
return;
}
if (!valid) {
minX = rect.left();
minY = rect.top();
maxX = rect.right();
maxY = rect.bottom();
valid = true;
return;
}
minX = std::min(minX, rect.left());
minY = std::min(minY, rect.top());
maxX = std::max(maxX, rect.right());
maxY = std::max(maxY, rect.bottom());
}
};
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
Vec2 p0 = transform.transformPoint(Vec2(rect.left(), rect.top()));
Vec2 p1 = transform.transformPoint(Vec2(rect.right(), rect.top()));
Vec2 p2 = transform.transformPoint(Vec2(rect.left(), rect.bottom()));
Vec2 p3 = transform.transformPoint(Vec2(rect.right(), rect.bottom()));
float minX = std::min(std::min(p0.x, p1.x), std::min(p2.x, p3.x));
float minY = std::min(std::min(p0.y, p1.y), std::min(p2.y, p3.y));
float maxX = std::max(std::max(p0.x, p1.x), std::max(p2.x, p3.x));
float maxY = std::max(std::max(p0.y, p1.y), std::max(p2.y, p3.y));
return Rect(minX, minY, maxX - minX, maxY - minY);
}
void combineHash(uint64& seed, uint64 value) {
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}
CharacterAnimation::CompositeFrameInfo combineCompositeFrameInfo(
const CharacterAnimation::CompositeFrameInfo& lhs,
const CharacterAnimation::CompositeFrameInfo& rhs) {
if (!lhs.valid) {
return rhs;
}
if (!rhs.valid) {
return lhs;
}
float left = std::floor(std::min(lhs.localBounds.left(), rhs.localBounds.left()));
float top = std::floor(std::min(lhs.localBounds.top(), rhs.localBounds.top()));
float right = std::ceil(std::max(lhs.localBounds.right(), rhs.localBounds.right()));
float bottom =
std::ceil(std::max(lhs.localBounds.bottom(), rhs.localBounds.bottom()));
CharacterAnimation::CompositeFrameInfo result;
result.valid = true;
result.localBounds =
Rect(left, top, std::max(right - left, 1.0f), std::max(bottom - top, 1.0f));
result.width =
std::max(static_cast<int>(std::lround(result.localBounds.width())), 1);
result.height =
std::max(static_cast<int>(std::lround(result.localBounds.height())), 1);
result.originInTexture =
Vec2(-result.localBounds.left(), -result.localBounds.top());
result.groundAnchorInTexture = result.originInTexture;
return result;
}
} // namespace } // namespace
bool CharacterAnimation::Init(CharacterObject* parent, bool CharacterAnimation::Init(CharacterObject* parent,
@@ -45,6 +123,21 @@ bool CharacterAnimation::Init(CharacterObject* parent,
direction_ = 1; direction_ = 1;
actionFrameFlagCallback_ = nullptr; actionFrameFlagCallback_ = nullptr;
actionEndCallback_ = nullptr; actionEndCallback_ = nullptr;
compositeCanvas_ = MakePtr<CanvasActor>();
compositeFrameInfoByAction_.clear();
compositeDirty_ = true;
compositeVersion_ = 0;
lastCompositeSignature_ = 0;
if (!compositeCanvas_->Init(1, 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"CharacterAnimation: failed to initialize composite canvas");
compositeCanvas_.Reset();
} else {
compositeCanvas_->SetCustomDrawCallback([this]() {
RenderCurrentActionToCompositeCanvas();
});
}
for (const auto& [actionName, actionPath] : config.animationPath) { for (const auto& [actionName, actionPath] : config.animationPath) {
for (const char* slotName : kAvatarParts) { for (const char* slotName : kAvatarParts) {
@@ -65,9 +158,27 @@ bool CharacterAnimation::Init(CharacterObject* parent,
config.jobId); config.jobId);
} }
BuildCompositeActionFrames();
return true; return true;
} }
void CharacterAnimation::Update(float deltaTime) {
Actor::Update(deltaTime);
uint64 renderSignature = CaptureCurrentRenderSignature();
if (renderSignature != lastCompositeSignature_) {
compositeDirty_ = true;
}
}
void CharacterAnimation::Render() {
if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) {
RefreshCompositeTextureIfNeeded();
}
Actor::Render();
}
std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) { std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) {
size_t pos = path.find("%04d"); size_t pos = path.find("%04d");
if (pos != std::string::npos) { if (pos != std::string::npos) {
@@ -262,6 +373,7 @@ bool CharacterAnimation::SetAction(const std::string& actionName) {
currentActionTag_ = actionName; currentActionTag_ = actionName;
RefreshRuntimeCallbacks(); RefreshRuntimeCallbacks();
SetDirection(direction_); SetDirection(direction_);
MarkCompositeDirty();
return true; return true;
} }
@@ -278,6 +390,8 @@ void CharacterAnimation::SetDirection(int direction) {
animation->SetDirection(direction_); animation->SetDirection(direction_);
} }
} }
MarkCompositeDirty();
} }
bool CharacterAnimation::IsCurrentActionFinished() const { bool CharacterAnimation::IsCurrentActionFinished() const {
@@ -327,4 +441,163 @@ void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
RefreshRuntimeCallbacks(); RefreshRuntimeCallbacks();
} }
bool CharacterAnimation::EnsureCompositeTextureReady() {
RefreshCompositeTextureIfNeeded();
return HasCompositeTexture();
}
bool CharacterAnimation::HasCompositeTexture() const {
return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
GetCurrentCompositeFrameInfo().valid;
}
Ptr<Texture> CharacterAnimation::GetCompositeTexture() const {
return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr;
}
Vec2 CharacterAnimation::GetCompositeTextureSize() const {
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
return Vec2(static_cast<float>(info.width), static_cast<float>(info.height));
}
Vec2 CharacterAnimation::GetCompositeOriginInTexture() const {
return GetCurrentCompositeFrameInfo().originInTexture;
}
Vec2 CharacterAnimation::GetCompositeGroundAnchorInTexture() const {
return GetCurrentCompositeFrameInfo().groundAnchorInTexture;
}
CharacterAnimation::CompositeFrameInfo CharacterAnimation::ComputeCompositeFrameInfo(
const std::vector<RefPtr<Animation>>& animations, int direction) const {
CompositeFrameInfo info;
BoundsAccumulator bounds;
for (const auto& animation : animations) {
if (!animation) {
continue;
}
Rect animationBounds;
if (!animation->GetStaticLocalBounds(direction, animationBounds)) {
continue;
}
bounds.includeRect(
transformRectBounds(animationBounds, animation->GetLocalTransform()));
}
if (!bounds.valid) {
return info;
}
float left = std::floor(bounds.minX);
float top = std::floor(bounds.minY);
float right = std::ceil(bounds.maxX);
float bottom = std::ceil(bounds.maxY);
info.valid = true;
info.localBounds = Rect(left, top, std::max(right - left, 1.0f),
std::max(bottom - top, 1.0f));
info.width = std::max(static_cast<int>(std::lround(info.localBounds.width())), 1);
info.height = std::max(static_cast<int>(std::lround(info.localBounds.height())), 1);
info.originInTexture = Vec2(-info.localBounds.left(), -info.localBounds.top());
info.groundAnchorInTexture = info.originInTexture;
return info;
}
void CharacterAnimation::BuildCompositeActionFrames() {
compositeFrameInfoByAction_.clear();
for (const auto& [actionName, animations] : actionAnimations_) {
CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(animations, 1);
CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(animations, -1);
compositeFrameInfoByAction_[actionName] =
combineCompositeFrameInfo(rightInfo, leftInfo);
}
}
CharacterAnimation::CompositeFrameInfo CharacterAnimation::GetCurrentCompositeFrameInfo() const {
auto it = compositeFrameInfoByAction_.find(currentActionTag_);
if (it == compositeFrameInfoByAction_.end()) {
return CompositeFrameInfo();
}
return it->second;
}
uint64 CharacterAnimation::CaptureCurrentRenderSignature() const {
uint64 signature = 1469598103934665603ULL;
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
combineHash(signature, static_cast<uint64>(std::hash<std::string>{}(currentActionTag_)));
auto currentIt = actionAnimations_.find(currentActionTag_);
if (currentIt == actionAnimations_.end()) {
return signature;
}
for (const auto& animation : currentIt->second) {
if (!animation) {
continue;
}
combineHash(signature, animation->GetRenderSignature());
}
return signature;
}
void CharacterAnimation::MarkCompositeDirty() {
compositeDirty_ = true;
if (compositeCanvas_) {
compositeCanvas_->SetDirty();
}
}
void CharacterAnimation::UpdateCompositeCamera() {
if (!compositeCanvas_) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
Camera& camera = compositeCanvas_->GetCanvasCamera();
camera.setViewport(std::max(info.width, 1), std::max(info.height, 1));
camera.setZoom(1.0f);
Vec2 cameraOrigin = GetWorldTransform().transformPoint(
Vec2(info.localBounds.left(), info.localBounds.top()));
camera.setPosition(cameraOrigin);
}
void CharacterAnimation::RefreshCompositeTextureIfNeeded() {
if (!compositeDirty_ || !compositeCanvas_) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
if (!info.valid) {
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
return;
}
if (!compositeCanvas_->SetCanvasSize(std::max(info.width, 1),
std::max(info.height, 1))) {
return;
}
UpdateCompositeCamera();
if (!compositeCanvas_->Redraw()) {
return;
}
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
++compositeVersion_;
}
void CharacterAnimation::RenderCurrentActionToCompositeCanvas() {
if (actionAnimations_.find(currentActionTag_) == actionAnimations_.end()) {
return;
}
RenderChildren();
}
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -1,8 +1,9 @@
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "character/CharacterShadowActor.h"
#include "common/math/GameMath.h"
#include "map/GameMap.h" #include "map/GameMap.h"
#include "world/GameWorld.h" #include "world/GameWorld.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <cmath>
#include <frostbite2D/core/application.h> #include <frostbite2D/core/application.h>
#include <frostbite2D/utils/startup_trace.h> #include <frostbite2D/utils/startup_trace.h>
#include <sstream> #include <sstream>
@@ -43,18 +44,24 @@ Vec3 ToWorldVector(const CharacterWorldPosition& position) {
static_cast<float>(position.z)); static_cast<float>(position.z));
} }
int32 RoundWorldCoordinate(float value) { constexpr int kShadowNormalLayerZBias = 1000000;
return static_cast<int32>(std::lround(value));
}
} // namespace } // namespace
CharacterObject::~CharacterObject() {
DetachShadowActor();
shadowActor_ = nullptr;
}
bool CharacterObject::Construction(int jobId) { bool CharacterObject::Construction(int jobId) {
ScopedStartupTrace startupTrace("CharacterObject::Construction"); ScopedStartupTrace startupTrace("CharacterObject::Construction");
// Reset all runtime state before rebuilding the character from config. // Reset all runtime state before rebuilding the character from config.
EnableEventReceive(); EnableEventReceive();
DetachShadowActor();
RemoveAllChildren(); RemoveAllChildren();
animationManager_ = nullptr; animationManager_ = nullptr;
shadowActor_ = nullptr;
shadowAttachedMap_ = nullptr;
config_.reset(); config_.reset();
currentAction_.clear(); currentAction_.clear();
actionLibrary_ = CharacterActionLibrary(); actionLibrary_ = CharacterActionLibrary();
@@ -106,6 +113,9 @@ bool CharacterObject::Construction(int jobId) {
} }
AddChild(animationManager_); AddChild(animationManager_);
shadowActor_ = MakePtr<CharacterShadowActor>();
shadowAttachedMap_ = nullptr;
{ {
ScopedStartupTrace stageTrace("CharacterObject initial action setup"); ScopedStartupTrace stageTrace("CharacterObject initial action setup");
if (!RequireAction("idle", "CharacterObject::Construction")) { if (!RequireAction("idle", "CharacterObject::Construction")) {
@@ -176,6 +186,57 @@ void CharacterObject::SyncActorPositionFromWorld() {
SetZOrder(worldPosition.y); SetZOrder(worldPosition.y);
} }
void CharacterObject::DetachShadowActor() {
if (shadowActor_ && shadowActor_->GetParent()) {
shadowActor_->GetParent()->RemoveChild(shadowActor_);
}
shadowAttachedMap_ = nullptr;
}
void CharacterObject::SyncShadowAttachment() {
if (!shadowActor_) {
return;
}
GameMap* map = FindOwningMap();
if (!map) {
DetachShadowActor();
return;
}
if (shadowAttachedMap_ == map && shadowActor_->GetParent()) {
return;
}
if (shadowActor_->GetParent()) {
shadowActor_->GetParent()->RemoveChild(shadowActor_);
}
map->AddObjectToLayer("normal", shadowActor_);
shadowAttachedMap_ = map;
}
void CharacterObject::SyncShadowPresentation() {
if (!shadowActor_) {
return;
}
CharacterShadowFrameSnapshot snapshot;
snapshot.groundScreenPosition = motor_.position.ToGroundPosition();
snapshot.zOrder = motor_.position.y - kShadowNormalLayerZBias;
if (HasCompositeTexture()) {
snapshot.visible = true;
snapshot.texture = GetCompositeTexture();
snapshot.textureSize = GetCompositeTextureSize();
snapshot.groundAnchorInTexture = GetCompositeGroundAnchorInTexture();
snapshot.textureVersion = GetCompositeTextureVersion();
}
shadowActor_->ApplyFrameSnapshot(snapshot);
}
GameMap* CharacterObject::FindOwningMap() const { GameMap* CharacterObject::FindOwningMap() const {
Actor* node = GetParent(); Actor* node = GetParent();
while (node) { while (node) {
@@ -199,9 +260,9 @@ void CharacterObject::ApplyMapMovementConstraints(
static_cast<float>(motor_.position.y - previousPosition.y), static_cast<float>(motor_.position.y - previousPosition.y),
static_cast<float>(motor_.position.z - previousPosition.z)); static_cast<float>(motor_.position.z - previousPosition.z));
Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset); Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset);
motor_.position.x = RoundWorldCoordinate(resolvedWorldPos.x); motor_.position.x = gameMath::RoundWorldCoordinate(resolvedWorldPos.x);
motor_.position.y = RoundWorldCoordinate(resolvedWorldPos.y); motor_.position.y = gameMath::RoundWorldCoordinate(resolvedWorldPos.y);
motor_.position.z = RoundWorldCoordinate(resolvedWorldPos.z); motor_.position.z = gameMath::RoundWorldCoordinate(resolvedWorldPos.z);
} }
void CharacterObject::QueueMapTransitionIfNeeded() { void CharacterObject::QueueMapTransitionIfNeeded() {
@@ -210,16 +271,24 @@ void CharacterObject::QueueMapTransitionIfNeeded() {
return; return;
} }
GameMap::MapMoveArea target;
if (!map->TryGetMoveAreaTarget(ToWorldVector(motor_.position), target)) {
return;
}
GameWorld* world = GameWorld::GetWorld(); GameWorld* world = GameWorld::GetWorld();
if (!world) { if (!world) {
return; return;
} }
size_t moveAreaIndex = map->FindMoveAreaIndex(ToWorldVector(motor_.position));
world->UpdateMoveAreaSuppression(map, moveAreaIndex);
if (moveAreaIndex == GameMap::kInvalidMoveAreaIndex ||
world->ShouldSuppressMoveArea(map, moveAreaIndex)) {
return;
}
const auto& moveAreaInfo = map->GetMoveAreaInfo();
if (moveAreaIndex >= moveAreaInfo.size()) {
return;
}
const GameMap::MapMoveArea& target = moveAreaInfo[moveAreaIndex];
world->RequestMoveCharacter(RefPtr<Actor>(this), target.town, target.area); world->RequestMoveCharacter(RefPtr<Actor>(this), target.town, target.area);
} }
@@ -313,6 +382,19 @@ void CharacterObject::ApplyHit(const HitContext& hit) {
stateMachine_.ForceHurt(*this, hurtAction); stateMachine_.ForceHurt(*this, hurtAction);
} }
void CharacterObject::Update(float deltaTime) {
Actor::Update(deltaTime);
SyncShadowAttachment();
SyncShadowPresentation();
}
void CharacterObject::PrepareRenderFrame() {
if (animationManager_) {
animationManager_->EnsureCompositeTextureReady();
}
SyncShadowPresentation();
}
void CharacterObject::OnUpdate(float deltaTime) { void CharacterObject::OnUpdate(float deltaTime) {
// Fixed update order keeps input, state transitions, and motion deterministic. // Fixed update order keeps input, state transitions, and motion deterministic.
lastDeltaTime_ = deltaTime; lastDeltaTime_ = deltaTime;
@@ -328,6 +410,12 @@ void CharacterObject::OnUpdate(float deltaTime) {
SetFacing(motor_.facing); SetFacing(motor_.facing);
} }
void CharacterObject::OnAdded(Actor* parent) {
(void)parent;
SyncShadowAttachment();
SyncShadowPresentation();
}
bool CharacterObject::OnEvent(const Event& event) { bool CharacterObject::OnEvent(const Event& event) {
if (!IsEventReceiveEnabled() || !inputEnabled_) { if (!IsEventReceiveEnabled() || !inputEnabled_) {
return false; return false;
@@ -400,6 +488,32 @@ animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const {
return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame(); return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame();
} }
bool CharacterObject::HasCompositeTexture() const {
return animationManager_ && animationManager_->HasCompositeTexture();
}
Ptr<Texture> CharacterObject::GetCompositeTexture() const {
return animationManager_ ? animationManager_->GetCompositeTexture() : nullptr;
}
Vec2 CharacterObject::GetCompositeTextureSize() const {
return animationManager_ ? animationManager_->GetCompositeTextureSize() : Vec2::Zero();
}
Vec2 CharacterObject::GetCompositeOriginInTexture() const {
return animationManager_ ? animationManager_->GetCompositeOriginInTexture()
: Vec2::Zero();
}
Vec2 CharacterObject::GetCompositeGroundAnchorInTexture() const {
return animationManager_ ? animationManager_->GetCompositeGroundAnchorInTexture()
: Vec2::Zero();
}
uint64 CharacterObject::GetCompositeTextureVersion() const {
return animationManager_ ? animationManager_->GetCompositeVersion() : 0;
}
void CharacterObject::SetAnimationFrameFlagCallback( void CharacterObject::SetAnimationFrameFlagCallback(
CharacterAnimation::ActionFrameFlagCallback callback) { CharacterAnimation::ActionFrameFlagCallback callback) {
if (animationManager_) { if (animationManager_) {

View File

@@ -0,0 +1,61 @@
#include "character/CharacterShadowActor.h"
#include <algorithm>
namespace frostbite2D {
namespace {
constexpr Color kShadowColor(0.0f, 0.0f, 0.0f, 0.42f);
const Vec2 kShadowScale(0.9f, 0.30f);
constexpr float kShadowSkewX = 50.0f;
} // namespace
CharacterShadowActor::CharacterShadowActor() {
SetName("characterShadow");
SetColor(kShadowColor);
SetScale(kShadowScale);
// Anchor stays on the ground point while the upper silhouette falls left.
SetSkew(kShadowSkewX, 0.0f);
SetBlendMode(BlendMode::Normal);
SetVisible(false);
}
void CharacterShadowActor::ApplyFrameSnapshot(
const CharacterShadowFrameSnapshot& snapshot) {
bool hasTexture = snapshot.visible && snapshot.texture && snapshot.textureSize.x > 0.0f &&
snapshot.textureSize.y > 0.0f;
if (!hasTexture) {
SetVisible(false);
return;
}
ApplyTextureState(snapshot);
SetPosition(snapshot.groundScreenPosition);
SetZOrder(snapshot.zOrder);
SetVisible(true);
}
void CharacterShadowActor::ApplyTextureState(
const CharacterShadowFrameSnapshot& snapshot) {
if (appliedTexture_ == snapshot.texture &&
appliedTextureSize_ == snapshot.textureSize &&
appliedGroundAnchorInTexture_ == snapshot.groundAnchorInTexture &&
appliedTextureVersion_ == snapshot.textureVersion) {
return;
}
SetTexture(snapshot.texture);
SetSize(snapshot.textureSize);
float safeWidth = std::max(snapshot.textureSize.x, 1.0f);
float safeHeight = std::max(snapshot.textureSize.y, 1.0f);
SetAnchor(std::clamp(snapshot.groundAnchorInTexture.x / safeWidth, 0.0f, 1.0f),
std::clamp(snapshot.groundAnchorInTexture.y / safeHeight, 0.0f, 1.0f));
appliedTexture_ = snapshot.texture;
appliedTextureSize_ = snapshot.textureSize;
appliedGroundAnchorInTexture_ = snapshot.groundAnchorInTexture;
appliedTextureVersion_ = snapshot.textureVersion;
}
} // namespace frostbite2D

View File

@@ -0,0 +1,35 @@
#include "common/InteractionHighlightSprite.h"
namespace frostbite2D {
InteractionHighlightSprite::InteractionHighlightSprite() {
SetName("interactionHighlight");
SetShader("outline_sprite");
SetColor(Colors::Green);
SetVisible(false);
}
void InteractionHighlightSprite::SetGroundAnchorInTexture(
const Vec2& groundAnchorInTexture) {
Vec2 textureSize = GetSize();
if (textureSize.x <= 0.0f || textureSize.y <= 0.0f) {
SetAnchor(0.5f, 1.0f);
texelSize_ = Vec2::One();
return;
}
SetAnchor(groundAnchorInTexture.x / textureSize.x,
groundAnchorInTexture.y / textureSize.y);
texelSize_ = Vec2(1.0f / textureSize.x, 1.0f / textureSize.y);
}
void InteractionHighlightSprite::ConfigureShader(Shader* shader) const {
Sprite::ConfigureShader(shader);
if (!shader) {
return;
}
shader->setVec2("u_texelSize", texelSize_);
}
} // namespace frostbite2D

View File

@@ -1,8 +1,9 @@
#include "common/GameDebugActor.h" #include "common/debug/GameDebugActor.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "map/GameMap.h" #include "map/GameMap.h"
#include "ui/NineSliceActor.h" #include "ui/NineSliceActor.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <algorithm>
#include <frostbite2D/2d/text_sprite.h> #include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <limits> #include <limits>
@@ -15,6 +16,7 @@ constexpr float kDebugHudMarginX = 12.0f;
constexpr float kDebugHudMarginY = 12.0f; constexpr float kDebugHudMarginY = 12.0f;
constexpr float kDebugHudPaddingX = 8.0f; constexpr float kDebugHudPaddingX = 8.0f;
constexpr float kDebugHudPaddingY = 6.0f; constexpr float kDebugHudPaddingY = 6.0f;
constexpr float kDebugHudLineGap = 4.0f;
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img"; constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
void ConfigureTextLine(RefPtr<TextSprite> textSprite, const char* name, void ConfigureTextLine(RefPtr<TextSprite> textSprite, const char* name,
@@ -102,7 +104,7 @@ void GameDebugActor::OnUpdate(float deltaTime) {
} }
void GameDebugActor::initOverlay() { void GameDebugActor::initOverlay() {
if (background_ || coordText_) { if (background_ || coordText_ || actionText_) {
return; return;
} }
@@ -120,43 +122,92 @@ void GameDebugActor::initOverlay() {
ConfigureTextLine(coordText_, "debugCoordText", 1); ConfigureTextLine(coordText_, "debugCoordText", 1);
AddChild(coordText_); AddChild(coordText_);
actionText_ = TextSprite::create();
ConfigureTextLine(actionText_, "debugActionText", 1);
AddChild(actionText_);
setOverlayVisible(false); setOverlayVisible(false);
} }
void GameDebugActor::updateOverlay() { void GameDebugActor::updateOverlay() {
if (!coordText_ || !debugMap_ || !trackedCharacter_ || if (!coordText_ || !actionText_) {
!debugMap_->IsDebugModeEnabled()) {
if (debugMap_) {
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
}
setOverlayVisible(false); setOverlayVisible(false);
return; return;
} }
updateMapDebugHighlight();
if (!trackedCharacter_) {
setOverlayVisible(false);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
char coordTextBuffer[96];
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(coordTextBuffer);
int frameCount = std::max(trackedCharacter_->GetCurrentAnimationFrameCount(), 1);
int frameIndex = std::clamp(trackedCharacter_->GetCurrentAnimationFrameIndex(), 0,
frameCount - 1);
char actionTextBuffer[128];
SDL_snprintf(actionTextBuffer, sizeof(actionTextBuffer),
"Action: %s dir:%c frame:%d/%d",
trackedCharacter_->GetCurrentAction().empty()
? "<none>"
: trackedCharacter_->GetCurrentAction().c_str(),
trackedCharacter_->GetDirection() >= 0 ? 'R' : 'L', frameIndex + 1,
frameCount);
actionText_->SetText(actionTextBuffer);
Vec2 coordTextSize = coordText_->GetTextSize();
Vec2 actionTextSize = actionText_->GetTextSize();
float panelWidth = std::max(coordTextSize.x, actionTextSize.x) + kDebugHudPaddingX * 2.0f;
float contentHeight = coordTextSize.y;
if (actionText_->IsVisible()) {
contentHeight += kDebugHudLineGap + actionTextSize.y;
}
float panelHeight = contentHeight + kDebugHudPaddingY * 2.0f;
Vec2 panelSize(panelWidth, panelHeight);
if (background_) {
background_->SetSize(panelSize);
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
float currentY = kDebugHudPaddingY;
coordText_->SetPosition(kDebugHudPaddingX, currentY);
currentY += coordTextSize.y;
if (actionText_->IsVisible()) {
currentY += kDebugHudLineGap;
actionText_->SetPosition(kDebugHudPaddingX, currentY);
currentY += actionTextSize.y;
}
setOverlayVisible(true);
}
void GameDebugActor::updateMapDebugHighlight() {
if (!debugMap_) {
return;
}
if (!trackedCharacter_ || !debugMap_->IsDebugModeEnabled()) {
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition(); const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
Vec3 currentWorldPos(static_cast<float>(worldPosition.x), Vec3 currentWorldPos(static_cast<float>(worldPosition.x),
static_cast<float>(worldPosition.y), static_cast<float>(worldPosition.y),
static_cast<float>(worldPosition.z)); static_cast<float>(worldPosition.z));
size_t moveAreaIndex = debugMap_->FindMoveAreaIndex(currentWorldPos); size_t moveAreaIndex = debugMap_->FindMoveAreaIndex(currentWorldPos);
debugMap_->SetDebugHighlightedMoveAreaIndex(moveAreaIndex); debugMap_->SetDebugHighlightedMoveAreaIndex(moveAreaIndex);
char coordTextBuffer[96];
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(coordTextBuffer);
Vec2 coordTextSize = coordText_->GetTextSize();
float panelWidth = coordTextSize.x + kDebugHudPaddingX * 2.0f;
float panelHeight = coordTextSize.y + kDebugHudPaddingY * 2.0f;
Vec2 panelSize(panelWidth, panelHeight);
if (background_) {
background_->SetSize(panelSize);
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
setOverlayVisible(true);
} }
void GameDebugActor::setOverlayVisible(bool visible) { void GameDebugActor::setOverlayVisible(bool visible) {
@@ -166,6 +217,9 @@ void GameDebugActor::setOverlayVisible(bool visible) {
if (coordText_) { if (coordText_) {
coordText_->SetVisible(visible); coordText_->SetVisible(visible);
} }
if (actionText_) {
actionText_->SetVisible(visible);
}
} }
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -0,0 +1,148 @@
#include "common/math/GameMath.h"
#include <algorithm>
#include <cmath>
namespace frostbite2D::gameMath {
namespace {
constexpr double kPolygonEpsilon = 0.001;
} // namespace
int32 RoundWorldCoordinate(float value) {
return static_cast<int32>(std::lround(value));
}
Vec2 MakeIntegerWorldPoint(int x, int y) {
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
return Vec3(static_cast<float>(x), static_cast<float>(y),
static_cast<float>(z));
}
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
Vec2 segment = end - start;
Vec2 toPoint = point - start;
double cross = static_cast<double>(segment.cross(toPoint));
if (std::abs(cross) > kPolygonEpsilon) {
return false;
}
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
return point.x >= minX && point.x <= maxX && point.y >= minY &&
point.y <= maxY;
}
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
if (polygon.size() < 3) {
return false;
}
bool inside = false;
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (IsPointOnSegment(point, start, end)) {
return true;
}
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
if (!crossesScanline) {
continue;
}
double intersectX =
static_cast<double>(start.x) +
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
(static_cast<double>(end.y) - static_cast<double>(start.y));
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
inside = !inside;
}
}
return inside;
}
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
}
std::vector<Vec2> BuildRectPolygon(const Rect& rect) {
if (rect.empty()) {
return {};
}
return {
Vec2(rect.left(), rect.top()),
Vec2(rect.right(), rect.top()),
Vec2(rect.right(), rect.bottom()),
Vec2(rect.left(), rect.bottom()),
};
}
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) {
std::vector<Rect> fillRects;
if (polygon.size() < 3) {
return fillRects;
}
float minY = polygon.front().y;
float maxY = polygon.front().y;
for (const auto& point : polygon) {
minY = std::min(minY, point.y);
maxY = std::max(maxY, point.y);
}
int scanlineMinY = static_cast<int>(std::floor(minY));
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
if (scanlineMaxY < scanlineMinY) {
return fillRects;
}
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
float scanY = static_cast<float>(y) + 0.5f;
std::vector<float> intersections;
intersections.reserve(polygon.size());
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (start.y == end.y) {
continue;
}
float edgeMinY = std::min(start.y, end.y);
float edgeMaxY = std::max(start.y, end.y);
if (scanY < edgeMinY || scanY >= edgeMaxY) {
continue;
}
float t = (scanY - start.y) / (end.y - start.y);
intersections.push_back(start.x + (end.x - start.x) * t);
}
if (intersections.size() < 2) {
continue;
}
std::sort(intersections.begin(), intersections.end());
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
float left = std::floor(intersections[i]);
float right = std::ceil(intersections[i + 1]);
float width = right - left;
if (width > 0.0f) {
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
}
}
}
return fillRects;
}
} // namespace frostbite2D::gameMath

View File

@@ -1,3 +1,6 @@
#include "audio/MapAudioController.h"
#include "common/math/GameMath.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h" #include "map/GameMap.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <frostbite2D/2d/sprite.h> #include <frostbite2D/2d/sprite.h>
@@ -24,71 +27,6 @@ static const int kLayerOrders[] = {
constexpr int kTileRootZOrder = -1000000; constexpr int kTileRootZOrder = -1000000;
constexpr float kExtendedTileStepY = 120.0f; constexpr float kExtendedTileStepY = 120.0f;
constexpr double kPolygonEpsilon = 0.001;
int RoundWorldCoordinate(float value) {
return static_cast<int>(std::lround(value));
}
Vec2 MakeIntegerWorldPoint(int x, int y) {
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
return Vec3(static_cast<float>(x), static_cast<float>(y),
static_cast<float>(z));
}
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
Vec2 segment = end - start;
Vec2 toPoint = point - start;
double cross = static_cast<double>(segment.cross(toPoint));
if (std::abs(cross) > kPolygonEpsilon) {
return false;
}
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
return point.x >= minX && point.x <= maxX && point.y >= minY &&
point.y <= maxY;
}
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
if (polygon.size() < 3) {
return false;
}
bool inside = false;
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (IsPointOnSegment(point, start, end)) {
return true;
}
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
if (!crossesScanline) {
continue;
}
double intersectX =
static_cast<double>(start.x) +
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
(static_cast<double>(end.y) - static_cast<double>(start.y));
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
inside = !inside;
}
}
return inside;
}
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
}
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite // 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
// 占位,避免整张地图中断。 // 占位,避免整张地图中断。
@@ -102,6 +40,49 @@ Ptr<Sprite> createMapSprite(const std::string& path, int index) {
return sprite; return sprite;
} }
MapAudioState BuildMapAudioState(const game::MapConfig& mapConfig) {
MapAudioState state;
if (mapConfig.soundIds.empty()) {
return state;
}
auto& audioDatabase = AudioDatabase::get();
for (const auto& soundId : mapConfig.soundIds) {
AudioEntryType type = audioDatabase.typeOf(soundId);
std::string filePath = audioDatabase.filePath(soundId);
if (filePath.empty()) {
continue;
}
if (type == AudioEntryType::Music) {
if (!state.bgmTrack) {
state.bgmTrack = MapAudioTrack{soundId, filePath};
}
continue;
}
if (type == AudioEntryType::Ambient) {
state.ambientLoops.push_back(MapAudioTrack{soundId, filePath});
continue;
}
if (type == AudioEntryType::Effect) {
SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: ignore one-shot map sound %s in [%s]",
soundId.c_str(), mapConfig.mapPath.c_str());
continue;
}
if (type == AudioEntryType::Random) {
SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: ignore random map audio %s in [%s]",
soundId.c_str(), mapConfig.mapPath.c_str());
}
}
return state;
}
} // namespace } // namespace
GameMap::GameMap() { GameMap::GameMap() {
@@ -172,27 +153,34 @@ bool GameMap::LoadMap(const std::string &mapName) {
} }
void GameMap::Enter() { void GameMap::Enter() {
// 地图进入时尝试找到第一个可播放的 BGM 并循环播放。 MapAudioController::get().ApplyMapAudio(BuildMapAudioState(mapConfig_));
if (!mapConfig_.soundIds.empty()) {
auto& audioDatabase = AudioDatabase::get();
for (const auto& soundId : mapConfig_.soundIds) {
if (audioDatabase.typeOf(soundId) != AudioEntryType::Music) {
continue;
}
std::string filePath = audioDatabase.filePath(soundId);
if (filePath.empty()) {
continue;
}
currentMusic_ = Music::loadFromPath(filePath);
if (currentMusic_ && currentMusic_->play(-1)) {
break;
}
}
}
} }
void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); } void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); }
void GameMap::Render() {
PrepareRuntimeObjectsForRender();
Actor::Render();
}
void GameMap::PrepareRuntimeObjectsForRender() {
auto layerIt = layerMap_.find("normal");
if (layerIt == layerMap_.end() || !layerIt->second) {
return;
}
for (auto it = layerIt->second->GetChildren().begin();
it != layerIt->second->GetChildren().end(); ++it) {
if (!*it) {
continue;
}
if (auto* character = dynamic_cast<CharacterObject*>((*it).Get())) {
character->PrepareRenderFrame();
}
}
}
void GameMap::InitTile() { void GameMap::InitTile() {
if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) { if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) {
return; return;
@@ -417,7 +405,7 @@ void GameMap::updateLayerPositions(const Vec2 &cameraFocus) {
return; return;
} }
Vec2 cameraPos = camera->getPosition(); Vec2 cameraPos = camera->getRenderPosition();
// 主视图平移完全交给底层 // 主视图平移完全交给底层
// Camera这里只保留地图层自己的静态校准和少量视差偏移。 // Camera这里只保留地图层自己的静态校准和少量视差偏移。
@@ -439,37 +427,53 @@ void GameMap::updateLayerPositions(const Vec2 &cameraFocus) {
} }
void GameMap::AddObject(RefPtr<Actor> object) { void GameMap::AddObject(RefPtr<Actor> object) {
AddObjectToLayer("normal", object);
}
void GameMap::AddObjectToLayer(const std::string& layerName, RefPtr<Actor> object) {
if (!object) { if (!object) {
return; return;
} }
// Keep dynamic objects on the normal layer and sort them by y.
auto layerIt = layerMap_.find(layerName);
if (layerIt == layerMap_.end() || !layerIt->second) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMap: layer %s missing, fallback to normal",
layerName.c_str());
layerIt = layerMap_.find("normal");
if (layerIt == layerMap_.end() || !layerIt->second) {
return;
}
}
// Keep runtime objects sorted by their ground y inside the target layer.
object->SetZOrder(static_cast<int>(object->GetPosition().y)); object->SetZOrder(static_cast<int>(object->GetPosition().y));
layerMap_["normal"]->AddObject(object); layerIt->second->AddObject(object);
} }
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
int currentX = RoundWorldCoordinate(curPos.x); int currentX = gameMath::RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y); int currentY = gameMath::RoundWorldCoordinate(curPos.y);
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x); int targetX = gameMath::RoundWorldCoordinate(curPos.x + posOffset.x);
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y); int targetY = gameMath::RoundWorldCoordinate(curPos.y + posOffset.y);
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z); int targetZ = gameMath::RoundWorldCoordinate(curPos.z + posOffset.z);
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ); Vec3 result = gameMath::MakeIntegerWorldPosition(currentX, currentY, targetZ);
if (!movableAreaCheckEnabled_) { if (!movableAreaCheckEnabled_) {
return MakeIntegerWorldPosition(targetX, targetY, targetZ); return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ);
} }
if (movablePolygon_.size() < 3) { if (movablePolygon_.size() < 3) {
return MakeIntegerWorldPosition(targetX, targetY, targetZ); return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ);
} }
// Prefer the full destination first; only fall back to single-axis sliding // Prefer the full destination first; only fall back to single-axis sliding
// when the combined move would leave the movable polygon. // when the combined move would leave the movable polygon.
if (IsPointMovable(movablePolygon_, targetX, targetY)) { if (gameMath::IsPointMovable(movablePolygon_, targetX, targetY)) {
result.x = static_cast<float>(targetX); result.x = static_cast<float>(targetX);
result.y = static_cast<float>(targetY); result.y = static_cast<float>(targetY);
return result; return result;
} }
bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY); bool isXValid = gameMath::IsPointMovable(movablePolygon_, targetX, currentY);
bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY); bool isYValid = gameMath::IsPointMovable(movablePolygon_, currentX, targetY);
if (isXValid && isYValid) { if (isXValid && isYValid) {
int moveX = std::abs(targetX - currentX); int moveX = std::abs(targetX - currentX);
@@ -500,12 +504,12 @@ GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const {
} }
size_t GameMap::FindMoveAreaIndex(const Vec3& curPos) const { size_t GameMap::FindMoveAreaIndex(const Vec3& curPos) const {
int currentX = RoundWorldCoordinate(curPos.x); int currentX = gameMath::RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y); int currentY = gameMath::RoundWorldCoordinate(curPos.y);
for (size_t i = 0; for (size_t i = 0;
i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size();
++i) { ++i) {
if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) { if (moveArea_[i].containsPoint(gameMath::MakeIntegerWorldPoint(currentX, currentY))) {
return i; return i;
} }
} }
@@ -534,6 +538,13 @@ const std::vector<GameMap::MapMoveArea>& GameMap::GetMoveAreaInfo() const {
return mapConfig_.townMovableAreaTargets; return mapConfig_.townMovableAreaTargets;
} }
game::MoveAreaBounds GameMap::GetMovablePositionBounds(size_t index) const {
if (index >= mapConfig_.townMovableAreaBounds.size()) {
return game::MoveAreaBounds();
}
return mapConfig_.townMovableAreaBounds[index];
}
Rect GameMap::GetMovablePositionArea(size_t index) const { Rect GameMap::GetMovablePositionArea(size_t index) const {
if (index >= mapConfig_.townMovableAreas.size()) { if (index >= mapConfig_.townMovableAreas.size()) {
return Rect::Zero(); return Rect::Zero();

View File

@@ -1,4 +1,6 @@
#include "map/GameMapLayer.h" #include "map/GameMapLayer.h"
#include "common/math/GameMath.h"
#include <SDL2/SDL.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <frostbite2D/graphics/renderer.h> #include <frostbite2D/graphics/renderer.h>
@@ -13,79 +15,54 @@ constexpr float kDebugEdgePointSize = 5.0f;
constexpr float kDebugVertexSize = 9.0f; constexpr float kDebugVertexSize = 9.0f;
constexpr float kDebugEdgeStep = 4.0f; constexpr float kDebugEdgeStep = 4.0f;
std::vector<Vec2> BuildRectPolygon(const Rect& rect) { struct BoundsAccumulator {
bool valid = false;
float left = 0.0f;
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
void IncludePoint(const Vec2& point) {
if (!valid) {
valid = true;
left = point.x;
top = point.y;
right = point.x;
bottom = point.y;
return;
}
left = std::min(left, point.x);
top = std::min(top, point.y);
right = std::max(right, point.x);
bottom = std::max(bottom, point.y);
}
void IncludeRect(const Rect& rect) {
if (rect.empty()) { if (rect.empty()) {
return {}; return;
} }
return { IncludePoint(Vec2(rect.left(), rect.top()));
Vec2(rect.left(), rect.top()), IncludePoint(Vec2(rect.right(), rect.bottom()));
Vec2(rect.right(), rect.top()), }
Vec2(rect.right(), rect.bottom()),
Vec2(rect.left(), rect.bottom()), Rect Build(float padding) const {
if (!valid) {
return Rect::Zero();
}
float paddedLeft = std::floor(left - padding);
float paddedTop = std::floor(top - padding);
float paddedRight = std::ceil(right + padding);
float paddedBottom = std::ceil(bottom + padding);
return Rect(paddedLeft, paddedTop,
std::max(paddedRight - paddedLeft, 1.0f),
std::max(paddedBottom - paddedTop, 1.0f));
}
}; };
}
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) { void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& drawOrigin,
std::vector<Rect> fillRects;
if (polygon.size() < 3) {
return fillRects;
}
float minY = polygon.front().y;
float maxY = polygon.front().y;
for (const auto& point : polygon) {
minY = std::min(minY, point.y);
maxY = std::max(maxY, point.y);
}
int scanlineMinY = static_cast<int>(std::floor(minY));
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
if (scanlineMaxY < scanlineMinY) {
return fillRects;
}
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
float scanY = static_cast<float>(y) + 0.5f;
std::vector<float> intersections;
intersections.reserve(polygon.size());
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (start.y == end.y) {
continue;
}
float edgeMinY = std::min(start.y, end.y);
float edgeMaxY = std::max(start.y, end.y);
if (scanY < edgeMinY || scanY >= edgeMaxY) {
continue;
}
float t = (scanY - start.y) / (end.y - start.y);
intersections.push_back(start.x + (end.x - start.x) * t);
}
if (intersections.size() < 2) {
continue;
}
std::sort(intersections.begin(), intersections.end());
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
float left = std::floor(intersections[i]);
float right = std::ceil(intersections[i + 1]);
float width = right - left;
if (width > 0.0f) {
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
}
}
}
return fillRects;
}
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
const Color& color) { const Color& color) {
if (polygon.size() < 2) { if (polygon.size() < 2) {
return; return;
@@ -93,8 +70,8 @@ void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigi
auto& renderer = Renderer::get(); auto& renderer = Renderer::get();
for (size_t i = 0; i < polygon.size(); ++i) { for (size_t i = 0; i < polygon.size(); ++i) {
Vec2 start = worldOrigin + polygon[i]; Vec2 start = drawOrigin + polygon[i];
Vec2 end = worldOrigin + polygon[(i + 1) % polygon.size()]; Vec2 end = drawOrigin + polygon[(i + 1) % polygon.size()];
Vec2 delta = end - start; Vec2 delta = end - start;
float length = delta.length(); float length = delta.length();
int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep))); int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep)));
@@ -112,10 +89,10 @@ void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigi
} }
void DrawPolygonVertices(const std::vector<Vec2>& polygon, void DrawPolygonVertices(const std::vector<Vec2>& polygon,
const Vec2& worldOrigin, const Color& color) { const Vec2& drawOrigin, const Color& color) {
auto& renderer = Renderer::get(); auto& renderer = Renderer::get();
for (const auto& vertex : polygon) { for (const auto& vertex : polygon) {
Vec2 point = worldOrigin + vertex; Vec2 point = drawOrigin + vertex;
renderer.drawQuad( renderer.drawQuad(
Rect(point.x - kDebugVertexSize * 0.5f, Rect(point.x - kDebugVertexSize * 0.5f,
point.y - kDebugVertexSize * 0.5f, kDebugVertexSize, point.y - kDebugVertexSize * 0.5f, kDebugVertexSize,
@@ -128,49 +105,197 @@ void DrawPolygonVertices(const std::vector<Vec2>& polygon,
void GameMapLayer::Render() { void GameMapLayer::Render() {
Actor::Render(); Actor::Render();
EnsureDebugOverlayCanvases();
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero()); if (feasibleAreaOverlayDirty_) {
RefreshFeasibleAreaOverlay();
}
if (moveAreaOverlayDirty_) {
RefreshMoveAreaOverlay();
}
if (feasibleAreaOverlayCanvas_ && feasibleAreaOverlayCanvas_->IsVisible()) {
feasibleAreaOverlayCanvas_->SetPosition(
GetWorldTransform().transformPoint(feasibleAreaOverlayBounds_.origin));
feasibleAreaOverlayCanvas_->SetOpacity(GetWorldOpacity());
feasibleAreaOverlayCanvas_->Render();
}
if (moveAreaOverlayCanvas_ && moveAreaOverlayCanvas_->IsVisible()) {
moveAreaOverlayCanvas_->SetPosition(
GetWorldTransform().transformPoint(moveAreaOverlayBounds_.origin));
moveAreaOverlayCanvas_->SetOpacity(GetWorldOpacity());
moveAreaOverlayCanvas_->Render();
}
}
void GameMapLayer::EnsureDebugOverlayCanvases() {
auto initCanvas = [this](RefPtr<CanvasActor>& canvas, const char* label,
std::function<void()> callback) {
if (canvas) {
return;
}
canvas = MakePtr<CanvasActor>();
if (!canvas || !canvas->Init(1, 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapLayer: failed to initialize %s canvas", label);
canvas.Reset();
return;
}
canvas->SetVisible(false);
canvas->SetClearColor(Colors::Transparent);
canvas->SetCustomDrawCallback(callback);
};
initCanvas(feasibleAreaOverlayCanvas_, "feasible area overlay",
[this]() { DrawFeasibleAreaOverlay(); });
initCanvas(moveAreaOverlayCanvas_, "move area overlay",
[this]() { DrawMoveAreaOverlay(); });
}
void GameMapLayer::RefreshFeasibleAreaOverlay() {
feasibleAreaOverlayDirty_ = false;
if (!feasibleAreaOverlayCanvas_) {
return;
}
feasibleAreaOverlayBounds_ = ComputeFeasibleAreaOverlayBounds();
if (feasibleAreaOverlayBounds_.empty()) {
feasibleAreaOverlayCanvas_->SetVisible(false);
return;
}
int width =
std::max(static_cast<int>(std::lround(feasibleAreaOverlayBounds_.width())), 1);
int height =
std::max(static_cast<int>(std::lround(feasibleAreaOverlayBounds_.height())), 1);
if (!feasibleAreaOverlayCanvas_->SetCanvasSize(width, height)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapLayer: failed to resize feasible area overlay canvas to %dx%d",
width, height);
feasibleAreaOverlayCanvas_->SetVisible(false);
feasibleAreaOverlayDirty_ = true;
return;
}
feasibleAreaOverlayCanvas_->SetVisible(true);
feasibleAreaOverlayCanvas_->SetDirty();
}
void GameMapLayer::RefreshMoveAreaOverlay() {
moveAreaOverlayDirty_ = false;
if (!moveAreaOverlayCanvas_) {
return;
}
moveAreaOverlayBounds_ = ComputeMoveAreaOverlayBounds();
if (moveAreaOverlayBounds_.empty()) {
moveAreaOverlayCanvas_->SetVisible(false);
return;
}
int width =
std::max(static_cast<int>(std::lround(moveAreaOverlayBounds_.width())), 1);
int height =
std::max(static_cast<int>(std::lround(moveAreaOverlayBounds_.height())), 1);
if (!moveAreaOverlayCanvas_->SetCanvasSize(width, height)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapLayer: failed to resize move area overlay canvas to %dx%d",
width, height);
moveAreaOverlayCanvas_->SetVisible(false);
moveAreaOverlayDirty_ = true;
return;
}
moveAreaOverlayCanvas_->SetVisible(true);
moveAreaOverlayCanvas_->SetDirty();
}
Rect GameMapLayer::ComputeFeasibleAreaOverlayBounds() const {
BoundsAccumulator bounds;
for (const auto& rect : feasibleAreaFillRects_) {
bounds.IncludeRect(rect);
}
for (const auto& point : feasibleAreaPolygon_) {
bounds.IncludePoint(point);
}
float padding = std::max(kDebugEdgePointSize, kDebugVertexSize) * 0.5f;
return bounds.Build(padding);
}
Rect GameMapLayer::ComputeMoveAreaOverlayBounds() const {
BoundsAccumulator bounds;
for (const auto& moveArea : moveAreaInfoList_) {
bounds.IncludeRect(moveArea.rect);
}
float padding = std::max(kDebugEdgePointSize, kDebugVertexSize) * 0.5f;
return bounds.Build(padding);
}
void GameMapLayer::DrawFeasibleAreaOverlay() const {
if (feasibleAreaOverlayBounds_.empty()) {
return;
}
Vec2 drawOrigin(-feasibleAreaOverlayBounds_.origin.x,
-feasibleAreaOverlayBounds_.origin.y);
for (const auto& rect : feasibleAreaFillRects_) { for (const auto& rect : feasibleAreaFillRects_) {
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, Rect drawRect(drawOrigin.x + rect.origin.x, drawOrigin.y + rect.origin.y,
rect.width(), rect.height()); rect.width(), rect.height());
Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha)); Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha));
} }
if (feasibleAreaPolygon_.empty()) {
return;
}
Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha);
DrawPolygonOutline(feasibleAreaPolygon_, drawOrigin, outlineColor);
DrawPolygonVertices(feasibleAreaPolygon_, drawOrigin, outlineColor);
}
void GameMapLayer::DrawMoveAreaOverlay() const {
if (moveAreaOverlayBounds_.empty()) {
return;
}
Vec2 drawOrigin(-moveAreaOverlayBounds_.origin.x, -moveAreaOverlayBounds_.origin.y);
for (const auto& moveArea : moveAreaInfoList_) { for (const auto& moveArea : moveAreaInfoList_) {
bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_; bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_;
Color fillColor = Color fillColor =
isHighlighted ? Color(0.0f, 1.0f, 1.0f, 0.60f) isHighlighted ? Color(0.0f, 1.0f, 1.0f, 0.60f)
: Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha); : Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha);
Rect drawRect(worldOrigin.x + moveArea.rect.origin.x, Rect drawRect(drawOrigin.x + moveArea.rect.origin.x,
worldOrigin.y + moveArea.rect.origin.y, drawOrigin.y + moveArea.rect.origin.y,
moveArea.rect.width(), moveArea.rect.height()); moveArea.rect.width(), moveArea.rect.height());
Renderer::get().drawQuad(drawRect, fillColor); Renderer::get().drawQuad(drawRect, fillColor);
} }
if (!feasibleAreaPolygon_.empty()) { for (const auto& moveArea : moveAreaInfoList_) {
Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha); std::vector<Vec2> polygon = gameMath::BuildRectPolygon(moveArea.rect);
DrawPolygonOutline(feasibleAreaPolygon_, worldOrigin, outlineColor);
DrawPolygonVertices(feasibleAreaPolygon_, worldOrigin, outlineColor);
}
for (auto& moveArea : moveAreaInfoList_) {
std::vector<Vec2> polygon = BuildRectPolygon(moveArea.rect);
if (polygon.empty()) { if (polygon.empty()) {
continue; continue;
} }
bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_; bool isHighlighted = moveArea.index == highlightedMoveAreaIndex_;
Color moveAreaOutlineColor = Color moveAreaOutlineColor =
isHighlighted ? Color(0.0f, 1.0f, 1.0f, 1.0f) isHighlighted ? Color(0.0f, 1.0f, 1.0f, 1.0f)
: Color(0.0f, 0.0f, 1.0f, kDebugOutlineAlpha); : Color(0.0f, 0.0f, 1.0f, kDebugOutlineAlpha);
DrawPolygonOutline(polygon, worldOrigin, moveAreaOutlineColor); DrawPolygonOutline(polygon, drawOrigin, moveAreaOutlineColor);
DrawPolygonVertices(polygon, worldOrigin, moveAreaOutlineColor); DrawPolygonVertices(polygon, drawOrigin, moveAreaOutlineColor);
} }
} }
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) { void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
feasibleAreaPolygon_ = polygon; feasibleAreaPolygon_ = polygon;
feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_); feasibleAreaFillRects_ = gameMath::BuildPolygonFillRects(feasibleAreaPolygon_);
feasibleAreaOverlayDirty_ = true;
} }
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) { void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) {
@@ -179,10 +304,16 @@ void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) {
debugArea.index = index; debugArea.index = index;
moveAreaInfoList_.push_back(std::move(debugArea)); moveAreaInfoList_.push_back(std::move(debugArea));
moveAreaOverlayDirty_ = true;
} }
void GameMapLayer::SetDebugHighlightedMoveAreaIndex(size_t index) { void GameMapLayer::SetDebugHighlightedMoveAreaIndex(size_t index) {
if (highlightedMoveAreaIndex_ == index) {
return;
}
highlightedMoveAreaIndex_ = index; highlightedMoveAreaIndex_ = index;
moveAreaOverlayDirty_ = true;
} }
void GameMapLayer::ClearDebugAreaInfo() { void GameMapLayer::ClearDebugAreaInfo() {
@@ -190,6 +321,17 @@ void GameMapLayer::ClearDebugAreaInfo() {
feasibleAreaFillRects_.clear(); feasibleAreaFillRects_.clear();
moveAreaInfoList_.clear(); moveAreaInfoList_.clear();
highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex; highlightedMoveAreaIndex_ = kInvalidMoveAreaIndex;
feasibleAreaOverlayBounds_ = Rect::Zero();
moveAreaOverlayBounds_ = Rect::Zero();
feasibleAreaOverlayDirty_ = true;
moveAreaOverlayDirty_ = true;
if (feasibleAreaOverlayCanvas_) {
feasibleAreaOverlayCanvas_->SetVisible(false);
}
if (moveAreaOverlayCanvas_) {
moveAreaOverlayCanvas_->SetVisible(false);
}
} }
void GameMapLayer::AddObject(RefPtr<Actor> obj) { void GameMapLayer::AddObject(RefPtr<Actor> obj) {

View File

@@ -0,0 +1,283 @@
#include "npc/NpcAnimation.h"
#include <frostbite2D/graphics/renderer.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
namespace frostbite2D {
namespace {
constexpr float kCompositePadding = 1.0f;
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
Vec2 p0 = transform.transformPoint(Vec2(rect.left(), rect.top()));
Vec2 p1 = transform.transformPoint(Vec2(rect.right(), rect.top()));
Vec2 p2 = transform.transformPoint(Vec2(rect.left(), rect.bottom()));
Vec2 p3 = transform.transformPoint(Vec2(rect.right(), rect.bottom()));
float minX = std::min(std::min(p0.x, p1.x), std::min(p2.x, p3.x));
float minY = std::min(std::min(p0.y, p1.y), std::min(p2.y, p3.y));
float maxX = std::max(std::max(p0.x, p1.x), std::max(p2.x, p3.x));
float maxY = std::max(std::max(p0.y, p1.y), std::max(p2.y, p3.y));
return Rect(minX, minY, maxX - minX, maxY - minY);
}
void combineHash(uint64& seed, uint64 value) {
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}
NpcAnimation::CompositeFrameInfo combineCompositeFrameInfo(
const NpcAnimation::CompositeFrameInfo& lhs,
const NpcAnimation::CompositeFrameInfo& rhs) {
if (!lhs.valid) {
return rhs;
}
if (!rhs.valid) {
return lhs;
}
float left = std::floor(std::min(lhs.localBounds.left(), rhs.localBounds.left()));
float top = std::floor(std::min(lhs.localBounds.top(), rhs.localBounds.top()));
float right = std::ceil(std::max(lhs.localBounds.right(), rhs.localBounds.right()));
float bottom =
std::ceil(std::max(lhs.localBounds.bottom(), rhs.localBounds.bottom()));
NpcAnimation::CompositeFrameInfo result;
result.valid = true;
result.localBounds =
Rect(left, top, std::max(right - left, 1.0f), std::max(bottom - top, 1.0f));
result.width =
std::max(static_cast<int>(std::lround(result.localBounds.width())), 1);
result.height =
std::max(static_cast<int>(std::lround(result.localBounds.height())), 1);
result.originInTexture =
Vec2(-result.localBounds.left(), -result.localBounds.top());
result.groundAnchorInTexture = result.originInTexture;
return result;
}
} // namespace
bool NpcAnimation::Init(const npc::NpcConfig& config) {
RemoveAllChildren();
displayAnimation_ = nullptr;
compositeCanvas_ = nullptr;
compositeFrameInfo_ = CompositeFrameInfo();
animationPath_.clear();
direction_ = 1;
compositeDirty_ = true;
compositeVersion_ = 0;
lastCompositeSignature_ = 0;
if (config.fieldAnimationPath.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcAnimation: npc %d missing field animation path", config.id);
return false;
}
compositeCanvas_ = MakePtr<CanvasActor>();
if (!compositeCanvas_ || !compositeCanvas_->Init(1, 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcAnimation: failed to initialize composite canvas for npc %d",
config.id);
compositeCanvas_.Reset();
} else {
compositeCanvas_->SetCustomDrawCallback([this]() {
RenderCurrentAnimationToCompositeCanvas();
});
}
auto animation = MakePtr<Animation>(config.fieldAnimationPath);
if (!animation || !animation->IsUsable()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcAnimation: failed to load animation %s for npc %d",
config.fieldAnimationPath.c_str(), config.id);
return false;
}
animationPath_ = config.fieldAnimationPath;
SDL_Log("NpcAnimation: npc %d loaded display animation %s",
config.id, animationPath_.c_str());
animation->SetVisible(true);
AddChild(animation);
displayAnimation_ = animation;
SetDirection(1);
displayAnimation_->Reset();
SetDirection(direction_);
CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(1);
CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(-1);
compositeFrameInfo_ = combineCompositeFrameInfo(rightInfo, leftInfo);
lastCompositeSignature_ = CaptureCurrentRenderSignature();
return true;
}
void NpcAnimation::Update(float deltaTime) {
Actor::Update(deltaTime);
uint64 renderSignature = CaptureCurrentRenderSignature();
if (renderSignature != lastCompositeSignature_) {
compositeDirty_ = true;
}
}
void NpcAnimation::Render() {
if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) {
RefreshCompositeTextureIfNeeded();
}
Actor::Render();
}
void NpcAnimation::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1;
if (displayAnimation_) {
displayAnimation_->SetDirection(direction_);
}
MarkCompositeDirty();
}
bool NpcAnimation::IsAnimationFinished() const {
return displayAnimation_ ? !displayAnimation_->IsUsable() : false;
}
bool NpcAnimation::GetDisplayLocalBounds(Rect& outBounds) const {
if (!displayAnimation_) {
outBounds = Rect();
return false;
}
return displayAnimation_->GetStaticLocalBounds(outBounds);
}
bool NpcAnimation::EnsureCompositeTextureReady() {
if (!Renderer::get().isFrameActive()) {
return HasCompositeTexture();
}
RefreshCompositeTextureIfNeeded();
return HasCompositeTexture();
}
bool NpcAnimation::HasCompositeTexture() const {
return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
GetCurrentCompositeFrameInfo().valid;
}
Ptr<Texture> NpcAnimation::GetCompositeTexture() const {
return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr;
}
Vec2 NpcAnimation::GetCompositeTextureSize() const {
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
return Vec2(static_cast<float>(info.width), static_cast<float>(info.height));
}
Vec2 NpcAnimation::GetCompositeGroundAnchorInTexture() const {
return GetCurrentCompositeFrameInfo().groundAnchorInTexture;
}
NpcAnimation::CompositeFrameInfo NpcAnimation::ComputeCompositeFrameInfo(
int direction) const {
CompositeFrameInfo info;
if (!displayAnimation_) {
return info;
}
Rect animationBounds;
if (!displayAnimation_->GetStaticLocalBounds(direction, animationBounds)) {
return info;
}
Rect bounds =
transformRectBounds(animationBounds, displayAnimation_->GetLocalTransform());
if (bounds.empty()) {
return info;
}
float left = std::floor(bounds.left() - kCompositePadding);
float top = std::floor(bounds.top() - kCompositePadding);
float right = std::ceil(bounds.right() + kCompositePadding);
float bottom = std::ceil(bounds.bottom() + kCompositePadding);
info.valid = true;
info.localBounds = Rect(left, top, std::max(right - left, 1.0f),
std::max(bottom - top, 1.0f));
info.width = std::max(static_cast<int>(std::lround(info.localBounds.width())), 1);
info.height = std::max(static_cast<int>(std::lround(info.localBounds.height())), 1);
info.originInTexture = Vec2(-info.localBounds.left(), -info.localBounds.top());
info.groundAnchorInTexture = info.originInTexture;
return info;
}
uint64 NpcAnimation::CaptureCurrentRenderSignature() const {
uint64 signature = 1469598103934665603ULL;
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
if (displayAnimation_) {
combineHash(signature, displayAnimation_->GetRenderSignature());
}
return signature;
}
void NpcAnimation::MarkCompositeDirty() {
compositeDirty_ = true;
if (compositeCanvas_) {
compositeCanvas_->SetDirty();
}
}
void NpcAnimation::UpdateCompositeCamera() {
if (!compositeCanvas_) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
Camera& camera = compositeCanvas_->GetCanvasCamera();
camera.setViewport(std::max(info.width, 1), std::max(info.height, 1));
camera.setZoom(1.0f);
Vec2 cameraOrigin = GetWorldTransform().transformPoint(
Vec2(info.localBounds.left(), info.localBounds.top()));
camera.setPosition(cameraOrigin);
}
void NpcAnimation::RefreshCompositeTextureIfNeeded() {
if (!compositeDirty_ || !compositeCanvas_) {
return;
}
if (!Renderer::get().isFrameActive()) {
return;
}
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
if (!info.valid) {
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
return;
}
if (!compositeCanvas_->SetCanvasSize(std::max(info.width, 1),
std::max(info.height, 1))) {
return;
}
UpdateCompositeCamera();
if (!compositeCanvas_->Redraw()) {
return;
}
compositeDirty_ = false;
lastCompositeSignature_ = CaptureCurrentRenderSignature();
++compositeVersion_;
}
void NpcAnimation::RenderCurrentAnimationToCompositeCanvas() {
if (!displayAnimation_) {
return;
}
RenderChildren();
}
} // namespace frostbite2D

View File

@@ -0,0 +1,217 @@
#include "npc/NpcDataLoader.h"
#include <SDL2/SDL.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace frostbite2D::npc {
namespace {
constexpr char kNpcListPath[] = "npc/npc.lst";
constexpr char kTagName[] = "[name]";
constexpr char kTagFieldName[] = "[field name]";
constexpr char kTagFieldAnimation[] = "[field animation]";
std::string toLowerCase(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
std::string trim(const std::string& value) {
size_t start = 0;
size_t end = value.size();
while (start < end && std::isspace(static_cast<unsigned char>(value[start]))) {
++start;
}
while (end > start &&
std::isspace(static_cast<unsigned char>(value[end - 1]))) {
--end;
}
return value.substr(start, end - start);
}
int toInt(const std::string& value, int fallback = 0) {
try {
return std::stoi(value);
} catch (...) {
return fallback;
}
}
bool isToken(const std::string& token, const char* expected) {
return trim(token) == expected;
}
std::string normalizeNpcPath(const std::string& path) {
auto& pvf = PvfArchive::get();
std::string normalized = pvf.normalizePath(path);
if (normalized.rfind("npc/", 0) == 0) {
return normalized;
}
return pvf.normalizePath("npc/" + normalized);
}
void logTokenPreview(const char* label, const std::vector<std::string>& tokens) {
std::string preview;
size_t previewCount = std::min<size_t>(tokens.size(), 8);
for (size_t i = 0; i < previewCount; ++i) {
if (!preview.empty()) {
preview += " | ";
}
preview += tokens[i];
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "%s: %s", label, preview.c_str());
}
class ScriptTokenStream {
public:
explicit ScriptTokenStream(const std::string& path)
: path_(toLowerCase(path)) {
auto& pvf = PvfArchive::get();
auto rawData = pvf.getFileRawData(path_);
if (!rawData) {
return;
}
ScriptParser parser(*rawData, path_);
if (!parser.isValid()) {
return;
}
for (const auto& value : parser.parseAll()) {
tokens_.push_back(value.toString());
}
valid_ = true;
}
bool isValid() const { return valid_; }
bool isEnd() const { return index_ >= tokens_.size(); }
std::string get() {
if (isEnd()) {
return {};
}
return tokens_[index_++];
}
private:
std::string path_;
std::vector<std::string> tokens_;
size_t index_ = 0;
bool valid_ = false;
};
std::map<int, std::string>& npcIndexCache() {
static std::map<int, std::string> cache;
static bool loaded = false;
if (!loaded) {
ScriptTokenStream stream(kNpcListPath);
if (stream.isValid()) {
while (!stream.isEnd()) {
std::string indexToken = stream.get();
if (indexToken.empty()) {
break;
}
std::string pathToken = stream.get();
if (pathToken.empty()) {
break;
}
cache[toInt(indexToken, -1)] = normalizeNpcPath(pathToken);
}
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcDataLoader: npc index %s not found", kNpcListPath);
}
loaded = true;
}
return cache;
}
std::map<int, NpcConfig>& npcConfigCache() {
static std::map<int, NpcConfig> cache;
return cache;
}
} // namespace
bool loadNpcIndex(std::map<int, std::string>& outIndex) {
outIndex = npcIndexCache();
if (!outIndex.empty()) {
SDL_Log("NpcDataLoader: loaded npc index entries=%d",
static_cast<int>(outIndex.size()));
}
return !outIndex.empty();
}
std::optional<NpcConfig> loadNpcConfig(int npcId) {
auto cacheIt = npcConfigCache().find(npcId);
if (cacheIt != npcConfigCache().end()) {
return cacheIt->second;
}
auto indexIt = npcIndexCache().find(npcId);
if (indexIt == npcIndexCache().end()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcDataLoader: npc %d not found in index", npcId);
return std::nullopt;
}
ScriptTokenStream stream(indexIt->second);
if (!stream.isValid()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcDataLoader: unable to load npc config %s",
indexIt->second.c_str());
return std::nullopt;
}
NpcConfig config;
config.id = npcId;
config.path = indexIt->second;
std::vector<std::string> tokenPreview;
while (!stream.isEnd()) {
std::string segment = stream.get();
if (tokenPreview.size() < 8) {
tokenPreview.push_back(segment);
}
if (isToken(segment, kTagName)) {
config.name = stream.get();
} else if (isToken(segment, kTagFieldName)) {
config.fieldName = stream.get();
} else if (isToken(segment, kTagFieldAnimation)) {
config.fieldAnimationPath = normalizeNpcPath(stream.get());
}
}
if (config.fieldName.empty()) {
config.fieldName = config.name;
}
if (config.name.empty()) {
config.name = config.fieldName;
}
if (config.fieldAnimationPath.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcDataLoader: npc %d config incomplete (name=%s, fieldName=%s, script=%s)",
npcId, config.name.c_str(), config.fieldName.c_str(),
indexIt->second.c_str());
logTokenPreview("NpcDataLoader token preview", tokenPreview);
return std::nullopt;
}
SDL_Log("NpcDataLoader: npc %d path=%s fieldAnimation=%s",
npcId, config.path.c_str(), config.fieldAnimationPath.c_str());
npcConfigCache()[npcId] = config;
return config;
}
} // namespace frostbite2D::npc

281
Game/src/npc/NpcObject.cpp Normal file
View File

@@ -0,0 +1,281 @@
#include "npc/NpcObject.h"
#include "common/math/GameMath.h"
#include <SDL2/SDL.h>
namespace frostbite2D {
namespace {
constexpr float kNpcNameMarginY = 8.0f;
constexpr float kNpcNameFallbackTopY = -96.0f;
const Vec2 kNpcNameOutlineOffsets[] = {
Vec2(-1.0f, -1.0f), Vec2(-1.0f, 0.0f), Vec2(-1.0f, 1.0f),
Vec2(0.0f, -1.0f), Vec2(0.0f, 1.0f),
Vec2(1.0f, -1.0f), Vec2(1.0f, 0.0f), Vec2(1.0f, 1.0f),
};
void ConfigureNameTextLabel(RefPtr<TextSprite> label,
const char* name,
const Color& color,
int zOrder) {
if (!label) {
return;
}
label->SetName(name);
label->SetFont("default");
label->SetAnchor(0.5f, 1.0f);
label->SetTextColor(color);
label->SetZOrder(zOrder);
}
const std::string& EmptyString() {
static const std::string kEmpty;
return kEmpty;
}
} // namespace
bool NpcObject::Construction(int npcId) {
RemoveAllChildren();
npcId_ = -1;
direction_ = 1;
worldPosition_ = CharacterWorldPosition();
config_.reset();
animation_ = nullptr;
interactionHighlight_ = nullptr;
nameOutlineLabels_.fill(nullptr);
nameLabel_ = nullptr;
interactable_ = true;
interacting_ = false;
interactionHighlighted_ = false;
syncedHighlightCompositeVersion_ = 0;
auto config = npc::loadNpcConfig(npcId);
if (!config) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcObject: failed to load npc config %d", npcId);
return false;
}
auto animation = MakePtr<NpcAnimation>();
if (!animation->Init(*config)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcObject: failed to init npc animation %d", npcId);
return false;
}
SDL_Log("NpcObject: npc %d config loaded name=%s fieldName=%s ani=%s",
npcId, config->name.c_str(), config->fieldName.c_str(),
config->fieldAnimationPath.c_str());
config_ = *config;
npcId_ = npcId;
animation_ = animation;
EnsureInteractionHighlight();
AddChild(animation_);
EnsureNameLabel();
SetWorldPosition({});
SetDirection(1);
SetInteractionHighlighted(false);
RefreshNameLabel();
SDL_Log("NpcObject: npc %d construction complete", npcId_);
return true;
}
void NpcObject::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1;
if (animation_) {
animation_->SetDirection(direction_);
}
RefreshNameLabel();
}
void NpcObject::SetNpcPosition(const Vec2& pos) {
worldPosition_.x = gameMath::RoundWorldCoordinate(pos.x);
worldPosition_.y = gameMath::RoundWorldCoordinate(pos.y);
SDL_Log("NpcObject: npc %d set ground position to (%d, %d, %d)",
npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z);
SyncActorPositionFromWorld();
}
void NpcObject::SetWorldPosition(const CharacterWorldPosition& pos) {
worldPosition_ = pos;
SDL_Log("NpcObject: npc %d set world position to (%d, %d, %d)",
npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z);
SyncActorPositionFromWorld();
}
void NpcObject::SetInteractable(bool interactable) {
interactable_ = interactable;
if (!interactable_) {
interacting_ = false;
}
}
void NpcObject::SetInteractionHighlighted(bool highlighted) {
if (interactionHighlighted_ == highlighted) {
SyncInteractionHighlight();
return;
}
interactionHighlighted_ = highlighted;
if (!interactionHighlighted_) {
syncedHighlightCompositeVersion_ = 0;
}
SyncInteractionHighlight();
}
void NpcObject::BeginInteract() {
if (CanInteract()) {
interacting_ = true;
}
}
void NpcObject::EndInteract() {
interacting_ = false;
}
bool NpcObject::CanInteract() const {
return interactable_ && config_.has_value();
}
const std::string& NpcObject::GetName() const {
return config_ ? config_->name : EmptyString();
}
const std::string& NpcObject::GetFieldName() const {
return config_ ? config_->fieldName : EmptyString();
}
const std::string& NpcObject::GetDisplayName() const {
const std::string& fieldName = GetFieldName();
if (!fieldName.empty()) {
return fieldName;
}
return GetName();
}
bool NpcObject::IsAnimationFinished() const {
return animation_ ? animation_->IsAnimationFinished() : false;
}
void NpcObject::Update(float deltaTime) {
Actor::Update(deltaTime);
}
void NpcObject::Render() {
SyncInteractionHighlight();
Actor::Render();
}
void NpcObject::EnsureInteractionHighlight() {
if (interactionHighlight_) {
return;
}
interactionHighlight_ = MakePtr<InteractionHighlightSprite>();
interactionHighlight_->SetZOrder(-1);
interactionHighlight_->SetPosition(0.0f, 0.0f);
interactionHighlight_->SetVisible(false);
AddChild(interactionHighlight_);
}
void NpcObject::EnsureNameLabel() {
if (nameLabel_) {
return;
}
for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) {
RefPtr<TextSprite> outlineLabel = TextSprite::create();
ConfigureNameTextLabel(outlineLabel, "npcNameOutline",
Color(0.0f, 0.0f, 0.0f, 1.0f), 1);
nameOutlineLabels_[i] = outlineLabel;
AddChild(outlineLabel);
}
nameLabel_ = TextSprite::create();
ConfigureNameTextLabel(nameLabel_, "npcName",
Color(1.0f, 0.96f, 0.78f, 1.0f), 2);
AddChild(nameLabel_);
}
void NpcObject::RefreshNameLabel() {
if (!nameLabel_) {
return;
}
const std::string& displayName = GetDisplayName();
bool hasDisplayName = !displayName.empty();
nameLabel_->SetVisible(hasDisplayName);
for (auto& outlineLabel : nameOutlineLabels_) {
if (outlineLabel) {
outlineLabel->SetVisible(hasDisplayName);
}
}
if (!hasDisplayName) {
return;
}
nameLabel_->SetText(displayName);
for (auto& outlineLabel : nameOutlineLabels_) {
if (outlineLabel) {
outlineLabel->SetText(displayName);
}
}
Rect displayBounds;
float labelBaseY = kNpcNameFallbackTopY;
if (animation_ && animation_->GetDisplayLocalBounds(displayBounds)) {
labelBaseY = displayBounds.top() - kNpcNameMarginY;
}
nameLabel_->SetPosition(0.0f, labelBaseY);
for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) {
if (!nameOutlineLabels_[i]) {
continue;
}
const Vec2& offset = kNpcNameOutlineOffsets[i];
nameOutlineLabels_[i]->SetPosition(offset.x, labelBaseY + offset.y);
}
}
void NpcObject::SyncInteractionHighlight() {
if (!interactionHighlight_) {
return;
}
if (!interactionHighlighted_ || !animation_ ||
!animation_->EnsureCompositeTextureReady() ||
!animation_->HasCompositeTexture()) {
interactionHighlight_->SetVisible(false);
return;
}
uint64 compositeVersion = animation_->GetCompositeVersion();
if (syncedHighlightCompositeVersion_ != compositeVersion) {
Ptr<Texture> compositeTexture = animation_->GetCompositeTexture();
Vec2 compositeSize = animation_->GetCompositeTextureSize();
if (!compositeTexture || compositeSize.x <= 0.0f || compositeSize.y <= 0.0f) {
interactionHighlight_->SetVisible(false);
return;
}
interactionHighlight_->SetTexture(compositeTexture);
interactionHighlight_->SetSize(compositeSize);
interactionHighlight_->SetGroundAnchorInTexture(
animation_->GetCompositeGroundAnchorInTexture());
interactionHighlight_->SetPosition(0.0f, 0.0f);
syncedHighlightCompositeVersion_ = compositeVersion;
}
interactionHighlight_->SetVisible(true);
}
void NpcObject::SyncActorPositionFromWorld() {
SetPosition(worldPosition_.ToScreenPosition());
SetZOrder(worldPosition_.y);
}
} // namespace frostbite2D

View File

@@ -1,6 +1,6 @@
#include "scene/GameDebugUIScene.h" #include "scene/GameDebugUIScene.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "common/GameDebugActor.h" #include "common/debug/GameDebugActor.h"
#include "map/GameMap.h" #include "map/GameMap.h"
namespace frostbite2D { namespace frostbite2D {

View File

@@ -7,7 +7,11 @@ namespace frostbite2D {
namespace { namespace {
constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map"; constexpr char kTestMapPath[] = "map/cataclysm/town/seria_room/elvengard.map";
constexpr int kTestNpcId = 2;
constexpr float kTestNpcOffsetX = 180.0f;
constexpr float kTestNpcOffsetY = -24.0f;
constexpr float kNpcInteractionHighlightRadius = 40.0f;
} // namespace } // namespace
@@ -24,6 +28,9 @@ void GameMapTestScene::onEnter() {
SceneManager::get().PushUIScene(debugScene_); SceneManager::get().PushUIScene(debugScene_);
if (initialized_) { if (initialized_) {
cameraController_.SetMap(map_.Get());
cameraController_.SetTarget(character_.Get());
cameraController_.SetDebugEnabled(false);
debugScene_->SetDebugContext(map_.Get(), character_.Get()); debugScene_->SetDebugContext(map_.Get(), character_.Get());
return; return;
} }
@@ -41,6 +48,8 @@ void GameMapTestScene::onEnter() {
} }
AddChild(map_); AddChild(map_);
character_.Reset();
npc_.Reset();
{ {
ScopedStartupTrace stageTrace("GameMapTestScene character construction"); ScopedStartupTrace stageTrace("GameMapTestScene character construction");
character_ = MakePtr<CharacterObject>(); character_ = MakePtr<CharacterObject>();
@@ -49,8 +58,7 @@ void GameMapTestScene::onEnter() {
"GameMapTestScene: failed to construct default character"); "GameMapTestScene: failed to construct default character");
character_.Reset(); character_.Reset();
} else { } else {
Vec2 spawnPos = Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.0f);
map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
character_->SetCharacterPosition(spawnPos); character_->SetCharacterPosition(spawnPos);
character_->EnableEventReceive(); character_->EnableEventReceive();
character_->SetEventPriority(-100); character_->SetEventPriority(-100);
@@ -58,8 +66,29 @@ void GameMapTestScene::onEnter() {
} }
} }
{
ScopedStartupTrace stageTrace("GameMapTestScene npc construction");
npc_ = MakePtr<NpcObject>();
if (!npc_->Construction(kTestNpcId)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to construct npc %d", kTestNpcId);
npc_.Reset();
} else {
Vec2 npcPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.0f);
if (character_) {
npcPos = character_->GetWorldPosition().ToGroundPosition() +
Vec2(kTestNpcOffsetX, kTestNpcOffsetY);
}
npc_->SetNpcPosition(npcPos);
npc_->SetDirection(-1);
SDL_Log("GameMapTestScene: npc %d final ground position (%d, %d, %d)",
kTestNpcId, npc_->GetWorldPosition().x, npc_->GetWorldPosition().y,
npc_->GetWorldPosition().z);
map_->AddObject(npc_);
}
}
cameraController_.SetMap(map_.Get()); cameraController_.SetMap(map_.Get());
// cameraController_.SetZoom(1.2f);
cameraController_.SetTarget(character_.Get()); cameraController_.SetTarget(character_.Get());
cameraController_.SetDebugEnabled(false); cameraController_.SetDebugEnabled(false);
if (character_) { if (character_) {
@@ -87,6 +116,18 @@ void GameMapTestScene::onExit() {
void GameMapTestScene::Update(float deltaTime) { void GameMapTestScene::Update(float deltaTime) {
Scene::Update(deltaTime); Scene::Update(deltaTime);
if (npc_) {
bool shouldHighlight = false;
if (character_) {
shouldHighlight =
character_->GetWorldPosition().ToGroundPosition().distance(
npc_->GetWorldPosition().ToGroundPosition()) <=
kNpcInteractionHighlightRadius;
}
npc_->SetInteractionHighlighted(shouldHighlight);
}
cameraController_.Update(deltaTime); cameraController_.Update(deltaTime);
} }

View File

@@ -15,7 +15,9 @@ Vec2 RoundWorldPoint(const Vec2& pos) {
} // namespace } // namespace
GameTown::GameTown() = default; GameTown::GameTown() {
EnableEventReceive();
}
bool GameTown::Init(int index, const std::string& townPath) { bool GameTown::Init(int index, const std::string& townPath) {
game::TownConfig config; game::TownConfig config;
@@ -68,10 +70,25 @@ RefPtr<GameMap> GameTown::GetArea(int index) const {
return it->map; return it->map;
} }
RefPtr<GameMap> GameTown::GetCurrentArea() const {
if (curMapIndex_ == -1) {
return nullptr;
}
return GetArea(curMapIndex_);
}
void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) { void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
int targetMapIndex = areaIndex; int targetMapIndex = areaIndex;
if (areaIndex == -2) { if (areaIndex == -2) {
targetMapIndex = sariaRoomId_ != -1 ? sariaRoomId_ : (mapList_.empty() ? -1 : mapList_.front().areaId); if (sariaRoomId_ != -1) {
targetMapIndex = sariaRoomId_;
} else {
targetMapIndex = mapList_.empty() ? -1 : mapList_.front().areaId;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameTown: no gate area configured, fallback to first area %d",
targetMapIndex);
}
} }
auto mapIt = std::find_if(mapList_.begin(), mapList_.end(), auto mapIt = std::find_if(mapList_.begin(), mapList_.end(),
@@ -100,7 +117,6 @@ void GameTown::AddCharacter(RefPtr<Actor> actor, int areaIndex) {
AddChild(mapIt->map); AddChild(mapIt->map);
cameraController_.SetMap(mapIt->map.Get()); cameraController_.SetMap(mapIt->map.Get());
cameraController_.SetZoom(1.2f);
cameraController_.SetTarget(actor.Get()); cameraController_.SetTarget(actor.Get());
cameraController_.SnapToDefaultFocus(); cameraController_.SnapToDefaultFocus();

View File

@@ -1,22 +1,57 @@
#include "world/GameWorld.h" #include "world/GameWorld.h"
#include "character/CharacterObject.h"
#include "map/GameDataLoader.h" #include "map/GameDataLoader.h"
#include "scene/GameDebugUIScene.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/scene/scene_manager.h>
namespace frostbite2D { namespace frostbite2D {
namespace {
constexpr int kDefaultTownId = 1;
Vec2 ComputeMoveAreaCenter(const game::MoveAreaBounds& bounds) {
return Vec2((bounds.leftTop.x + bounds.rightBottom.x) * 0.5f,
(bounds.leftTop.y + bounds.rightBottom.y) * 0.5f);
}
void SetActorGroundPosition(RefPtr<Actor> actor, const Vec2& position) {
if (!actor) {
return;
}
if (auto* character = dynamic_cast<CharacterObject*>(actor.Get())) {
character->SetCharacterPosition(position);
return;
}
actor->SetPosition(position);
}
} // namespace
GameWorld::GameWorld() = default; GameWorld::GameWorld() = default;
void GameWorld::onEnter() { void GameWorld::onEnter() {
Scene::onEnter(); Scene::onEnter();
if (initialized_) { EnsureDebugScene();
return;
}
if (!initialized_) {
initialized_ = InitWorld(); initialized_ = InitWorld();
} }
RefreshDebugContext();
}
void GameWorld::onExit() { void GameWorld::onExit() {
pendingCharacterMove_.reset();
ClearSuppressedMoveArea();
if (debugScene_) {
debugScene_->ClearDebugContext();
SceneManager::get().RemoveUIScene(debugScene_.Get());
}
Scene::onExit(); Scene::onExit();
} }
@@ -26,6 +61,14 @@ void GameWorld::Update(float deltaTime) {
} }
bool GameWorld::InitWorld() { bool GameWorld::InitWorld() {
RemoveAllChildren();
townPathMap_.clear();
townMap_.clear();
mainActor_.Reset();
pendingCharacterMove_.reset();
ClearSuppressedMoveArea();
curTown_ = -1;
townPathMap_ = game::loadTownList(); townPathMap_ = game::loadTownList();
if (townPathMap_.empty()) { if (townPathMap_.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found");
@@ -45,7 +88,36 @@ bool GameWorld::InitWorld() {
return false; return false;
} }
AddCharacter(nullptr, townMap_.begin()->first); int defaultTownId = kDefaultTownId;
if (townMap_.find(defaultTownId) == townMap_.end()) {
defaultTownId = townMap_.begin()->first;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: default town %d missing, fallback to town %d",
kDefaultTownId, defaultTownId);
}
if (!InitMainCharacter(defaultTownId)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: falling back to empty town %d without main character",
defaultTownId);
AddCharacter(nullptr, defaultTownId);
}
return true;
}
bool GameWorld::InitMainCharacter(int townId) {
auto mainCharacter = MakePtr<CharacterObject>();
if (!mainCharacter->Construction(0)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: failed to construct default main character");
return false;
}
mainCharacter->EnableEventReceive();
mainCharacter->SetEventPriority(-100);
mainCharacter->SetInputEnabled(true);
AddCharacter(mainCharacter, townId);
return true; return true;
} }
@@ -60,6 +132,7 @@ void GameWorld::AddCharacter(RefPtr<Actor> actor, int townId) {
curTown_ = townId; curTown_ = townId;
it->second->AddCharacter(actor); it->second->AddCharacter(actor);
AddChild(it->second); AddChild(it->second);
RefreshDebugContext();
} }
void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) { void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
@@ -69,6 +142,44 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
return; return;
} }
ClearSuppressedMoveArea();
int sourceTownId = curTown_;
int sourceAreaId = -1;
if (curTown_ != -1) {
auto currentTown = townMap_.find(curTown_);
if (currentTown != townMap_.end()) {
sourceAreaId = currentTown->second->GetCurAreaIndex();
}
}
RefPtr<GameMap> targetMap = townIt->second->GetArea(area);
if (actor && targetMap && sourceTownId != -1 && sourceAreaId != -1) {
bool foundTransitionEntry = false;
const auto& moveAreaInfo = targetMap->GetMoveAreaInfo();
for (size_t i = 0; i < moveAreaInfo.size(); ++i) {
if (moveAreaInfo[i].town != sourceTownId || moveAreaInfo[i].area != sourceAreaId) {
continue;
}
game::MoveAreaBounds bounds = targetMap->GetMovablePositionBounds(i);
SetActorGroundPosition(actor, ComputeMoveAreaCenter(bounds));
SetSuppressedMoveArea(targetMap.Get(), i);
foundTransitionEntry = true;
break;
}
if (!foundTransitionEntry) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: no matching move area in town %d area %d for source town %d area %d",
townId, area, sourceTownId, sourceAreaId);
}
} else if (actor && !targetMap) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameWorld: target area %d missing in town %d, use town fallback spawn",
area, townId);
}
if (curTown_ != -1) { if (curTown_ != -1) {
auto currentTown = townMap_.find(curTown_); auto currentTown = townMap_.find(curTown_);
if (currentTown != townMap_.end()) { if (currentTown != townMap_.end()) {
@@ -80,6 +191,7 @@ void GameWorld::MoveCharacter(RefPtr<Actor> actor, int townId, int area) {
mainActor_ = actor; mainActor_ = actor;
townIt->second->AddCharacter(actor, area); townIt->second->AddCharacter(actor, area);
AddChild(townIt->second); AddChild(townIt->second);
RefreshDebugContext();
} }
void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area) { void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area) {
@@ -92,6 +204,22 @@ void GameWorld::RequestMoveCharacter(RefPtr<Actor> actor, int townId, int area)
pendingCharacterMove_ = PendingCharacterMove{actor, townId, area}; pendingCharacterMove_ = PendingCharacterMove{actor, townId, area};
} }
void GameWorld::UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex) {
if (!suppressedMoveArea_) {
return;
}
if (suppressedMoveArea_->map != map ||
suppressedMoveArea_->moveAreaIndex != moveAreaIndex) {
ClearSuppressedMoveArea();
}
}
bool GameWorld::ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const {
return suppressedMoveArea_ && suppressedMoveArea_->map == map &&
suppressedMoveArea_->moveAreaIndex == moveAreaIndex;
}
void GameWorld::ProcessPendingCharacterMove() { void GameWorld::ProcessPendingCharacterMove() {
if (!pendingCharacterMove_) { if (!pendingCharacterMove_) {
return; return;
@@ -102,6 +230,63 @@ void GameWorld::ProcessPendingCharacterMove() {
MoveCharacter(move.actor, move.townId, move.area); MoveCharacter(move.actor, move.townId, move.area);
} }
void GameWorld::EnsureDebugScene() {
if (!debugScene_) {
debugScene_ = MakePtr<GameDebugUIScene>();
}
SceneManager::get().RemoveUIScene(debugScene_.Get());
SceneManager::get().PushUIScene(debugScene_);
}
void GameWorld::RefreshDebugContext() {
if (!debugScene_) {
return;
}
debugScene_->SetDebugContext(GetCurrentMap(), GetMainCharacter());
}
void GameWorld::SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex) {
if (!map || moveAreaIndex == GameMap::kInvalidMoveAreaIndex) {
ClearSuppressedMoveArea();
return;
}
suppressedMoveArea_ = SuppressedMoveArea{map, moveAreaIndex};
}
void GameWorld::ClearSuppressedMoveArea() {
suppressedMoveArea_.reset();
}
Actor* GameWorld::GetMainActor() const {
return mainActor_.Get();
}
CharacterObject* GameWorld::GetMainCharacter() const {
return dynamic_cast<CharacterObject*>(mainActor_.Get());
}
GameTown* GameWorld::GetCurrentTown() const {
auto it = townMap_.find(curTown_);
if (it == townMap_.end()) {
return nullptr;
}
return it->second.Get();
}
GameMap* GameWorld::GetCurrentMap() const {
GameTown* town = GetCurrentTown();
if (!town) {
return nullptr;
}
RefPtr<GameMap> area = town->GetCurrentArea();
return area.Get();
}
GameWorld* GameWorld::GetWorld() { GameWorld* GameWorld::GetWorld() {
return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene()); return dynamic_cast<GameWorld*>(SceneManager::get().GetCurrentScene());
} }