Compare commits
10 Commits
f86ce35b68
...
db0fd3a17e
| Author | SHA1 | Date | |
|---|---|---|---|
| db0fd3a17e | |||
| 5af657c5c9 | |||
| caad22cca7 | |||
| 6855860d64 | |||
| e570fec599 | |||
| 808431f92c | |||
| 6684abd131 | |||
| 875af43f88 | |||
| 62b0f6dafd | |||
| 35c80247b3 |
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
64
Frostbite2D/include/frostbite2D/2d/canvas_actor.h
Normal file
64
Frostbite2D/include/frostbite2D/2d/canvas_actor.h
Normal 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
|
||||||
@@ -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:
|
|||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
46
Frostbite2D/include/frostbite2D/graphics/render_resolution.h
Normal file
46
Frostbite2D/include/frostbite2D/graphics/render_resolution.h
Normal 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
|
||||||
72
Frostbite2D/include/frostbite2D/graphics/render_style.h
Normal file
72
Frostbite2D/include/frostbite2D/graphics/render_style.h
Normal 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
|
||||||
34
Frostbite2D/include/frostbite2D/graphics/render_texture.h
Normal file
34
Frostbite2D/include/frostbite2D/graphics/render_texture.h
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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_; ///< 随机组映射
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
141
Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp
Normal file
141
Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp
Normal 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
|
||||||
@@ -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_) {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ void TextSprite::RenderText() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (texture) {
|
if (texture) {
|
||||||
|
texture->setSampling(TextureSampling::Linear);
|
||||||
SetTexture(texture);
|
SetTexture(texture);
|
||||||
SetSizeToTexture();
|
SetSizeToTexture();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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_);
|
||||||
|
|||||||
@@ -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_);
|
||||||
}
|
}
|
||||||
|
|||||||
67
Frostbite2D/src/frostbite2D/graphics/render_texture.cpp
Normal file
67
Frostbite2D/src/frostbite2D/graphics/render_texture.cpp
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
48
Game/include/audio/MapAudioController.h
Normal file
48
Game/include/audio/MapAudioController.h
Normal 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
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
32
Game/include/character/CharacterShadowActor.h
Normal file
32
Game/include/character/CharacterShadowActor.h
Normal 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
|
||||||
20
Game/include/common/InteractionHighlightSprite.h
Normal file
20
Game/include/common/InteractionHighlightSprite.h
Normal 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
|
||||||
@@ -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
|
||||||
19
Game/include/common/math/GameMath.h
Normal file
19
Game/include/common/math/GameMath.h
Normal 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
|
||||||
@@ -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_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
56
Game/include/npc/NpcAnimation.h
Normal file
56
Game/include/npc/NpcAnimation.h
Normal 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
|
||||||
20
Game/include/npc/NpcDataLoader.h
Normal file
20
Game/include/npc/NpcDataLoader.h
Normal 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
|
||||||
68
Game/include/npc/NpcObject.h
Normal file
68
Game/include/npc/NpcObject.h
Normal 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
|
||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
172
Game/src/audio/MapAudioController.cpp
Normal file
172
Game/src/audio/MapAudioController.cpp
Normal 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
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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_) {
|
||||||
|
|||||||
61
Game/src/character/CharacterShadowActor.cpp
Normal file
61
Game/src/character/CharacterShadowActor.cpp
Normal 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
|
||||||
35
Game/src/common/InteractionHighlightSprite.cpp
Normal file
35
Game/src/common/InteractionHighlightSprite.cpp
Normal 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
|
||||||
@@ -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
|
||||||
148
Game/src/common/math/GameMath.cpp
Normal file
148
Game/src/common/math/GameMath.cpp
Normal 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
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
283
Game/src/npc/NpcAnimation.cpp
Normal file
283
Game/src/npc/NpcAnimation.cpp
Normal 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
|
||||||
217
Game/src/npc/NpcDataLoader.cpp
Normal file
217
Game/src/npc/NpcDataLoader.cpp
Normal 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
281
Game/src/npc/NpcObject.cpp
Normal 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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user