refactor: 移除示例游戏和动画系统相关代码
移除 push_box、flappy_bird 等示例游戏代码及相关资源文件 删除动画系统相关实现文件,包括动画解析器、帧渲染器等 清理不再使用的 Action 系统代码 移除 xmake.lua 中不再需要的示例项目配置
This commit is contained in:
parent
59f01900fb
commit
313a56bf72
|
|
@ -1,172 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/types.h>
|
||||
#include <functional>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
class Node;
|
||||
|
||||
/**
|
||||
* @brief 动作状态枚举
|
||||
*/
|
||||
enum class ActionState {
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Completed
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 动作基类
|
||||
*
|
||||
* 所有动作的基类,定义了动作的核心接口。
|
||||
* 动作用于修改 Node 的属性,实现动画效果。
|
||||
*/
|
||||
class Action {
|
||||
public:
|
||||
using CompletionCallback = std::function<void()>;
|
||||
|
||||
Action();
|
||||
virtual ~Action() = default;
|
||||
|
||||
Action(const Action&) = delete;
|
||||
Action& operator=(const Action&) = delete;
|
||||
Action(Action&&) = default;
|
||||
Action& operator=(Action&&) = default;
|
||||
|
||||
/**
|
||||
* @brief 检查动作是否完成
|
||||
* @return true 如果动作已完成
|
||||
*/
|
||||
virtual bool isDone() const;
|
||||
|
||||
/**
|
||||
* @brief 使用目标节点启动动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
virtual void startWithTarget(Node* target);
|
||||
|
||||
/**
|
||||
* @brief 停止动作
|
||||
*/
|
||||
virtual void stop();
|
||||
|
||||
/**
|
||||
* @brief 每帧调用的步进函数
|
||||
* @param dt 帧时间间隔
|
||||
*/
|
||||
virtual void step(float dt);
|
||||
|
||||
/**
|
||||
* @brief 更新动作状态
|
||||
* @param time 归一化时间 [0, 1]
|
||||
*/
|
||||
virtual void update(float time);
|
||||
|
||||
/**
|
||||
* @brief 克隆动作
|
||||
* @return 动作的深拷贝
|
||||
*/
|
||||
virtual Action* clone() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 创建反向动作
|
||||
* @return 反向动作
|
||||
*/
|
||||
virtual Action* reverse() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 暂停动作
|
||||
*/
|
||||
void pause();
|
||||
|
||||
/**
|
||||
* @brief 恢复动作
|
||||
*/
|
||||
void resume();
|
||||
|
||||
/**
|
||||
* @brief 重启动作
|
||||
*/
|
||||
void restart();
|
||||
|
||||
/**
|
||||
* @brief 设置完成回调
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
void setCompletionCallback(const CompletionCallback& callback) {
|
||||
completionCallback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取目标节点
|
||||
* @return 目标节点指针
|
||||
*/
|
||||
Node* getTarget() const { return target_; }
|
||||
|
||||
/**
|
||||
* @brief 获取原始目标节点
|
||||
* @return 原始目标节点指针
|
||||
*/
|
||||
Node* getOriginalTarget() const { return originalTarget_; }
|
||||
|
||||
/**
|
||||
* @brief 获取动作状态
|
||||
* @return 当前状态
|
||||
*/
|
||||
ActionState getState() const { return state_; }
|
||||
|
||||
/**
|
||||
* @brief 获取标签
|
||||
* @return 标签值
|
||||
*/
|
||||
int getTag() const { return tag_; }
|
||||
|
||||
/**
|
||||
* @brief 设置标签
|
||||
* @param tag 标签值
|
||||
*/
|
||||
void setTag(int tag) { tag_ = tag; }
|
||||
|
||||
/**
|
||||
* @brief 获取标志位
|
||||
* @return 标志位
|
||||
*/
|
||||
unsigned int getFlags() const { return flags_; }
|
||||
|
||||
/**
|
||||
* @brief 设置标志位
|
||||
* @param flags 标志位
|
||||
*/
|
||||
void setFlags(unsigned int flags) { flags_ = flags; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief 动作开始时调用
|
||||
*/
|
||||
virtual void onStart() {}
|
||||
|
||||
/**
|
||||
* @brief 动作完成时调用
|
||||
*/
|
||||
virtual void onComplete() {
|
||||
if (completionCallback_) {
|
||||
completionCallback_();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置动作完成状态
|
||||
*/
|
||||
void setDone() { state_ = ActionState::Completed; }
|
||||
|
||||
Node* target_ = nullptr;
|
||||
Node* originalTarget_ = nullptr;
|
||||
ActionState state_ = ActionState::Idle;
|
||||
int tag_ = -1;
|
||||
unsigned int flags_ = 0;
|
||||
CompletionCallback completionCallback_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action_interval.h>
|
||||
#include <extra2d/action/ease.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 缓动动作基类
|
||||
*
|
||||
* 使用装饰器模式包装其他动作,实现缓动效果。
|
||||
*/
|
||||
class ActionEase : public ActionInterval {
|
||||
public:
|
||||
virtual ~ActionEase();
|
||||
|
||||
/**
|
||||
* @brief 获取内部动作
|
||||
* @return 内部动作指针
|
||||
*/
|
||||
ActionInterval* getInnerAction() const { return innerAction_; }
|
||||
|
||||
void startWithTarget(Node* target) override;
|
||||
void stop() override;
|
||||
void update(float time) override;
|
||||
ActionInterval* clone() const override = 0;
|
||||
ActionInterval* reverse() const override = 0;
|
||||
|
||||
protected:
|
||||
ActionEase() = default;
|
||||
bool initWithAction(ActionInterval* action);
|
||||
void onUpdate(float progress) override {}
|
||||
|
||||
ActionInterval* innerAction_ = nullptr;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 指数缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseExponentialIn : public ActionEase {
|
||||
public:
|
||||
static EaseExponentialIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseExponentialOut : public ActionEase {
|
||||
public:
|
||||
static EaseExponentialOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseExponentialInOut : public ActionEase {
|
||||
public:
|
||||
static EaseExponentialInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 正弦缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseSineIn : public ActionEase {
|
||||
public:
|
||||
static EaseSineIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseSineOut : public ActionEase {
|
||||
public:
|
||||
static EaseSineOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseSineInOut : public ActionEase {
|
||||
public:
|
||||
static EaseSineInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 弹性缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseElasticIn : public ActionEase {
|
||||
public:
|
||||
static EaseElasticIn* create(ActionInterval* action, float period = 0.3f);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
|
||||
protected:
|
||||
float period_ = 0.3f;
|
||||
};
|
||||
|
||||
class EaseElasticOut : public ActionEase {
|
||||
public:
|
||||
static EaseElasticOut* create(ActionInterval* action, float period = 0.3f);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
|
||||
protected:
|
||||
float period_ = 0.3f;
|
||||
};
|
||||
|
||||
class EaseElasticInOut : public ActionEase {
|
||||
public:
|
||||
static EaseElasticInOut* create(ActionInterval* action, float period = 0.3f);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
|
||||
protected:
|
||||
float period_ = 0.3f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 弹跳缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseBounceIn : public ActionEase {
|
||||
public:
|
||||
static EaseBounceIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseBounceOut : public ActionEase {
|
||||
public:
|
||||
static EaseBounceOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseBounceInOut : public ActionEase {
|
||||
public:
|
||||
static EaseBounceInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 回震缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseBackIn : public ActionEase {
|
||||
public:
|
||||
static EaseBackIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseBackOut : public ActionEase {
|
||||
public:
|
||||
static EaseBackOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseBackInOut : public ActionEase {
|
||||
public:
|
||||
static EaseBackInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 二次缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseQuadIn : public ActionEase {
|
||||
public:
|
||||
static EaseQuadIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuadOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuadOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuadInOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuadInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 三次缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseCubicIn : public ActionEase {
|
||||
public:
|
||||
static EaseCubicIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseCubicOut : public ActionEase {
|
||||
public:
|
||||
static EaseCubicOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseCubicInOut : public ActionEase {
|
||||
public:
|
||||
static EaseCubicInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 四次缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseQuartIn : public ActionEase {
|
||||
public:
|
||||
static EaseQuartIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuartOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuartOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuartInOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuartInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 五次缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseQuintIn : public ActionEase {
|
||||
public:
|
||||
static EaseQuintIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuintOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuintOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseQuintInOut : public ActionEase {
|
||||
public:
|
||||
static EaseQuintInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 圆形缓动
|
||||
// ============================================================================
|
||||
|
||||
class EaseCircleIn : public ActionEase {
|
||||
public:
|
||||
static EaseCircleIn* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseCircleOut : public ActionEase {
|
||||
public:
|
||||
static EaseCircleOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
class EaseCircleInOut : public ActionEase {
|
||||
public:
|
||||
static EaseCircleInOut* create(ActionInterval* action);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义缓动
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 自定义缓动动作
|
||||
*/
|
||||
class EaseCustom : public ActionEase {
|
||||
public:
|
||||
static EaseCustom* create(ActionInterval* action, EaseFunction easeFunc);
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
void update(float time) override;
|
||||
|
||||
protected:
|
||||
EaseFunction easeFunc_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/finite_time_action.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 瞬时动作基类
|
||||
*
|
||||
* 瞬间完成的动作,中间没有任何动画效果。
|
||||
* 继承自 FiniteTimeAction,持续时间为 0。
|
||||
*/
|
||||
class ActionInstant : public FiniteTimeAction {
|
||||
public:
|
||||
ActionInstant();
|
||||
virtual ~ActionInstant() = default;
|
||||
|
||||
/**
|
||||
* @brief 检查动作是否完成
|
||||
* @return 总是返回 true
|
||||
*/
|
||||
bool isDone() const override;
|
||||
|
||||
/**
|
||||
* @brief 使用目标节点启动动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void startWithTarget(Node* target) override;
|
||||
|
||||
/**
|
||||
* @brief 每帧调用的步进函数
|
||||
* @param dt 帧时间间隔
|
||||
*/
|
||||
void step(float dt) override;
|
||||
|
||||
/**
|
||||
* @brief 克隆动作
|
||||
* @return 动作的深拷贝
|
||||
*/
|
||||
ActionInstant* clone() const override = 0;
|
||||
|
||||
/**
|
||||
* @brief 创建反向动作
|
||||
* @return 反向动作
|
||||
*/
|
||||
ActionInstant* reverse() const override = 0;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief 执行瞬时动作
|
||||
*/
|
||||
virtual void execute() = 0;
|
||||
|
||||
bool done_ = false;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action_instant.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <functional>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 回调动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 无参数回调动作
|
||||
*/
|
||||
class CallFunc : public ActionInstant {
|
||||
public:
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
static CallFunc* create(const Callback& callback);
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
|
||||
Callback callback_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 带节点参数的回调动作
|
||||
*/
|
||||
class CallFuncN : public ActionInstant {
|
||||
public:
|
||||
using Callback = std::function<void(Node*)>;
|
||||
|
||||
static CallFuncN* create(const Callback& callback);
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
|
||||
Callback callback_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 位置动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 瞬间放置到指定位置
|
||||
*/
|
||||
class Place : public ActionInstant {
|
||||
public:
|
||||
static Place* create(const Vec2& position);
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
|
||||
Vec2 position_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 翻转动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief X轴翻转动作
|
||||
*/
|
||||
class FlipX : public ActionInstant {
|
||||
public:
|
||||
static FlipX* create(bool flipX);
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
|
||||
bool flipX_ = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Y轴翻转动作
|
||||
*/
|
||||
class FlipY : public ActionInstant {
|
||||
public:
|
||||
static FlipY* create(bool flipY);
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
|
||||
bool flipY_ = false;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 可见性动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 显示动作
|
||||
*/
|
||||
class Show : public ActionInstant {
|
||||
public:
|
||||
static Show* create();
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 隐藏动作
|
||||
*/
|
||||
class Hide : public ActionInstant {
|
||||
public:
|
||||
static Hide* create();
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 切换可见性动作
|
||||
*/
|
||||
class ToggleVisibility : public ActionInstant {
|
||||
public:
|
||||
static ToggleVisibility* create();
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 节点管理动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 移除自身动作
|
||||
*/
|
||||
class RemoveSelf : public ActionInstant {
|
||||
public:
|
||||
static RemoveSelf* create();
|
||||
|
||||
ActionInstant* clone() const override;
|
||||
ActionInstant* reverse() const override;
|
||||
|
||||
protected:
|
||||
void execute() override;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/finite_time_action.h>
|
||||
#include <extra2d/action/ease.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 时间间隔动作基类
|
||||
*
|
||||
* 在指定时间内完成的动作,中间会有动画效果。
|
||||
* 继承自 FiniteTimeAction。
|
||||
*/
|
||||
class ActionInterval : public FiniteTimeAction {
|
||||
public:
|
||||
ActionInterval() = default;
|
||||
explicit ActionInterval(float duration);
|
||||
virtual ~ActionInterval() = default;
|
||||
|
||||
/**
|
||||
* @brief 获取已流逝时间
|
||||
* @return 已流逝时间(秒)
|
||||
*/
|
||||
float getElapsed() const { return elapsed_; }
|
||||
|
||||
/**
|
||||
* @brief 检查动作是否完成
|
||||
* @return true 如果动作已完成
|
||||
*/
|
||||
bool isDone() const override;
|
||||
|
||||
/**
|
||||
* @brief 使用目标节点启动动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void startWithTarget(Node* target) override;
|
||||
|
||||
/**
|
||||
* @brief 停止动作
|
||||
*/
|
||||
void stop() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧调用的步进函数
|
||||
* @param dt 帧时间间隔
|
||||
*/
|
||||
void step(float dt) override;
|
||||
|
||||
/**
|
||||
* @brief 设置振幅比率(用于缓动)
|
||||
* @param amp 振幅比率
|
||||
*/
|
||||
void setAmplitudeRate(float amp) { amplitudeRate_ = amp; }
|
||||
|
||||
/**
|
||||
* @brief 获取振幅比率
|
||||
* @return 振幅比率
|
||||
*/
|
||||
float getAmplitudeRate() const { return amplitudeRate_; }
|
||||
|
||||
/**
|
||||
* @brief 设置内置缓动函数
|
||||
* @param easeFunc 缓动函数
|
||||
*/
|
||||
void setEaseFunction(EaseFunction easeFunc) { easeFunc_ = easeFunc; }
|
||||
|
||||
/**
|
||||
* @brief 获取内置缓动函数
|
||||
* @return 缓动函数
|
||||
*/
|
||||
EaseFunction getEaseFunction() const { return easeFunc_; }
|
||||
|
||||
/**
|
||||
* @brief 克隆动作
|
||||
* @return 动作的深拷贝
|
||||
*/
|
||||
ActionInterval* clone() const override = 0;
|
||||
|
||||
/**
|
||||
* @brief 创建反向动作
|
||||
* @return 反向动作
|
||||
*/
|
||||
ActionInterval* reverse() const override = 0;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief 动作开始时调用
|
||||
*/
|
||||
virtual void onStart() {}
|
||||
|
||||
/**
|
||||
* @brief 动作更新时调用
|
||||
* @param progress 归一化进度 [0, 1]
|
||||
*/
|
||||
virtual void onUpdate(float progress) = 0;
|
||||
|
||||
float elapsed_ = 0.0f;
|
||||
bool firstTick_ = true;
|
||||
float amplitudeRate_ = 1.0f;
|
||||
EaseFunction easeFunc_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action_interval.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/color.h>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 移动动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 相对移动动作
|
||||
*/
|
||||
class MoveBy : public ActionInterval {
|
||||
public:
|
||||
static MoveBy* create(float duration, const Vec2& delta);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Vec2 delta_;
|
||||
Vec2 startPosition_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝对移动动作
|
||||
*/
|
||||
class MoveTo : public ActionInterval {
|
||||
public:
|
||||
static MoveTo* create(float duration, const Vec2& position);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Vec2 endPosition_;
|
||||
Vec2 startPosition_;
|
||||
Vec2 delta_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 跳跃动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 相对跳跃动作
|
||||
*/
|
||||
class JumpBy : public ActionInterval {
|
||||
public:
|
||||
static JumpBy* create(float duration, const Vec2& position, float height, int jumps);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Vec2 startPosition_;
|
||||
Vec2 delta_;
|
||||
float height_ = 0.0f;
|
||||
int jumps_ = 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝对跳跃动作
|
||||
*/
|
||||
class JumpTo : public JumpBy {
|
||||
public:
|
||||
static JumpTo* create(float duration, const Vec2& position, float height, int jumps);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
|
||||
Vec2 endPosition_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 贝塞尔曲线动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 贝塞尔曲线配置
|
||||
*/
|
||||
struct BezierConfig {
|
||||
Vec2 controlPoint1;
|
||||
Vec2 controlPoint2;
|
||||
Vec2 endPosition;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 相对贝塞尔曲线动作
|
||||
*/
|
||||
class BezierBy : public ActionInterval {
|
||||
public:
|
||||
static BezierBy* create(float duration, const BezierConfig& config);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
static float bezierat(float a, float b, float c, float d, float t);
|
||||
|
||||
BezierConfig config_;
|
||||
Vec2 startPosition_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝对贝塞尔曲线动作
|
||||
*/
|
||||
class BezierTo : public BezierBy {
|
||||
public:
|
||||
static BezierTo* create(float duration, const BezierConfig& config);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
|
||||
BezierConfig originalConfig_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 缩放动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 相对缩放动作
|
||||
*/
|
||||
class ScaleBy : public ActionInterval {
|
||||
public:
|
||||
static ScaleBy* create(float duration, float scale);
|
||||
static ScaleBy* create(float duration, float scaleX, float scaleY);
|
||||
static ScaleBy* create(float duration, const Vec2& scale);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Vec2 deltaScale_;
|
||||
Vec2 startScale_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝对缩放动作
|
||||
*/
|
||||
class ScaleTo : public ActionInterval {
|
||||
public:
|
||||
static ScaleTo* create(float duration, float scale);
|
||||
static ScaleTo* create(float duration, float scaleX, float scaleY);
|
||||
static ScaleTo* create(float duration, const Vec2& scale);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Vec2 endScale_;
|
||||
Vec2 startScale_;
|
||||
Vec2 delta_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 旋转动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 相对旋转动作
|
||||
*/
|
||||
class RotateBy : public ActionInterval {
|
||||
public:
|
||||
static RotateBy* create(float duration, float deltaAngle);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
float deltaAngle_ = 0.0f;
|
||||
float startAngle_ = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 绝对旋转动作
|
||||
*/
|
||||
class RotateTo : public ActionInterval {
|
||||
public:
|
||||
static RotateTo* create(float duration, float angle);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
float endAngle_ = 0.0f;
|
||||
float startAngle_ = 0.0f;
|
||||
float deltaAngle_ = 0.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 淡入淡出动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 淡入动作
|
||||
*/
|
||||
class FadeIn : public ActionInterval {
|
||||
public:
|
||||
static FadeIn* create(float duration);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
float startOpacity_ = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 淡出动作
|
||||
*/
|
||||
class FadeOut : public ActionInterval {
|
||||
public:
|
||||
static FadeOut* create(float duration);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
float startOpacity_ = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 淡入到指定透明度动作
|
||||
*/
|
||||
class FadeTo : public ActionInterval {
|
||||
public:
|
||||
static FadeTo* create(float duration, float opacity);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
float endOpacity_ = 0.0f;
|
||||
float startOpacity_ = 0.0f;
|
||||
float deltaOpacity_ = 0.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 闪烁动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 闪烁动作
|
||||
*/
|
||||
class Blink : public ActionInterval {
|
||||
public:
|
||||
static Blink* create(float duration, int times);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
int times_ = 1;
|
||||
int currentTimes_ = 0;
|
||||
bool originalVisible_ = true;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 色调动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 色调变化动作
|
||||
*/
|
||||
class TintTo : public ActionInterval {
|
||||
public:
|
||||
static TintTo* create(float duration, uint8_t red, uint8_t green, uint8_t blue);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Color3B startColor_;
|
||||
Color3B endColor_;
|
||||
Color3B deltaColor_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 相对色调变化动作
|
||||
*/
|
||||
class TintBy : public ActionInterval {
|
||||
public:
|
||||
static TintBy* create(float duration, int16_t deltaRed, int16_t deltaGreen, int16_t deltaBlue);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
Color3B startColor_;
|
||||
int16_t deltaR_ = 0;
|
||||
int16_t deltaG_ = 0;
|
||||
int16_t deltaB_ = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 组合动作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 序列动作
|
||||
*/
|
||||
class Sequence : public ActionInterval {
|
||||
public:
|
||||
static Sequence* create(ActionInterval* action1, ...);
|
||||
static Sequence* create(const std::vector<ActionInterval*>& actions);
|
||||
|
||||
~Sequence();
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
std::vector<ActionInterval*> actions_;
|
||||
size_t currentIndex_ = 0;
|
||||
float split_ = 0.0f;
|
||||
float last_ = -1.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 并行动作
|
||||
*/
|
||||
class Spawn : public ActionInterval {
|
||||
public:
|
||||
static Spawn* create(ActionInterval* action1, ...);
|
||||
static Spawn* create(const std::vector<ActionInterval*>& actions);
|
||||
|
||||
~Spawn();
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
std::vector<ActionInterval*> actions_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 重复动作
|
||||
*/
|
||||
class Repeat : public ActionInterval {
|
||||
public:
|
||||
static Repeat* create(ActionInterval* action, int times);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
bool isDone() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
ActionInterval* innerAction_ = nullptr;
|
||||
int times_ = 1;
|
||||
int currentTimes_ = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 永久重复动作
|
||||
*/
|
||||
class RepeatForever : public ActionInterval {
|
||||
public:
|
||||
static RepeatForever* create(ActionInterval* action);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
bool isDone() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
ActionInterval* innerAction_ = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 延时动作
|
||||
*/
|
||||
class DelayTime : public ActionInterval {
|
||||
public:
|
||||
static DelayTime* create(float duration);
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onUpdate(float progress) override {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 反向时间动作
|
||||
*/
|
||||
class ReverseTime : public ActionInterval {
|
||||
public:
|
||||
static ReverseTime* create(ActionInterval* action);
|
||||
|
||||
~ReverseTime();
|
||||
|
||||
ActionInterval* clone() const override;
|
||||
ActionInterval* reverse() const override;
|
||||
|
||||
protected:
|
||||
void onStart() override;
|
||||
void onUpdate(float progress) override;
|
||||
|
||||
ActionInterval* innerAction_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 动作管理器
|
||||
*
|
||||
* 单例模式,集中管理所有动作的调度和生命周期。
|
||||
* 负责动作的添加、移除、暂停、恢复和更新。
|
||||
*/
|
||||
class ActionManager {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* @return ActionManager 实例指针
|
||||
*/
|
||||
static ActionManager* getInstance();
|
||||
|
||||
/**
|
||||
* @brief 销毁单例实例
|
||||
*/
|
||||
static void destroyInstance();
|
||||
|
||||
/**
|
||||
* @brief 添加动作到管理器
|
||||
* @param action 动作指针
|
||||
* @param target 目标节点
|
||||
* @param paused 是否暂停
|
||||
*/
|
||||
void addAction(Action* action, Node* target, bool paused = false);
|
||||
|
||||
/**
|
||||
* @brief 移除指定动作
|
||||
* @param action 动作指针
|
||||
*/
|
||||
void removeAction(Action* action);
|
||||
|
||||
/**
|
||||
* @brief 根据标签移除动作
|
||||
* @param tag 标签值
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void removeActionByTag(int tag, Node* target);
|
||||
|
||||
/**
|
||||
* @brief 根据标志位移除动作
|
||||
* @param flags 标志位
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void removeActionsByFlags(unsigned int flags, Node* target);
|
||||
|
||||
/**
|
||||
* @brief 移除目标节点的所有动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void removeAllActionsFromTarget(Node* target);
|
||||
|
||||
/**
|
||||
* @brief 移除所有动作
|
||||
*/
|
||||
void removeAllActions();
|
||||
|
||||
/**
|
||||
* @brief 根据标签获取动作
|
||||
* @param tag 标签值
|
||||
* @param target 目标节点
|
||||
* @return 动作指针,未找到返回 nullptr
|
||||
*/
|
||||
Action* getActionByTag(int tag, Node* target);
|
||||
|
||||
/**
|
||||
* @brief 获取目标节点的动作数量
|
||||
* @param target 目标节点
|
||||
* @return 动作数量
|
||||
*/
|
||||
size_t getActionCount(Node* target) const;
|
||||
|
||||
/**
|
||||
* @brief 暂停目标节点的所有动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void pauseTarget(Node* target);
|
||||
|
||||
/**
|
||||
* @brief 恢复目标节点的所有动作
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void resumeTarget(Node* target);
|
||||
|
||||
/**
|
||||
* @brief 检查目标节点是否暂停
|
||||
* @param target 目标节点
|
||||
* @return true 如果暂停
|
||||
*/
|
||||
bool isPaused(Node* target) const;
|
||||
|
||||
/**
|
||||
* @brief 更新所有动作(每帧调用)
|
||||
* @param dt 帧时间间隔
|
||||
*/
|
||||
void update(float dt);
|
||||
|
||||
private:
|
||||
ActionManager();
|
||||
~ActionManager();
|
||||
|
||||
struct ActionElement {
|
||||
std::vector<Action*> actions;
|
||||
Node* target = nullptr;
|
||||
bool paused = false;
|
||||
int actionIndex = 0;
|
||||
Action* currentAction = nullptr;
|
||||
bool currentActionSalvaged = false;
|
||||
};
|
||||
|
||||
using ActionMap = std::unordered_map<Node*, ActionElement>;
|
||||
|
||||
void removeActionAt(size_t index, ActionElement& element);
|
||||
void deleteAction(Action* action);
|
||||
|
||||
ActionMap targets_;
|
||||
static ActionManager* instance_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action.h>
|
||||
#include <extra2d/action/action_interval.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 速度控制动作
|
||||
*
|
||||
* 包装其他动作,实现动态速度控制。
|
||||
* 可以在运行时调整动作的播放速度。
|
||||
*/
|
||||
class Speed : public Action {
|
||||
public:
|
||||
/**
|
||||
* @brief 创建速度控制动作
|
||||
* @param action 内部动作
|
||||
* @param speed 速度倍率(1.0 为正常速度)
|
||||
* @return 动作指针
|
||||
*/
|
||||
static Speed* create(ActionInterval* action, float speed);
|
||||
|
||||
~Speed();
|
||||
|
||||
/**
|
||||
* @brief 获取速度倍率
|
||||
* @return 速度倍率
|
||||
*/
|
||||
float getSpeed() const { return speed_; }
|
||||
|
||||
/**
|
||||
* @brief 设置速度倍率
|
||||
* @param speed 速度倍率
|
||||
*/
|
||||
void setSpeed(float speed) { speed_ = speed; }
|
||||
|
||||
/**
|
||||
* @brief 获取内部动作
|
||||
* @return 内部动作指针
|
||||
*/
|
||||
ActionInterval* getInnerAction() const { return innerAction_; }
|
||||
|
||||
void startWithTarget(Node* target) override;
|
||||
void stop() override;
|
||||
void step(float dt) override;
|
||||
bool isDone() const override;
|
||||
Action* clone() const override;
|
||||
Action* reverse() const override;
|
||||
|
||||
protected:
|
||||
Speed() = default;
|
||||
|
||||
ActionInterval* innerAction_ = nullptr;
|
||||
float speed_ = 1.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 跟随动作
|
||||
*
|
||||
* 使节点跟随另一个节点移动。
|
||||
* 常用于相机跟随玩家。
|
||||
*/
|
||||
class Follow : public Action {
|
||||
public:
|
||||
/**
|
||||
* @brief 创建跟随动作
|
||||
* @param followedNode 被跟随的节点
|
||||
* @return 动作指针
|
||||
*/
|
||||
static Follow* create(Node* followedNode);
|
||||
|
||||
/**
|
||||
* @brief 创建带边界的跟随动作
|
||||
* @param followedNode 被跟随的节点
|
||||
* @param boundary 边界矩形
|
||||
* @return 动作指针
|
||||
*/
|
||||
static Follow* create(Node* followedNode, const Rect& boundary);
|
||||
|
||||
~Follow();
|
||||
|
||||
/**
|
||||
* @brief 获取被跟随的节点
|
||||
* @return 被跟随的节点指针
|
||||
*/
|
||||
Node* getFollowedNode() const { return followedNode_; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否设置了边界
|
||||
* @return true 如果设置了边界
|
||||
*/
|
||||
bool isBoundarySet() const { return boundarySet_; }
|
||||
|
||||
void startWithTarget(Node* target) override;
|
||||
void stop() override;
|
||||
void step(float dt) override;
|
||||
bool isDone() const override;
|
||||
Action* clone() const override;
|
||||
Action* reverse() const override;
|
||||
|
||||
protected:
|
||||
Follow() = default;
|
||||
|
||||
Node* followedNode_ = nullptr;
|
||||
Rect boundary_;
|
||||
bool boundarySet_ = false;
|
||||
Vec2 halfScreenSize_;
|
||||
Vec2 fullScreenSize_;
|
||||
Vec2 leftBoundary_;
|
||||
Vec2 rightBoundary_;
|
||||
Vec2 topBoundary_;
|
||||
Vec2 bottomBoundary_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 目标动作
|
||||
*
|
||||
* 允许在一个节点上运行动作,但目标为另一个节点。
|
||||
*/
|
||||
class TargetedAction : public Action {
|
||||
public:
|
||||
/**
|
||||
* @brief 创建目标动作
|
||||
* @param target 目标节点
|
||||
* @param action 要运行的动作
|
||||
* @return 动作指针
|
||||
*/
|
||||
static TargetedAction* create(Node* target, FiniteTimeAction* action);
|
||||
|
||||
~TargetedAction();
|
||||
|
||||
/**
|
||||
* @brief 获取目标节点
|
||||
* @return 目标节点指针
|
||||
*/
|
||||
Node* getTargetNode() const { return targetNode_; }
|
||||
|
||||
/**
|
||||
* @brief 设置目标节点
|
||||
* @param target 目标节点
|
||||
*/
|
||||
void setTargetNode(Node* target) { targetNode_ = target; }
|
||||
|
||||
/**
|
||||
* @brief 获取内部动作
|
||||
* @return 内部动作指针
|
||||
*/
|
||||
FiniteTimeAction* getAction() const { return innerAction_; }
|
||||
|
||||
void startWithTarget(Node* target) override;
|
||||
void stop() override;
|
||||
void step(float dt) override;
|
||||
bool isDone() const override;
|
||||
Action* clone() const override;
|
||||
Action* reverse() const override;
|
||||
|
||||
protected:
|
||||
TargetedAction() = default;
|
||||
|
||||
Node* targetNode_ = nullptr;
|
||||
FiniteTimeAction* innerAction_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 缓动函数类型
|
||||
*/
|
||||
using EaseFunction = float (*)(float);
|
||||
|
||||
// ============================================================================
|
||||
// 线性缓动
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 线性缓动(无缓动)
|
||||
* @param t 归一化时间 [0, 1]
|
||||
* @return 缓动后的值
|
||||
*/
|
||||
float easeLinear(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 二次缓动 (Quad)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuad(float t);
|
||||
float easeOutQuad(float t);
|
||||
float easeInOutQuad(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 三次缓动 (Cubic)
|
||||
// ============================================================================
|
||||
|
||||
float easeInCubic(float t);
|
||||
float easeOutCubic(float t);
|
||||
float easeInOutCubic(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 四次缓动 (Quart)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuart(float t);
|
||||
float easeOutQuart(float t);
|
||||
float easeInOutQuart(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 五次缓动 (Quint)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuint(float t);
|
||||
float easeOutQuint(float t);
|
||||
float easeInOutQuint(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 正弦缓动 (Sine)
|
||||
// ============================================================================
|
||||
|
||||
float easeInSine(float t);
|
||||
float easeOutSine(float t);
|
||||
float easeInOutSine(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 指数缓动 (Exponential)
|
||||
// ============================================================================
|
||||
|
||||
float easeInExpo(float t);
|
||||
float easeOutExpo(float t);
|
||||
float easeInOutExpo(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 圆形缓动 (Circular)
|
||||
// ============================================================================
|
||||
|
||||
float easeInCirc(float t);
|
||||
float easeOutCirc(float t);
|
||||
float easeInOutCirc(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 回震缓动 (Back)
|
||||
// ============================================================================
|
||||
|
||||
float easeInBack(float t);
|
||||
float easeOutBack(float t);
|
||||
float easeInOutBack(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 弹性缓动 (Elastic)
|
||||
// ============================================================================
|
||||
|
||||
float easeInElastic(float t);
|
||||
float easeOutElastic(float t);
|
||||
float easeInOutElastic(float t);
|
||||
|
||||
// ============================================================================
|
||||
// 弹跳缓动 (Bounce)
|
||||
// ============================================================================
|
||||
|
||||
float easeInBounce(float t);
|
||||
float easeOutBounce(float t);
|
||||
float easeInOutBounce(float t);
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/action/action.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 有限时间动作基类
|
||||
*
|
||||
* 所有具有持续时间的动作的基类。
|
||||
* 继承自 Action,添加了持续时间属性。
|
||||
*/
|
||||
class FiniteTimeAction : public Action {
|
||||
public:
|
||||
FiniteTimeAction() = default;
|
||||
explicit FiniteTimeAction(float duration);
|
||||
virtual ~FiniteTimeAction() = default;
|
||||
|
||||
/**
|
||||
* @brief 获取动作持续时间
|
||||
* @return 持续时间(秒)
|
||||
*/
|
||||
float getDuration() const { return duration_; }
|
||||
|
||||
/**
|
||||
* @brief 设置动作持续时间
|
||||
* @param duration 持续时间(秒)
|
||||
*/
|
||||
void setDuration(float duration) { duration_ = duration; }
|
||||
|
||||
/**
|
||||
* @brief 克隆动作
|
||||
* @return 动作的深拷贝
|
||||
*/
|
||||
FiniteTimeAction* clone() const override = 0;
|
||||
|
||||
/**
|
||||
* @brief 创建反向动作
|
||||
* @return 反向动作
|
||||
*/
|
||||
FiniteTimeAction* reverse() const override = 0;
|
||||
|
||||
protected:
|
||||
float duration_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// ALS 图层信息
|
||||
// ============================================================================
|
||||
struct AlsLayerInfo {
|
||||
std::string aniPath; // 子动画的 ANI 文件路径
|
||||
int zOrder = 0; // 层级顺序
|
||||
Vec2 offset; // 层偏移
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ALS 解析结果
|
||||
// ============================================================================
|
||||
struct AlsParseResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
std::vector<AlsLayerInfo> layers;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AlsParser - ALS 复合动画文件解析器
|
||||
// 解析 .als 文件获取多层动画的图层信息
|
||||
// ============================================================================
|
||||
class AlsParser {
|
||||
public:
|
||||
AlsParser() = default;
|
||||
|
||||
/// 从文件解析
|
||||
AlsParseResult parse(const std::string &filePath);
|
||||
|
||||
/// 从内存内容解析
|
||||
AlsParseResult parseFromMemory(const std::string &content,
|
||||
const std::string &basePath = "");
|
||||
|
||||
/// 设置基础路径
|
||||
void setBasePath(const std::string &basePath) { basePath_ = basePath; }
|
||||
|
||||
private:
|
||||
std::string basePath_;
|
||||
|
||||
std::string resolvePath(const std::string &relativePath) const;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <extra2d/animation/ani_parser.h>
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// DNF ANI 二进制格式中的节点类型枚举
|
||||
// ============================================================================
|
||||
enum class AniNodeType : uint16_t {
|
||||
Loop = 0,
|
||||
Shadow = 1,
|
||||
Coord = 3,
|
||||
ImageRate = 7,
|
||||
ImageRotate = 8,
|
||||
RGBA = 9,
|
||||
Interpolation = 10,
|
||||
GraphicEffect = 11,
|
||||
Delay = 12,
|
||||
DamageType = 13,
|
||||
DamageBox = 14,
|
||||
AttackBox = 15,
|
||||
PlaySound = 16,
|
||||
Preload = 17,
|
||||
Spectrum = 18,
|
||||
SetFlag = 23,
|
||||
FlipType = 24,
|
||||
LoopStart = 25,
|
||||
LoopEnd = 26,
|
||||
Clip = 27,
|
||||
Operation = 28,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AniBinaryParser - ANI 二进制格式解析器
|
||||
// 参考 DNF-Porting 的 PvfAnimation 实现
|
||||
// ============================================================================
|
||||
class AniBinaryParser {
|
||||
public:
|
||||
AniBinaryParser() = default;
|
||||
|
||||
/// 从二进制数据解析
|
||||
AniParseResult parse(const uint8_t *data, size_t length);
|
||||
|
||||
/// 从文件解析
|
||||
AniParseResult parseFromFile(const std::string &filePath);
|
||||
|
||||
/// 设置路径替换回调
|
||||
void setPathResolver(PathResolveCallback callback) {
|
||||
pathResolver_ = std::move(callback);
|
||||
}
|
||||
|
||||
/// 设置基础路径
|
||||
void setBasePath(const std::string &basePath) { basePath_ = basePath; }
|
||||
|
||||
private:
|
||||
PathResolveCallback pathResolver_;
|
||||
std::string basePath_;
|
||||
|
||||
std::string resolvePath(const std::string &relativePath) const;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/animation/animation_clip.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// ANI 文件解析结果
|
||||
// ============================================================================
|
||||
struct AniParseResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
Ptr<AnimationClip> clip;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AniParser - ANI 脚本文件解析器
|
||||
// 将原始 ANI 文件格式解析为 AnimationClip 数据
|
||||
// ============================================================================
|
||||
class AniParser {
|
||||
public:
|
||||
AniParser() = default;
|
||||
|
||||
/// 从文件解析
|
||||
AniParseResult parse(const std::string &filePath);
|
||||
|
||||
/// 从内存内容解析
|
||||
AniParseResult parseFromMemory(const std::string &content,
|
||||
const std::string &basePath = "");
|
||||
|
||||
/// 设置路径替换回调(对应原始 AdditionalOptions)
|
||||
void setPathResolver(PathResolveCallback callback) {
|
||||
pathResolver_ = std::move(callback);
|
||||
}
|
||||
|
||||
/// 设置基础路径(用于解析相对路径)
|
||||
void setBasePath(const std::string &basePath) { basePath_ = basePath; }
|
||||
|
||||
private:
|
||||
PathResolveCallback pathResolver_;
|
||||
std::string basePath_;
|
||||
|
||||
std::string resolvePath(const std::string &relativePath) const;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/animation/animation_controller.h>
|
||||
#include <extra2d/scene/sprite.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// AnimatedSprite - 动画精灵节点
|
||||
// 将 AnimationController 与 Sprite 渲染桥接,接入场景图
|
||||
// ============================================================================
|
||||
class AnimatedSprite : public Sprite {
|
||||
public:
|
||||
AnimatedSprite();
|
||||
~AnimatedSprite() override = default;
|
||||
|
||||
// ------ 静态工厂 ------
|
||||
static Ptr<AnimatedSprite> create();
|
||||
static Ptr<AnimatedSprite> create(Ptr<AnimationClip> clip);
|
||||
static Ptr<AnimatedSprite> create(const std::string &aniFilePath);
|
||||
|
||||
// ------ 动画绑定 ------
|
||||
void setAnimationClip(Ptr<AnimationClip> clip);
|
||||
void loadAnimation(const std::string &aniFilePath);
|
||||
Ptr<AnimationClip> getAnimationClip() const;
|
||||
|
||||
// ------ 动画字典 ------
|
||||
void addAnimation(const std::string &name, Ptr<AnimationClip> clip);
|
||||
void play(const std::string &name, bool loop = true);
|
||||
bool hasAnimation(const std::string &name) const;
|
||||
Ptr<AnimationClip> getAnimation(const std::string &name) const;
|
||||
const std::string &getCurrentAnimationName() const;
|
||||
|
||||
// ------ 播放控制(委托 controller_)------
|
||||
void play();
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void reset();
|
||||
bool isPlaying() const;
|
||||
bool isPaused() const;
|
||||
bool isStopped() const;
|
||||
|
||||
// ------ 属性控制 ------
|
||||
void setLooping(bool loop);
|
||||
bool isLooping() const;
|
||||
void setPlaybackSpeed(float speed);
|
||||
float getPlaybackSpeed() const;
|
||||
|
||||
// ------ 帧控制 ------
|
||||
void setFrameIndex(size_t index);
|
||||
size_t getCurrentFrameIndex() const;
|
||||
size_t getTotalFrames() const;
|
||||
void nextFrame();
|
||||
void prevFrame();
|
||||
|
||||
// ------ 帧范围限制 ------
|
||||
/// 设置帧播放范围(用于精灵图动画,限制在指定范围内循环)
|
||||
/// @param start 起始帧索引(包含)
|
||||
/// @param end 结束帧索引(包含),-1表示不限制
|
||||
void setFrameRange(int start, int end = -1);
|
||||
|
||||
/// 获取当前帧范围
|
||||
/// @return pair<起始帧, 结束帧>,结束帧为-1表示不限制
|
||||
std::pair<int, int> getFrameRange() const;
|
||||
|
||||
/// 清除帧范围限制(恢复播放所有帧)
|
||||
void clearFrameRange();
|
||||
|
||||
/// 检查是否设置了帧范围限制
|
||||
bool hasFrameRange() const;
|
||||
|
||||
// ------ 回调 ------
|
||||
void setCompletionCallback(AnimationController::CompletionCallback cb);
|
||||
void setKeyframeCallback(AnimationController::KeyframeCallback cb);
|
||||
void setSoundTriggerCallback(AnimationController::SoundTriggerCallback cb);
|
||||
|
||||
// ------ 碰撞盒访问(当前帧)------
|
||||
const std::vector<std::array<int32_t, 6>> &getCurrentDamageBoxes() const;
|
||||
const std::vector<std::array<int32_t, 6>> &getCurrentAttackBoxes() const;
|
||||
|
||||
// ------ 帧变换控制 ------
|
||||
/// 设置是否由动画帧数据覆盖节点的 position/scale/rotation
|
||||
/// ANI 动画需要开启(默认),精灵图动画应关闭
|
||||
void setApplyFrameTransform(bool apply) { applyFrameTransform_ = apply; }
|
||||
bool isApplyFrameTransform() const { return applyFrameTransform_; }
|
||||
|
||||
// ------ 自动播放 ------
|
||||
void setAutoPlay(bool autoPlay) { autoPlay_ = autoPlay; }
|
||||
bool isAutoPlay() const { return autoPlay_; }
|
||||
|
||||
// ------ 直接控制器访问 ------
|
||||
AnimationController &getController() { return controller_; }
|
||||
const AnimationController &getController() const { return controller_; }
|
||||
|
||||
protected:
|
||||
void onUpdate(float dt) override;
|
||||
void onEnter() override;
|
||||
|
||||
private:
|
||||
AnimationController controller_;
|
||||
bool autoPlay_ = false;
|
||||
bool applyFrameTransform_ = true;
|
||||
|
||||
// 动画字典
|
||||
std::unordered_map<std::string, Ptr<AnimationClip>> animations_;
|
||||
std::string currentAnimationName_;
|
||||
static const std::string emptyString_;
|
||||
|
||||
// 帧范围限制(用于精灵图动画)
|
||||
int frameRangeStart_ = 0; // 起始帧索引
|
||||
int frameRangeEnd_ = -1; // 结束帧索引,-1表示不限制
|
||||
|
||||
// 空碰撞盒列表(用于无帧时返回引用)
|
||||
static const std::vector<std::array<int32_t, 6>> emptyBoxes_;
|
||||
|
||||
void applyFrame(const AnimationFrame &frame);
|
||||
void onFrameChanged(size_t oldIdx, size_t newIdx,
|
||||
const AnimationFrame &frame);
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/animation_clip.h>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// 路径替换回调(对应原始 AdditionalOptions)
|
||||
using PathResolveCallback = std::function<std::string(const std::string &)>;
|
||||
|
||||
// ============================================================================
|
||||
// AnimationCache - 动画片段全局缓存(借鉴 Cocos AnimationCache)
|
||||
// 同一 ANI 文件只解析一次,后续直接复用数据
|
||||
// ============================================================================
|
||||
class AnimationCache {
|
||||
public:
|
||||
static AnimationCache &getInstance() {
|
||||
static AnimationCache instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ------ 加载与获取 ------
|
||||
|
||||
/// 从文件加载(自动缓存),已缓存则直接返回
|
||||
/// 注意:实际的 ANI 解析逻辑在 AniParser 中实现
|
||||
/// 此方法在 animation_cache.cpp 中实现,依赖 AniParser
|
||||
Ptr<AnimationClip> loadClip(const std::string &aniFilePath);
|
||||
|
||||
/// 从缓存获取(不触发加载)
|
||||
Ptr<AnimationClip> getClip(const std::string &name) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = clips_.find(name);
|
||||
if (it != clips_.end())
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// 手动添加到缓存
|
||||
void addClip(Ptr<AnimationClip> clip, const std::string &name) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
clips_[name] = std::move(clip);
|
||||
}
|
||||
|
||||
// ------ 缓存管理 ------
|
||||
|
||||
bool has(const std::string &name) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return clips_.find(name) != clips_.end();
|
||||
}
|
||||
|
||||
void removeClip(const std::string &name) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
clips_.erase(name);
|
||||
}
|
||||
|
||||
/// 移除未被外部引用的动画片段
|
||||
void removeUnusedClips() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto it = clips_.begin(); it != clips_.end();) {
|
||||
if (it->second.use_count() == 1) {
|
||||
it = clips_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
clips_.clear();
|
||||
}
|
||||
|
||||
size_t count() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return clips_.size();
|
||||
}
|
||||
|
||||
// ------ 路径配置 ------
|
||||
void setPathResolver(PathResolveCallback resolver) {
|
||||
pathResolver_ = std::move(resolver);
|
||||
}
|
||||
|
||||
PathResolveCallback getPathResolver() const { return pathResolver_; }
|
||||
|
||||
private:
|
||||
AnimationCache() = default;
|
||||
~AnimationCache() = default;
|
||||
AnimationCache(const AnimationCache &) = delete;
|
||||
AnimationCache &operator=(const AnimationCache &) = delete;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, Ptr<AnimationClip>> clips_;
|
||||
PathResolveCallback pathResolver_;
|
||||
};
|
||||
|
||||
// 便捷宏
|
||||
#define E2D_ANIMATION_CACHE() ::extra2d::AnimationCache::getInstance()
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <extra2d/animation/animation_frame.h>
|
||||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// AnimationClip - 动画片段(纯数据,可复用)
|
||||
// 借鉴 Cocos:一份 AnimationClip 可被多个 AnimationNode 同时使用
|
||||
// ============================================================================
|
||||
class AnimationClip {
|
||||
public:
|
||||
AnimationClip() = default;
|
||||
|
||||
explicit AnimationClip(const std::string &name) : name_(name) {}
|
||||
|
||||
// ------ 帧管理 ------
|
||||
void addFrame(const AnimationFrame &frame) { frames_.push_back(frame); }
|
||||
|
||||
void addFrame(AnimationFrame &&frame) { frames_.push_back(std::move(frame)); }
|
||||
|
||||
void insertFrame(size_t index, const AnimationFrame &frame) {
|
||||
assert(index <= frames_.size());
|
||||
frames_.insert(frames_.begin() + static_cast<ptrdiff_t>(index), frame);
|
||||
}
|
||||
|
||||
void removeFrame(size_t index) {
|
||||
assert(index < frames_.size());
|
||||
frames_.erase(frames_.begin() + static_cast<ptrdiff_t>(index));
|
||||
}
|
||||
|
||||
void clearFrames() { frames_.clear(); }
|
||||
|
||||
const AnimationFrame &getFrame(size_t index) const {
|
||||
assert(index < frames_.size());
|
||||
return frames_[index];
|
||||
}
|
||||
|
||||
AnimationFrame &getFrame(size_t index) {
|
||||
assert(index < frames_.size());
|
||||
return frames_[index];
|
||||
}
|
||||
|
||||
size_t getFrameCount() const { return frames_.size(); }
|
||||
bool empty() const { return frames_.empty(); }
|
||||
|
||||
// ------ 全局属性(对应原始 AnimationFlag)------
|
||||
FramePropertySet &globalProperties() { return globalProperties_; }
|
||||
const FramePropertySet &globalProperties() const { return globalProperties_; }
|
||||
|
||||
bool isLooping() const {
|
||||
return globalProperties_.getOr<bool>(FramePropertyKey::Loop, false);
|
||||
}
|
||||
|
||||
void setLooping(bool loop) { globalProperties_.withLoop(loop); }
|
||||
|
||||
// ------ 时间信息 ------
|
||||
float getTotalDuration() const {
|
||||
float total = 0.0f;
|
||||
for (const auto &frame : frames_) {
|
||||
total += frame.delay;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ------ 预计算最大帧尺寸 ------
|
||||
Size getMaxFrameSize() const {
|
||||
Size maxSize;
|
||||
for (const auto &frame : frames_) {
|
||||
if (frame.spriteFrame && frame.spriteFrame->isValid()) {
|
||||
const auto &rect = frame.spriteFrame->getRect();
|
||||
if (rect.size.width > maxSize.width)
|
||||
maxSize.width = rect.size.width;
|
||||
if (rect.size.height > maxSize.height)
|
||||
maxSize.height = rect.size.height;
|
||||
}
|
||||
}
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
// ------ 元数据 ------
|
||||
void setName(const std::string &name) { name_ = name; }
|
||||
const std::string &getName() const { return name_; }
|
||||
|
||||
void setSourcePath(const std::string &path) { sourcePath_ = path; }
|
||||
const std::string &getSourcePath() const { return sourcePath_; }
|
||||
|
||||
// ------ 静态工厂 ------
|
||||
static Ptr<AnimationClip> create(const std::string &name = "") {
|
||||
return makePtr<AnimationClip>(name);
|
||||
}
|
||||
|
||||
/// 从精灵图网格创建(所有帧按顺序)
|
||||
static Ptr<AnimationClip> createFromGrid(Ptr<Texture> texture, int frameWidth,
|
||||
int frameHeight,
|
||||
float frameDurationMs = 100.0f,
|
||||
int frameCount = -1, int spacing = 0,
|
||||
int margin = 0) {
|
||||
if (!texture)
|
||||
return nullptr;
|
||||
|
||||
int texW = texture->getWidth();
|
||||
int texH = texture->getHeight();
|
||||
int usableW = texW - 2 * margin;
|
||||
int usableH = texH - 2 * margin;
|
||||
int cols = (usableW + spacing) / (frameWidth + spacing);
|
||||
int rows = (usableH + spacing) / (frameHeight + spacing);
|
||||
int total = (frameCount > 0) ? frameCount : cols * rows;
|
||||
|
||||
auto clip = makePtr<AnimationClip>();
|
||||
for (int i = 0; i < total; ++i) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
if (row >= rows)
|
||||
break;
|
||||
|
||||
// 翻转行顺序:精灵图第0行在顶部,但OpenGL纹理V坐标从底部开始
|
||||
// 所以将行索引翻转,使第0行对应纹理底部(V=1.0),第3行对应纹理顶部(V=0.0)
|
||||
int flippedRow = (rows - 1) - row;
|
||||
|
||||
Rect rect(
|
||||
static_cast<float>(margin + col * (frameWidth + spacing)),
|
||||
static_cast<float>(margin + flippedRow * (frameHeight + spacing)),
|
||||
static_cast<float>(frameWidth), static_cast<float>(frameHeight));
|
||||
|
||||
auto sf = SpriteFrame::create(texture, rect);
|
||||
AnimationFrame frame;
|
||||
frame.spriteFrame = std::move(sf);
|
||||
frame.delay = frameDurationMs;
|
||||
clip->addFrame(std::move(frame));
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
/// 从精灵图网格创建(指定帧索引列表)
|
||||
static Ptr<AnimationClip>
|
||||
createFromGridIndices(Ptr<Texture> texture, int frameWidth, int frameHeight,
|
||||
const std::vector<int> &frameIndices,
|
||||
float frameDurationMs = 100.0f, int spacing = 0,
|
||||
int margin = 0) {
|
||||
if (!texture)
|
||||
return nullptr;
|
||||
|
||||
int texW = texture->getWidth();
|
||||
int texH = texture->getHeight();
|
||||
int usableW = texW - 2 * margin;
|
||||
int usableH = texH - 2 * margin;
|
||||
int cols = (usableW + spacing) / (frameWidth + spacing);
|
||||
int rows = (usableH + spacing) / (frameHeight + spacing);
|
||||
|
||||
auto clip = makePtr<AnimationClip>();
|
||||
for (int idx : frameIndices) {
|
||||
int col = idx % cols;
|
||||
int row = idx / cols;
|
||||
|
||||
// 翻转行顺序:精灵图第0行在顶部,但OpenGL纹理V坐标从底部开始
|
||||
int flippedRow = (rows - 1) - row;
|
||||
|
||||
Rect rect(
|
||||
static_cast<float>(margin + col * (frameWidth + spacing)),
|
||||
static_cast<float>(margin + flippedRow * (frameHeight + spacing)),
|
||||
static_cast<float>(frameWidth), static_cast<float>(frameHeight));
|
||||
|
||||
auto sf = SpriteFrame::create(texture, rect);
|
||||
AnimationFrame frame;
|
||||
frame.spriteFrame = std::move(sf);
|
||||
frame.delay = frameDurationMs;
|
||||
clip->addFrame(std::move(frame));
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string sourcePath_;
|
||||
std::vector<AnimationFrame> frames_;
|
||||
FramePropertySet globalProperties_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/animation_clip.h>
|
||||
#include <extra2d/animation/interpolation_engine.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 动画播放状态
|
||||
// ============================================================================
|
||||
enum class AnimPlayState : uint8 { Stopped, Playing, Paused };
|
||||
|
||||
// ============================================================================
|
||||
// AnimationController - 动画播放控制器
|
||||
// 借鉴 Cocos Creator 的 AnimationState:纯播放逻辑,不持有渲染资源
|
||||
// ============================================================================
|
||||
class AnimationController {
|
||||
public:
|
||||
// 回调类型定义
|
||||
using FrameChangeCallback = std::function<void(size_t oldIdx, size_t newIdx,
|
||||
const AnimationFrame &frame)>;
|
||||
using KeyframeCallback = std::function<void(int flagIndex)>;
|
||||
using SoundTriggerCallback = std::function<void(const std::string &path)>;
|
||||
using CompletionCallback = std::function<void()>;
|
||||
|
||||
AnimationController() = default;
|
||||
|
||||
// ------ 绑定动画数据 ------
|
||||
void setClip(Ptr<AnimationClip> clip);
|
||||
Ptr<AnimationClip> getClip() const { return clip_; }
|
||||
|
||||
// ------ 播放控制 ------
|
||||
void play();
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void reset();
|
||||
|
||||
// ------ 帧控制 ------
|
||||
void setFrameIndex(size_t index);
|
||||
void nextFrame();
|
||||
void prevFrame();
|
||||
|
||||
// ------ 核心更新(每帧调用)------
|
||||
void update(float dt);
|
||||
|
||||
// ------ 状态查询 ------
|
||||
AnimPlayState getState() const { return state_; }
|
||||
bool isPlaying() const { return state_ == AnimPlayState::Playing; }
|
||||
bool isPaused() const { return state_ == AnimPlayState::Paused; }
|
||||
bool isStopped() const { return state_ == AnimPlayState::Stopped; }
|
||||
|
||||
size_t getCurrentFrameIndex() const { return currentFrameIndex_; }
|
||||
size_t getTotalFrames() const;
|
||||
const AnimationFrame &getCurrentFrame() const;
|
||||
|
||||
float getPlaybackSpeed() const { return playbackSpeed_; }
|
||||
void setPlaybackSpeed(float speed) { playbackSpeed_ = speed; }
|
||||
|
||||
bool isLooping() const;
|
||||
void setLooping(bool loop);
|
||||
|
||||
// ------ 插值状态 ------
|
||||
float getInterpolationFactor() const { return interpolationFactor_; }
|
||||
bool isInterpolating() const { return interpolating_; }
|
||||
|
||||
// ------ 回调注册 ------
|
||||
void setFrameChangeCallback(FrameChangeCallback cb) {
|
||||
onFrameChange_ = std::move(cb);
|
||||
}
|
||||
void setKeyframeCallback(KeyframeCallback cb) { onKeyframe_ = std::move(cb); }
|
||||
void setSoundTriggerCallback(SoundTriggerCallback cb) {
|
||||
onSoundTrigger_ = std::move(cb);
|
||||
}
|
||||
void setCompletionCallback(CompletionCallback cb) {
|
||||
onComplete_ = std::move(cb);
|
||||
}
|
||||
|
||||
private:
|
||||
Ptr<AnimationClip> clip_;
|
||||
AnimPlayState state_ = AnimPlayState::Stopped;
|
||||
|
||||
size_t currentFrameIndex_ = 0;
|
||||
float accumulatedTime_ = 0.0f; // 当前帧已累积时间 (ms)
|
||||
float playbackSpeed_ = 1.0f;
|
||||
bool loopOverride_ = false; // 外部循环覆盖值
|
||||
bool hasLoopOverride_ = false; // 是否使用外部循环覆盖
|
||||
|
||||
// 插值状态
|
||||
bool interpolating_ = false;
|
||||
float interpolationFactor_ = 0.0f;
|
||||
|
||||
// 回调
|
||||
FrameChangeCallback onFrameChange_;
|
||||
KeyframeCallback onKeyframe_;
|
||||
SoundTriggerCallback onSoundTrigger_;
|
||||
CompletionCallback onComplete_;
|
||||
|
||||
// 内部方法
|
||||
void advanceFrame(size_t newIndex);
|
||||
void processFrameProperties(const AnimationFrame &frame);
|
||||
void updateInterpolation();
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// 前向声明
|
||||
class Node;
|
||||
|
||||
// ============================================================================
|
||||
// 动画事件类型
|
||||
// ============================================================================
|
||||
enum class AnimationEventType : uint32_t {
|
||||
FrameChanged = 0x2001, // 帧切换
|
||||
KeyframeHit = 0x2002, // 关键帧触发
|
||||
SoundTrigger = 0x2003, // 音效触发
|
||||
AnimationStart = 0x2004, // 动画开始播放
|
||||
AnimationEnd = 0x2005, // 动画播放结束
|
||||
AnimationLoop = 0x2006, // 动画循环一轮
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 动画事件数据
|
||||
// ============================================================================
|
||||
struct AnimationEvent {
|
||||
AnimationEventType type;
|
||||
size_t frameIndex = 0;
|
||||
size_t previousFrameIndex = 0;
|
||||
int keyframeFlag = -1;
|
||||
std::string soundPath;
|
||||
Node *source = nullptr;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 动画事件回调类型
|
||||
// ============================================================================
|
||||
using AnimationEventCallback = std::function<void(const AnimationEvent &)>;
|
||||
using KeyframeHitCallback = std::function<void(int flagIndex)>;
|
||||
using AnimationCompleteCallback = std::function<void()>;
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <extra2d/animation/frame_property.h>
|
||||
#include <extra2d/animation/sprite_frame.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// AnimationFrame - 单帧数据
|
||||
// 引用 SpriteFrame 而非直接持有纹理(借鉴 Cocos 模式)
|
||||
// 通过 FramePropertySet 支持不固定数据(ANI Flag 系统增强版)
|
||||
// ============================================================================
|
||||
struct AnimationFrame {
|
||||
// ------ 核心数据(固定部分)------
|
||||
Ptr<SpriteFrame> spriteFrame; // 精灵帧引用(Cocos 模式)
|
||||
std::string texturePath; // 原始图片路径(用于解析时定位资源)
|
||||
int textureIndex = 0; // 精灵图集索引
|
||||
Vec2 offset; // 位置偏移
|
||||
float delay = 100.0f; // 帧延迟(毫秒)
|
||||
|
||||
// ------ 碰撞盒数据(DNF ANI 格式)------
|
||||
std::vector<std::array<int32_t, 6>> damageBoxes; // 伤害碰撞盒
|
||||
std::vector<std::array<int32_t, 6>> attackBoxes; // 攻击碰撞盒
|
||||
|
||||
// ------ 不固定数据(属性集合)------
|
||||
FramePropertySet properties; // 类型安全的 Flag 系统
|
||||
|
||||
// ------ 便捷方法 ------
|
||||
bool hasTexture() const {
|
||||
return spriteFrame != nullptr && spriteFrame->isValid();
|
||||
}
|
||||
|
||||
bool hasInterpolation() const {
|
||||
return properties.getOr<bool>(FramePropertyKey::Interpolation, false);
|
||||
}
|
||||
|
||||
bool hasKeyframeCallback() const {
|
||||
return properties.has(FramePropertyKey::SetFlag);
|
||||
}
|
||||
|
||||
int getKeyframeIndex() const {
|
||||
return properties.getOr<int>(FramePropertyKey::SetFlag, -1);
|
||||
}
|
||||
|
||||
Vec2 getEffectiveScale() const {
|
||||
return properties.getOr<Vec2>(FramePropertyKey::ImageRate, Vec2::One());
|
||||
}
|
||||
|
||||
float getEffectiveRotation() const {
|
||||
return properties.getOr<float>(FramePropertyKey::ImageRotate, 0.0f);
|
||||
}
|
||||
|
||||
Color getEffectiveColor() const {
|
||||
return properties.getOr<Color>(FramePropertyKey::ColorTint, Colors::White);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/animation/animation_clip.h>
|
||||
#include <extra2d/animation/animation_controller.h>
|
||||
#include <extra2d/animation/animation_event.h>
|
||||
#include <extra2d/animation/frame_renderer.h>
|
||||
#include <extra2d/scene/node.h>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// AnimationNode - 动画节点(继承 Node)
|
||||
// 使用 FrameRenderer 单渲染器策略,不依赖 Sprite 基类
|
||||
// 适用于需要独立渲染控制的动画(如特效、复合动画图层)
|
||||
// ============================================================================
|
||||
class AnimationNode : public Node {
|
||||
public:
|
||||
AnimationNode();
|
||||
~AnimationNode() override = default;
|
||||
|
||||
// ------ 静态工厂(Cocos 风格)------
|
||||
static Ptr<AnimationNode> create();
|
||||
static Ptr<AnimationNode> create(Ptr<AnimationClip> clip);
|
||||
static Ptr<AnimationNode> create(const std::string &aniFilePath);
|
||||
|
||||
// ------ 动画数据 ------
|
||||
void setClip(Ptr<AnimationClip> clip);
|
||||
Ptr<AnimationClip> getClip() const;
|
||||
bool loadFromFile(const std::string &aniFilePath);
|
||||
|
||||
// ------ 播放控制 ------
|
||||
void play();
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void reset();
|
||||
|
||||
bool isPlaying() const;
|
||||
bool isPaused() const;
|
||||
bool isStopped() const;
|
||||
|
||||
void setPlaybackSpeed(float speed);
|
||||
float getPlaybackSpeed() const;
|
||||
void setLooping(bool loop);
|
||||
bool isLooping() const;
|
||||
|
||||
// ------ 帧控制 ------
|
||||
void setFrameIndex(size_t index);
|
||||
size_t getCurrentFrameIndex() const;
|
||||
size_t getTotalFrames() const;
|
||||
|
||||
// ------ 事件回调 ------
|
||||
void setKeyframeCallback(KeyframeHitCallback callback);
|
||||
void setCompletionCallback(AnimationCompleteCallback callback);
|
||||
void
|
||||
setFrameChangeCallback(AnimationController::FrameChangeCallback callback);
|
||||
void addEventListener(AnimationEventCallback callback);
|
||||
|
||||
// ------ 视觉属性 ------
|
||||
void setTintColor(const Color &color);
|
||||
Color getTintColor() const { return tintColor_; }
|
||||
void setFlipX(bool flip) { flipX_ = flip; }
|
||||
void setFlipY(bool flip) { flipY_ = flip; }
|
||||
bool isFlipX() const { return flipX_; }
|
||||
bool isFlipY() const { return flipY_; }
|
||||
|
||||
// ------ 自动播放 ------
|
||||
void setAutoPlay(bool autoPlay) { autoPlay_ = autoPlay; }
|
||||
bool isAutoPlay() const { return autoPlay_; }
|
||||
|
||||
// ------ 碰撞盒访问 ------
|
||||
const std::vector<std::array<int32_t, 6>> &getCurrentDamageBoxes() const;
|
||||
const std::vector<std::array<int32_t, 6>> &getCurrentAttackBoxes() const;
|
||||
|
||||
// ------ 查询 ------
|
||||
Size getMaxFrameSize() const;
|
||||
Rect getBoundingBox() const override;
|
||||
|
||||
// ------ 直接访问 ------
|
||||
AnimationController &getController() { return controller_; }
|
||||
const AnimationController &getController() const { return controller_; }
|
||||
FrameRenderer &getFrameRenderer() { return frameRenderer_; }
|
||||
const FrameRenderer &getFrameRenderer() const { return frameRenderer_; }
|
||||
|
||||
protected:
|
||||
void onUpdate(float dt) override;
|
||||
void onDraw(RenderBackend &renderer) override;
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
private:
|
||||
AnimationController controller_;
|
||||
FrameRenderer frameRenderer_;
|
||||
Color tintColor_ = Colors::White;
|
||||
bool flipX_ = false;
|
||||
bool flipY_ = false;
|
||||
bool autoPlay_ = false;
|
||||
std::vector<AnimationEventCallback> eventListeners_;
|
||||
|
||||
static const std::vector<std::array<int32_t, 6>> emptyBoxes_;
|
||||
|
||||
void setupControllerCallbacks();
|
||||
void dispatchEvent(const AnimationEvent &event);
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/als_parser.h>
|
||||
#include <extra2d/animation/animation_event.h>
|
||||
#include <extra2d/animation/animation_node.h>
|
||||
#include <extra2d/scene/node.h>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// CompositeAnimation - ALS 多层复合动画节点
|
||||
// 管理多个 AnimationNode 图层,统一控制播放
|
||||
// 对应 DNF 的 ALS 格式(多层动画叠加)
|
||||
// ============================================================================
|
||||
class CompositeAnimation : public Node {
|
||||
public:
|
||||
CompositeAnimation() = default;
|
||||
~CompositeAnimation() override = default;
|
||||
|
||||
// ------ 静态工厂 ------
|
||||
static Ptr<CompositeAnimation> create();
|
||||
static Ptr<CompositeAnimation> create(const std::string &alsFilePath);
|
||||
|
||||
// ------ 加载 ------
|
||||
bool loadFromFile(const std::string &alsFilePath);
|
||||
|
||||
// ------ 图层管理 ------
|
||||
void addLayer(Ptr<AnimationNode> node, int zOrder = 0);
|
||||
void removeLayer(size_t index);
|
||||
Ptr<AnimationNode> getLayer(size_t index) const;
|
||||
Ptr<AnimationNode> getMainLayer() const;
|
||||
size_t getLayerCount() const;
|
||||
|
||||
// ------ 统一播放控制 ------
|
||||
void play();
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void reset();
|
||||
void setPlaybackSpeed(float speed);
|
||||
void setLooping(bool loop);
|
||||
|
||||
bool isPlaying() const;
|
||||
bool isStopped() const;
|
||||
|
||||
// ------ 事件回调(绑定到主图层)------
|
||||
void setKeyframeCallback(KeyframeHitCallback callback);
|
||||
void setCompletionCallback(AnimationCompleteCallback callback);
|
||||
void addEventListener(AnimationEventCallback callback);
|
||||
|
||||
// ------ 视觉属性(应用到所有图层)------
|
||||
void setTintColor(const Color &color);
|
||||
void setFlipX(bool flip);
|
||||
void setFlipY(bool flip);
|
||||
|
||||
private:
|
||||
struct LayerEntry {
|
||||
Ptr<AnimationNode> node;
|
||||
int zOrder = 0;
|
||||
};
|
||||
std::vector<LayerEntry> layers_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <any>
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 帧属性键 - 强类型枚举替代原始 ANI 的字符串键
|
||||
// ============================================================================
|
||||
enum class FramePropertyKey : uint32 {
|
||||
// 事件触发
|
||||
SetFlag = 0x0001, // int: 关键帧回调索引
|
||||
PlaySound = 0x0002, // string: 音效路径
|
||||
|
||||
// 变换属性
|
||||
ImageRate = 0x0010, // Vec2: 缩放比例
|
||||
ImageRotate = 0x0011, // float: 旋转角度(度)
|
||||
ImageOffset = 0x0012, // Vec2: 额外位置偏移
|
||||
|
||||
// 视觉效果
|
||||
BlendLinearDodge = 0x0020, // bool: 线性减淡
|
||||
BlendAdditive = 0x0021, // bool: 加法混合
|
||||
ColorTint = 0x0022, // Color: RGBA 颜色
|
||||
|
||||
// 控制标记
|
||||
Interpolation = 0x0030, // bool: 启用到下一帧的插值
|
||||
Loop = 0x0031, // bool: 全局循环标记
|
||||
|
||||
// DNF ANI 扩展属性
|
||||
DamageType = 0x0040, // int: 伤害类型 (0=Normal, 1=SuperArmor, 2=Unbreakable)
|
||||
Shadow = 0x0041, // bool: 阴影
|
||||
FlipType = 0x0042, // int: 翻转类型 (1=Horizon, 2=Vertical, 3=All)
|
||||
Coord = 0x0043, // int: 坐标系
|
||||
LoopStart = 0x0044, // bool: 循环起始标记
|
||||
LoopEnd = 0x0045, // int: 循环结束帧数
|
||||
GraphicEffect = 0x0046, // int: 图形特效类型
|
||||
ClipRegion = 0x0047, // vector<int>: 裁剪区域 [4个int16]
|
||||
|
||||
// 用户自定义扩展区间 (0x1000+)
|
||||
UserDefined = 0x1000,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 帧属性值 - variant 多态值(优化版本)
|
||||
// 使用紧凑存储,常用小类型直接内联,大类型使用索引引用
|
||||
// ============================================================================
|
||||
|
||||
// 前向声明
|
||||
struct FramePropertyValue;
|
||||
|
||||
// 属性存储类型枚举
|
||||
enum class PropertyValueType : uint8_t {
|
||||
Empty = 0,
|
||||
Bool = 1,
|
||||
Int = 2,
|
||||
Float = 3,
|
||||
Vec2 = 4,
|
||||
Color = 5,
|
||||
String = 6, // 字符串使用索引引用
|
||||
IntVector = 7, // vector<int> 使用索引引用
|
||||
};
|
||||
|
||||
// 紧凑的属性值结构(16字节)
|
||||
struct FramePropertyValue {
|
||||
PropertyValueType type = PropertyValueType::Empty;
|
||||
uint8_t padding[3] = {0};
|
||||
|
||||
// 使用结构体包装非平凡类型,使其可以在union中使用
|
||||
struct Vec2Storage {
|
||||
float x, y;
|
||||
Vec2Storage() = default;
|
||||
Vec2Storage(const Vec2& v) : x(v.x), y(v.y) {}
|
||||
operator Vec2() const { return Vec2(x, y); }
|
||||
};
|
||||
|
||||
struct ColorStorage {
|
||||
float r, g, b, a;
|
||||
ColorStorage() = default;
|
||||
ColorStorage(const Color& c) : r(c.r), g(c.g), b(c.b), a(c.a) {}
|
||||
operator Color() const { return Color(r, g, b, a); }
|
||||
};
|
||||
|
||||
union Data {
|
||||
bool boolValue;
|
||||
int intValue;
|
||||
float floatValue;
|
||||
Vec2Storage vec2Value;
|
||||
ColorStorage colorValue;
|
||||
uint32_t stringIndex; // 字符串池索引
|
||||
uint32_t vectorIndex; // vector池索引
|
||||
|
||||
Data() : intValue(0) {} // 默认构造函数
|
||||
~Data() {} // 析构函数
|
||||
} data;
|
||||
|
||||
FramePropertyValue() : type(PropertyValueType::Empty) {}
|
||||
explicit FramePropertyValue(bool v) : type(PropertyValueType::Bool) { data.boolValue = v; }
|
||||
explicit FramePropertyValue(int v) : type(PropertyValueType::Int) { data.intValue = v; }
|
||||
explicit FramePropertyValue(float v) : type(PropertyValueType::Float) { data.floatValue = v; }
|
||||
explicit FramePropertyValue(const Vec2& v) : type(PropertyValueType::Vec2) { data.vec2Value = v; }
|
||||
explicit FramePropertyValue(const Color& v) : type(PropertyValueType::Color) { data.colorValue = v; }
|
||||
|
||||
bool isInline() const {
|
||||
return type <= PropertyValueType::Color;
|
||||
}
|
||||
|
||||
bool isString() const { return type == PropertyValueType::String; }
|
||||
bool isIntVector() const { return type == PropertyValueType::IntVector; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FramePropertyKey 的 hash 支持
|
||||
// ============================================================================
|
||||
struct FramePropertyKeyHash {
|
||||
size_t operator()(FramePropertyKey key) const noexcept {
|
||||
return std::hash<uint32>{}(static_cast<uint32>(key));
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FramePropertySet - 单帧属性集合(优化版本)
|
||||
// 使用紧凑存储和线性探测哈希表,提高缓存命中率
|
||||
// ============================================================================
|
||||
class FramePropertySet {
|
||||
public:
|
||||
FramePropertySet() = default;
|
||||
|
||||
// ------ 设置属性 ------
|
||||
void set(FramePropertyKey key, FramePropertyValue value);
|
||||
void set(FramePropertyKey key, bool value) { set(key, FramePropertyValue(value)); }
|
||||
void set(FramePropertyKey key, int value) { set(key, FramePropertyValue(value)); }
|
||||
void set(FramePropertyKey key, float value) { set(key, FramePropertyValue(value)); }
|
||||
void set(FramePropertyKey key, const Vec2& value) { set(key, FramePropertyValue(value)); }
|
||||
void set(FramePropertyKey key, const Color& value) { set(key, FramePropertyValue(value)); }
|
||||
void set(FramePropertyKey key, const std::string& value);
|
||||
void set(FramePropertyKey key, const std::vector<int>& value);
|
||||
|
||||
void setCustom(const std::string &key, std::any value);
|
||||
|
||||
// ------ 类型安全获取 ------
|
||||
template <typename T> std::optional<T> get(FramePropertyKey key) const;
|
||||
|
||||
template <typename T>
|
||||
T getOr(FramePropertyKey key, const T &defaultValue) const {
|
||||
auto result = get<T>(key);
|
||||
return result.value_or(defaultValue);
|
||||
}
|
||||
|
||||
std::optional<std::any> getCustom(const std::string &key) const;
|
||||
|
||||
// ------ 查询 ------
|
||||
bool has(FramePropertyKey key) const;
|
||||
bool hasCustom(const std::string &key) const;
|
||||
bool empty() const { return properties_.empty() && customProperties_.empty(); }
|
||||
size_t count() const { return properties_.size() + customProperties_.size(); }
|
||||
|
||||
// ------ 移除 ------
|
||||
void remove(FramePropertyKey key);
|
||||
void removeCustom(const std::string &key);
|
||||
void clear();
|
||||
|
||||
// ------ 迭代 ------
|
||||
using PropertyMap = std::unordered_map<FramePropertyKey, FramePropertyValue,
|
||||
FramePropertyKeyHash>;
|
||||
const PropertyMap &properties() const { return properties_; }
|
||||
|
||||
// ------ 链式 API ------
|
||||
FramePropertySet &withSetFlag(int index);
|
||||
FramePropertySet &withPlaySound(const std::string &path);
|
||||
FramePropertySet &withImageRate(const Vec2 &scale);
|
||||
FramePropertySet &withImageRotate(float degrees);
|
||||
FramePropertySet &withColorTint(const Color &color);
|
||||
FramePropertySet &withInterpolation(bool enabled = true);
|
||||
FramePropertySet &withBlendLinearDodge(bool enabled = true);
|
||||
FramePropertySet &withLoop(bool enabled = true);
|
||||
|
||||
private:
|
||||
PropertyMap properties_;
|
||||
std::unordered_map<std::string, std::any> customProperties_;
|
||||
|
||||
// 字符串池和vector池,用于存储大对象
|
||||
mutable std::vector<std::string> stringPool_;
|
||||
mutable std::vector<std::vector<int>> vectorPool_;
|
||||
mutable uint32_t nextStringIndex_ = 0;
|
||||
mutable uint32_t nextVectorIndex_ = 0;
|
||||
|
||||
uint32_t allocateString(const std::string& str);
|
||||
uint32_t allocateVector(const std::vector<int>& vec);
|
||||
const std::string* getString(uint32_t index) const;
|
||||
const std::vector<int>* getVector(uint32_t index) const;
|
||||
};
|
||||
|
||||
// 模板特化声明
|
||||
template <> std::optional<bool> FramePropertySet::get<bool>(FramePropertyKey key) const;
|
||||
template <> std::optional<int> FramePropertySet::get<int>(FramePropertyKey key) const;
|
||||
template <> std::optional<float> FramePropertySet::get<float>(FramePropertyKey key) const;
|
||||
template <> std::optional<Vec2> FramePropertySet::get<Vec2>(FramePropertyKey key) const;
|
||||
template <> std::optional<Color> FramePropertySet::get<Color>(FramePropertyKey key) const;
|
||||
template <> std::optional<std::string> FramePropertySet::get<std::string>(FramePropertyKey key) const;
|
||||
template <> std::optional<std::vector<int>> FramePropertySet::get<std::vector<int>>(FramePropertyKey key) const;
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/animation_frame.h>
|
||||
#include <extra2d/animation/interpolation_engine.h>
|
||||
#include <extra2d/animation/sprite_frame.h>
|
||||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// FrameRenderer - 帧渲染器
|
||||
// 单渲染器 + SpriteFrame 引用策略,替代 N帧=N个Sprite 的旧设计
|
||||
// 负责预加载帧的 SpriteFrame、渲染当前帧、处理混合模式
|
||||
// ============================================================================
|
||||
class FrameRenderer {
|
||||
public:
|
||||
FrameRenderer() = default;
|
||||
|
||||
// ------ 预加载 ------
|
||||
// 解析所有帧的 SpriteFrame(通过 SpriteFrameCache)
|
||||
bool preloadFrames(const std::vector<AnimationFrame> &frames);
|
||||
void releaseFrames();
|
||||
|
||||
// ------ 渲染当前帧 ------
|
||||
void renderFrame(RenderBackend &renderer, const AnimationFrame &frame,
|
||||
size_t frameIndex, const Vec2 &position, float nodeOpacity,
|
||||
const Color &tintColor, bool flipX, bool flipY);
|
||||
|
||||
// ------ 渲染插值帧 ------
|
||||
void renderInterpolated(RenderBackend &renderer,
|
||||
const AnimationFrame &fromFrame, size_t fromIndex,
|
||||
const InterpolatedProperties &props,
|
||||
const Vec2 &position, float nodeOpacity,
|
||||
const Color &tintColor, bool flipX, bool flipY);
|
||||
|
||||
// ------ 混合模式映射 ------
|
||||
static BlendMode mapBlendMode(const FramePropertySet &props);
|
||||
|
||||
// ------ 查询 ------
|
||||
Ptr<SpriteFrame> getSpriteFrame(size_t frameIndex) const;
|
||||
Size getMaxFrameSize() const { return maxFrameSize_; }
|
||||
bool isLoaded() const { return !spriteFrames_.empty(); }
|
||||
|
||||
private:
|
||||
std::vector<Ptr<SpriteFrame>> spriteFrames_;
|
||||
Size maxFrameSize_;
|
||||
|
||||
void drawSpriteFrame(RenderBackend &renderer, Ptr<SpriteFrame> sf,
|
||||
const Vec2 &position, const Vec2 &offset,
|
||||
const Vec2 &scale, float rotation, float opacity,
|
||||
const Color &tint, bool flipX, bool flipY,
|
||||
BlendMode blend);
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <extra2d/animation/animation_frame.h>
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 插值结果 - 两帧之间的插值后属性
|
||||
// ============================================================================
|
||||
struct InterpolatedProperties {
|
||||
Vec2 position;
|
||||
Vec2 scale = Vec2::One();
|
||||
float rotation = 0.0f;
|
||||
Color color = Colors::White;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 插值曲线类型
|
||||
// ============================================================================
|
||||
enum class InterpolationCurve : uint8 {
|
||||
Linear, // 线性(原始系统的 uniform velocity)
|
||||
EaseIn, // 缓入
|
||||
EaseOut, // 缓出
|
||||
EaseInOut, // 缓入缓出
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// InterpolationEngine - 帧间属性插值计算(静态方法,无状态)
|
||||
// 独立于 AnimationController,可复用于其他系统
|
||||
// ============================================================================
|
||||
class InterpolationEngine {
|
||||
public:
|
||||
/// 核心插值计算:根据 t 因子 (0~1) 计算两帧之间的插值属性
|
||||
static InterpolatedProperties
|
||||
interpolate(const AnimationFrame &from, const AnimationFrame &to, float t,
|
||||
InterpolationCurve curve = InterpolationCurve::Linear) {
|
||||
float curvedT = applyCurve(t, curve);
|
||||
|
||||
InterpolatedProperties result;
|
||||
result.position = lerpPosition(from, to, curvedT);
|
||||
result.scale = lerpScale(from, to, curvedT);
|
||||
result.rotation = lerpRotation(from, to, curvedT);
|
||||
result.color = lerpColor(from, to, curvedT);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 位置插值
|
||||
static Vec2 lerpPosition(const AnimationFrame &from, const AnimationFrame &to,
|
||||
float t) {
|
||||
return Vec2::lerp(from.offset, to.offset, t);
|
||||
}
|
||||
|
||||
/// 缩放插值
|
||||
static Vec2 lerpScale(const AnimationFrame &from, const AnimationFrame &to,
|
||||
float t) {
|
||||
Vec2 fromScale = from.getEffectiveScale();
|
||||
Vec2 toScale = to.getEffectiveScale();
|
||||
return Vec2::lerp(fromScale, toScale, t);
|
||||
}
|
||||
|
||||
/// 旋转插值
|
||||
static float lerpRotation(const AnimationFrame &from,
|
||||
const AnimationFrame &to, float t) {
|
||||
float fromRot = from.getEffectiveRotation();
|
||||
float toRot = to.getEffectiveRotation();
|
||||
return math::lerp(fromRot, toRot, t);
|
||||
}
|
||||
|
||||
/// 颜色插值
|
||||
static Color lerpColor(const AnimationFrame &from, const AnimationFrame &to,
|
||||
float t) {
|
||||
Color fromColor = from.getEffectiveColor();
|
||||
Color toColor = to.getEffectiveColor();
|
||||
return Color::lerp(fromColor, toColor, t);
|
||||
}
|
||||
|
||||
/// 应用曲线函数
|
||||
static float applyCurve(float t, InterpolationCurve curve) {
|
||||
t = math::clamp(t, 0.0f, 1.0f);
|
||||
|
||||
switch (curve) {
|
||||
case InterpolationCurve::Linear:
|
||||
return t;
|
||||
|
||||
case InterpolationCurve::EaseIn:
|
||||
return t * t;
|
||||
|
||||
case InterpolationCurve::EaseOut:
|
||||
return t * (2.0f - t);
|
||||
|
||||
case InterpolationCurve::EaseInOut:
|
||||
if (t < 0.5f)
|
||||
return 2.0f * t * t;
|
||||
else
|
||||
return -1.0f + (4.0f - 2.0f * t) * t;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/graphics/texture.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// SpriteFrame - 精灵帧(纹理 + 区域 + 偏移的中间抽象)
|
||||
// 借鉴 Cocos2d-x SpriteFrame:解耦纹理物理存储与逻辑帧
|
||||
// 一个纹理图集可包含多个 SpriteFrame,减少纹理切换提升渲染性能
|
||||
// ============================================================================
|
||||
class SpriteFrame {
|
||||
public:
|
||||
SpriteFrame() = default;
|
||||
|
||||
SpriteFrame(Ptr<Texture> texture, const Rect &rect)
|
||||
: texture_(std::move(texture)), rect_(rect), originalSize_(rect.size) {}
|
||||
|
||||
SpriteFrame(Ptr<Texture> texture, const Rect &rect, const Vec2 &offset,
|
||||
const Size &originalSize)
|
||||
: texture_(std::move(texture)), rect_(rect), offset_(offset),
|
||||
originalSize_(originalSize) {}
|
||||
|
||||
// ------ 静态创建 ------
|
||||
static Ptr<SpriteFrame> create(Ptr<Texture> texture, const Rect &rect) {
|
||||
return makePtr<SpriteFrame>(std::move(texture), rect);
|
||||
}
|
||||
|
||||
static Ptr<SpriteFrame> create(Ptr<Texture> texture, const Rect &rect,
|
||||
const Vec2 &offset, const Size &originalSize) {
|
||||
return makePtr<SpriteFrame>(std::move(texture), rect, offset, originalSize);
|
||||
}
|
||||
|
||||
// ------ 纹理信息 ------
|
||||
void setTexture(Ptr<Texture> texture) { texture_ = std::move(texture); }
|
||||
Ptr<Texture> getTexture() const { return texture_; }
|
||||
|
||||
// ------ 矩形区域(在纹理图集中的位置)------
|
||||
void setRect(const Rect &rect) { rect_ = rect; }
|
||||
const Rect &getRect() const { return rect_; }
|
||||
|
||||
// ------ 偏移(图集打包时的裁剪偏移)------
|
||||
void setOffset(const Vec2 &offset) { offset_ = offset; }
|
||||
const Vec2 &getOffset() const { return offset_; }
|
||||
|
||||
// ------ 原始尺寸(裁剪前的完整尺寸)------
|
||||
void setOriginalSize(const Size &size) { originalSize_ = size; }
|
||||
const Size &getOriginalSize() const { return originalSize_; }
|
||||
|
||||
// ------ 旋转标志(图集工具可能旋转90度)------
|
||||
void setRotated(bool rotated) { rotated_ = rotated; }
|
||||
bool isRotated() const { return rotated_; }
|
||||
|
||||
// ------ 名称(用于缓存索引)------
|
||||
void setName(const std::string &name) { name_ = name; }
|
||||
const std::string &getName() const { return name_; }
|
||||
|
||||
// ------ 有效性检查 ------
|
||||
bool isValid() const { return texture_ != nullptr; }
|
||||
|
||||
private:
|
||||
Ptr<Texture> texture_;
|
||||
Rect rect_;
|
||||
Vec2 offset_;
|
||||
Size originalSize_;
|
||||
bool rotated_ = false;
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/animation/sprite_frame.h>
|
||||
#include <extra2d/graphics/texture.h>
|
||||
#include <extra2d/resource/resource_manager.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// SpriteFrameCache - 精灵帧全局缓存(借鉴 Cocos SpriteFrameCache)
|
||||
// 全局单例管理所有精灵帧,避免重复创建,支持图集自动切割
|
||||
// ============================================================================
|
||||
class SpriteFrameCache {
|
||||
public:
|
||||
static SpriteFrameCache &getInstance() {
|
||||
static SpriteFrameCache instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ------ 添加帧 ------
|
||||
|
||||
/// 添加单个精灵帧
|
||||
void addSpriteFrame(Ptr<SpriteFrame> frame, const std::string &name) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frames_[name] = std::move(frame);
|
||||
}
|
||||
|
||||
/// 从纹理和矩形区域创建并添加帧
|
||||
void addSpriteFrameFromTexture(Ptr<Texture> texture, const Rect &rect,
|
||||
const std::string &name) {
|
||||
auto frame = SpriteFrame::create(std::move(texture), rect);
|
||||
frame->setName(name);
|
||||
addSpriteFrame(std::move(frame), name);
|
||||
}
|
||||
|
||||
/// 从纹理图集批量切割添加(等宽等高网格)
|
||||
void addSpriteFramesFromGrid(const std::string &texturePath, int frameWidth,
|
||||
int frameHeight, int frameCount = -1,
|
||||
int spacing = 0, int margin = 0) {
|
||||
auto texture = loadTextureFromFile(texturePath);
|
||||
if (!texture)
|
||||
return;
|
||||
addSpriteFramesFromGrid(texture, texturePath, frameWidth, frameHeight,
|
||||
frameCount, spacing, margin);
|
||||
}
|
||||
|
||||
/// 从纹理对象批量切割添加(等宽等高网格,无需走 TexturePool)
|
||||
void addSpriteFramesFromGrid(Ptr<Texture> texture,
|
||||
const std::string &keyPrefix, int frameWidth,
|
||||
int frameHeight, int frameCount = -1,
|
||||
int spacing = 0, int margin = 0) {
|
||||
if (!texture)
|
||||
return;
|
||||
|
||||
int texW = texture->getWidth();
|
||||
int texH = texture->getHeight();
|
||||
int usableW = texW - 2 * margin;
|
||||
int usableH = texH - 2 * margin;
|
||||
int cols = (usableW + spacing) / (frameWidth + spacing);
|
||||
int rows = (usableH + spacing) / (frameHeight + spacing);
|
||||
int total = (frameCount > 0) ? frameCount : cols * rows;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (int i = 0; i < total; ++i) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
if (row >= rows)
|
||||
break;
|
||||
|
||||
Rect rect(static_cast<float>(margin + col * (frameWidth + spacing)),
|
||||
static_cast<float>(margin + row * (frameHeight + spacing)),
|
||||
static_cast<float>(frameWidth),
|
||||
static_cast<float>(frameHeight));
|
||||
|
||||
std::string name = keyPrefix + "#" + std::to_string(i);
|
||||
auto frame = SpriteFrame::create(texture, rect);
|
||||
frame->setName(name);
|
||||
frames_[name] = std::move(frame);
|
||||
}
|
||||
}
|
||||
|
||||
// ------ 获取帧 ------
|
||||
|
||||
/// 按名称获取
|
||||
Ptr<SpriteFrame> getSpriteFrame(const std::string &name) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = frames_.find(name);
|
||||
if (it != frames_.end())
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// 通过路径+索引获取或创建(ANI 格式的定位方式)
|
||||
Ptr<SpriteFrame> getOrCreateFromFile(const std::string &texturePath,
|
||||
int index = 0) {
|
||||
std::string key = texturePath + "#" + std::to_string(index);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = frames_.find(key);
|
||||
if (it != frames_.end())
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// 缓存未命中,加载纹理并创建 SpriteFrame
|
||||
auto texture = loadTextureFromFile(texturePath);
|
||||
if (!texture)
|
||||
return nullptr;
|
||||
|
||||
// 默认整张纹理作为一帧(index=0),或用整张纹理
|
||||
Rect rect(0.0f, 0.0f, static_cast<float>(texture->getWidth()),
|
||||
static_cast<float>(texture->getHeight()));
|
||||
|
||||
auto frame = SpriteFrame::create(texture, rect);
|
||||
frame->setName(key);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frames_[key] = frame;
|
||||
return frame;
|
||||
}
|
||||
|
||||
// ------ 缓存管理 ------
|
||||
|
||||
bool has(const std::string &name) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return frames_.find(name) != frames_.end();
|
||||
}
|
||||
|
||||
void removeSpriteFrame(const std::string &name) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frames_.erase(name);
|
||||
}
|
||||
|
||||
/// 移除未被外部引用的精灵帧(use_count == 1 表示仅缓存自身持有)
|
||||
void removeUnusedSpriteFrames() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto it = frames_.begin(); it != frames_.end();) {
|
||||
if (it->second.use_count() == 1) {
|
||||
it = frames_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frames_.clear();
|
||||
}
|
||||
|
||||
size_t count() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return frames_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
SpriteFrameCache() = default;
|
||||
~SpriteFrameCache() = default;
|
||||
SpriteFrameCache(const SpriteFrameCache &) = delete;
|
||||
SpriteFrameCache &operator=(const SpriteFrameCache &) = delete;
|
||||
|
||||
/**
|
||||
* @brief 从文件加载纹理(使用 ResourceManager 的 LRU 缓存)
|
||||
* @param filepath 纹理文件路径
|
||||
* @return 纹理对象,失败返回nullptr
|
||||
*/
|
||||
Ptr<Texture> loadTextureFromFile(const std::string &filepath) {
|
||||
// 使用 ResourceManager 的纹理缓存机制
|
||||
// 这样可以享受 LRU 缓存、自动清理和缓存统计等功能
|
||||
auto &resources = ResourceManager::getInstance();
|
||||
|
||||
// 先检查缓存中是否已有该纹理
|
||||
auto texture = resources.getTexture(filepath);
|
||||
if (texture) {
|
||||
E2D_TRACE("SpriteFrameCache: 使用缓存纹理: {}", filepath);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// 缓存未命中,通过 ResourceManager 加载
|
||||
texture = resources.loadTexture(filepath);
|
||||
if (!texture) {
|
||||
E2D_ERROR("SpriteFrameCache: 加载纹理失败: {}", filepath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
E2D_TRACE("SpriteFrameCache: 加载新纹理: {}", filepath);
|
||||
return texture;
|
||||
}
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, Ptr<SpriteFrame>> frames_;
|
||||
};
|
||||
|
||||
// 便捷宏
|
||||
#define E2D_SPRITE_FRAME_CACHE() ::extra2d::SpriteFrameCache::getInstance()
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/effects/particle_system.h>
|
||||
#include <extra2d/effects/post_process.h>
|
||||
#include <extra2d/graphics/shader_system.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 自定义特效类型
|
||||
// ============================================================================
|
||||
enum class CustomEffectType {
|
||||
Particle, // 粒子特效
|
||||
PostProcess, // 后处理特效
|
||||
Shader, // Shader特效
|
||||
Combined // 组合特效
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义特效配置
|
||||
// ============================================================================
|
||||
struct CustomEffectConfig {
|
||||
std::string name; // 特效名称
|
||||
CustomEffectType type; // 特效类型
|
||||
std::string description; // 描述
|
||||
|
||||
// 粒子特效配置
|
||||
EmitterConfig emitterConfig;
|
||||
|
||||
// 后处理特效配置
|
||||
std::string shaderVertPath; // 顶点着色器路径
|
||||
std::string shaderFragPath; // 片段着色器路径
|
||||
std::unordered_map<std::string, float> shaderParams; // Shader参数
|
||||
|
||||
// 通用配置
|
||||
float duration; // 持续时间(-1表示无限)
|
||||
bool loop; // 是否循环
|
||||
float delay; // 延迟启动时间
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义特效基类
|
||||
// ============================================================================
|
||||
class CustomEffect {
|
||||
public:
|
||||
explicit CustomEffect(const CustomEffectConfig &config);
|
||||
virtual ~CustomEffect() = default;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ------------------------------------------------------------------------
|
||||
virtual bool init();
|
||||
virtual void update(float dt);
|
||||
virtual void render(RenderBackend &renderer);
|
||||
virtual void shutdown();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 控制
|
||||
// ------------------------------------------------------------------------
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
void reset();
|
||||
|
||||
bool isPlaying() const { return playing_; }
|
||||
bool isFinished() const { return finished_; }
|
||||
float getElapsedTime() const { return elapsedTime_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 配置
|
||||
// ------------------------------------------------------------------------
|
||||
const std::string &getName() const { return config_.name; }
|
||||
const CustomEffectConfig &getConfig() const { return config_; }
|
||||
|
||||
void setPosition(const Vec2 &pos) { position_ = pos; }
|
||||
void setRotation(float rot) { rotation_ = rot; }
|
||||
void setScale(float scale) { scale_ = scale; }
|
||||
|
||||
Vec2 getPosition() const { return position_; }
|
||||
float getRotation() const { return rotation_; }
|
||||
float getScale() const { return scale_; }
|
||||
|
||||
protected:
|
||||
CustomEffectConfig config_;
|
||||
Vec2 position_ = Vec2::Zero();
|
||||
float rotation_ = 0.0f;
|
||||
float scale_ = 1.0f;
|
||||
|
||||
bool playing_ = false;
|
||||
bool paused_ = false;
|
||||
bool finished_ = false;
|
||||
float elapsedTime_ = 0.0f;
|
||||
float delayTimer_ = 0.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义粒子特效
|
||||
// ============================================================================
|
||||
class CustomParticleEffect : public CustomEffect {
|
||||
public:
|
||||
explicit CustomParticleEffect(const CustomEffectConfig &config);
|
||||
|
||||
bool init() override;
|
||||
void update(float dt) override;
|
||||
void render(RenderBackend &renderer) override;
|
||||
void shutdown() override;
|
||||
|
||||
void play();
|
||||
void stop();
|
||||
|
||||
Ptr<ParticleEmitter> getEmitter() { return emitter_; }
|
||||
|
||||
private:
|
||||
Ptr<ParticleSystem> particleSystem_;
|
||||
Ptr<ParticleEmitter> emitter_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义后处理特效
|
||||
// ============================================================================
|
||||
class CustomPostProcessEffect : public CustomEffect, public PostProcessEffect {
|
||||
public:
|
||||
explicit CustomPostProcessEffect(const CustomEffectConfig &config);
|
||||
|
||||
bool init() override;
|
||||
void update(float dt) override;
|
||||
void shutdown() override;
|
||||
|
||||
void onShaderBind(GLShader &shader) override;
|
||||
|
||||
void setParam(const std::string &name, float value);
|
||||
float getParam(const std::string &name) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, float> runtimeParams_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义特效工厂
|
||||
// ============================================================================
|
||||
class CustomEffectFactory {
|
||||
public:
|
||||
using EffectCreator =
|
||||
std::function<Ptr<CustomEffect>(const CustomEffectConfig &)>;
|
||||
|
||||
static CustomEffectFactory &getInstance();
|
||||
|
||||
// 注册自定义特效创建器
|
||||
void registerEffect(const std::string &typeName, EffectCreator creator);
|
||||
|
||||
// 创建特效
|
||||
Ptr<CustomEffect> create(const std::string &typeName,
|
||||
const CustomEffectConfig &config);
|
||||
|
||||
// 检查是否已注册
|
||||
bool isRegistered(const std::string &typeName) const;
|
||||
|
||||
// 获取所有已注册的类型
|
||||
std::vector<std::string> getRegisteredTypes() const;
|
||||
|
||||
private:
|
||||
CustomEffectFactory() = default;
|
||||
~CustomEffectFactory() = default;
|
||||
CustomEffectFactory(const CustomEffectFactory &) = delete;
|
||||
CustomEffectFactory &operator=(const CustomEffectFactory &) = delete;
|
||||
|
||||
std::unordered_map<std::string, EffectCreator> creators_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 自定义特效管理器
|
||||
// ============================================================================
|
||||
class CustomEffectManager {
|
||||
public:
|
||||
static CustomEffectManager &getInstance();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 初始化和关闭
|
||||
// ------------------------------------------------------------------------
|
||||
bool init();
|
||||
void shutdown();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 特效管理
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 从文件加载特效配置(支持JSON和文本格式)
|
||||
* 自动检测格式:JSON格式优先,失败则回退到文本格式
|
||||
*/
|
||||
bool loadFromFile(const std::string &filepath);
|
||||
|
||||
/**
|
||||
* @brief 从文本文件加载特效配置(简化格式)
|
||||
* 格式:每行一个参数,如 EMISSION 100
|
||||
*/
|
||||
bool loadFromTextFile(const std::string &filepath);
|
||||
|
||||
/**
|
||||
* @brief 保存特效配置到文件
|
||||
* @param useJson true=JSON格式, false=文本格式
|
||||
*/
|
||||
bool saveToFile(const std::string &name, const std::string &filepath,
|
||||
bool useJson = true);
|
||||
|
||||
/**
|
||||
* @brief 保存所有特效配置到一个JSON文件
|
||||
*/
|
||||
bool saveAllToFile(const std::string &filepath);
|
||||
|
||||
/**
|
||||
* @brief 注册特效配置
|
||||
*/
|
||||
void registerConfig(const std::string &name,
|
||||
const CustomEffectConfig &config);
|
||||
|
||||
/**
|
||||
* @brief 获取特效配置
|
||||
*/
|
||||
CustomEffectConfig *getConfig(const std::string &name);
|
||||
|
||||
/**
|
||||
* @brief 移除特效配置
|
||||
*/
|
||||
void removeConfig(const std::string &name);
|
||||
|
||||
/**
|
||||
* @brief 获取所有配置名称
|
||||
*/
|
||||
std::vector<std::string> getConfigNames() const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 特效实例管理
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 创建特效实例
|
||||
*/
|
||||
Ptr<CustomEffect> createEffect(const std::string &name);
|
||||
|
||||
/**
|
||||
* @brief 从配置直接创建特效
|
||||
*/
|
||||
Ptr<CustomEffect> createEffectFromConfig(const CustomEffectConfig &config);
|
||||
|
||||
/**
|
||||
* @brief 销毁特效实例
|
||||
*/
|
||||
void destroyEffect(Ptr<CustomEffect> effect);
|
||||
|
||||
/**
|
||||
* @brief 更新所有特效
|
||||
*/
|
||||
void update(float dt);
|
||||
|
||||
/**
|
||||
* @brief 渲染所有特效
|
||||
*/
|
||||
void render(RenderBackend &renderer);
|
||||
|
||||
/**
|
||||
* @brief 停止所有特效
|
||||
*/
|
||||
void stopAll();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 便捷方法
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 播放特效(简写)
|
||||
*/
|
||||
Ptr<CustomEffect> play(const std::string &name, const Vec2 &position);
|
||||
|
||||
/**
|
||||
* @brief 播放特效并自动销毁
|
||||
*/
|
||||
void playOneShot(const std::string &name, const Vec2 &position);
|
||||
|
||||
private:
|
||||
CustomEffectManager() = default;
|
||||
~CustomEffectManager() = default;
|
||||
CustomEffectManager(const CustomEffectManager &) = delete;
|
||||
CustomEffectManager &operator=(const CustomEffectManager &) = delete;
|
||||
|
||||
std::unordered_map<std::string, CustomEffectConfig> configs_;
|
||||
std::vector<Ptr<CustomEffect>> activeEffects_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 便捷宏
|
||||
// ============================================================================
|
||||
#define E2D_CUSTOM_EFFECT_MANAGER() \
|
||||
::extra2d::CustomEffectManager::getInstance()
|
||||
#define E2D_CUSTOM_EFFECT_FACTORY() \
|
||||
::extra2d::CustomEffectFactory::getInstance()
|
||||
|
||||
// ============================================================================
|
||||
// 预设特效快速创建
|
||||
// ============================================================================
|
||||
class EffectBuilder {
|
||||
public:
|
||||
// 粒子特效
|
||||
static CustomEffectConfig Particle(const std::string &name);
|
||||
static CustomEffectConfig Fire(const std::string &name);
|
||||
static CustomEffectConfig Smoke(const std::string &name);
|
||||
static CustomEffectConfig Explosion(const std::string &name);
|
||||
static CustomEffectConfig Magic(const std::string &name);
|
||||
static CustomEffectConfig Sparkle(const std::string &name);
|
||||
|
||||
// 后处理特效
|
||||
static CustomEffectConfig Bloom(const std::string &name);
|
||||
static CustomEffectConfig Blur(const std::string &name);
|
||||
static CustomEffectConfig Vignette(const std::string &name);
|
||||
static CustomEffectConfig ColorGrading(const std::string &name);
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/graphics/texture.h>
|
||||
#include <extra2d/scene/node.h>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 快速随机数生成器 - 使用 xorshift 算法,比 std::mt19937 更快
|
||||
// ============================================================================
|
||||
class FastRNG {
|
||||
public:
|
||||
explicit FastRNG(uint32_t seed = 0) : state_(seed ? seed : 0x853c49e67) {}
|
||||
|
||||
float nextFloat() {
|
||||
return static_cast<float>(next()) / static_cast<float>(UINT32_MAX);
|
||||
}
|
||||
|
||||
float nextFloat(float min, float max) {
|
||||
return min + (max - min) * nextFloat();
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t state_;
|
||||
|
||||
uint32_t next() {
|
||||
uint32_t x = state_;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
state_ = x;
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 粒子数据
|
||||
// ============================================================================
|
||||
struct Particle {
|
||||
Vec2 position;
|
||||
Vec2 velocity;
|
||||
Vec2 acceleration;
|
||||
float rotation;
|
||||
float angularVelocity;
|
||||
float size;
|
||||
float sizeDelta;
|
||||
Color color;
|
||||
Color colorDelta;
|
||||
float life;
|
||||
float maxLife;
|
||||
bool active;
|
||||
|
||||
Particle()
|
||||
: position(Vec2::Zero()), velocity(Vec2::Zero()),
|
||||
acceleration(Vec2::Zero()), rotation(0.0f), angularVelocity(0.0f),
|
||||
size(1.0f), sizeDelta(0.0f), color(Colors::White),
|
||||
colorDelta(Colors::Transparent), life(0.0f), maxLife(1.0f),
|
||||
active(false) {}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 发射器配置
|
||||
// ============================================================================
|
||||
struct EmitterConfig {
|
||||
// 发射速率
|
||||
float emissionRate = 100.0f; // 每秒发射粒子数
|
||||
float emissionDuration = -1.0f; // 发射持续时间(-1表示无限)
|
||||
|
||||
// 粒子生命周期
|
||||
float minLife = 1.0f;
|
||||
float maxLife = 2.0f;
|
||||
|
||||
// 粒子大小
|
||||
float minStartSize = 10.0f;
|
||||
float maxStartSize = 20.0f;
|
||||
float minEndSize = 0.0f;
|
||||
float maxEndSize = 5.0f;
|
||||
|
||||
// 粒子速度
|
||||
Vec2 minVelocity = Vec2(-50.0f, -50.0f);
|
||||
Vec2 maxVelocity = Vec2(50.0f, 50.0f);
|
||||
|
||||
// 粒子加速度
|
||||
Vec2 acceleration = Vec2(0.0f, -100.0f); // 重力
|
||||
|
||||
// 粒子旋转
|
||||
float minRotation = 0.0f;
|
||||
float maxRotation = 360.0f;
|
||||
float minAngularVelocity = -90.0f;
|
||||
float maxAngularVelocity = 90.0f;
|
||||
|
||||
// 颜色
|
||||
Color startColor = Colors::White;
|
||||
Color endColor = Colors::Transparent;
|
||||
|
||||
// 发射形状
|
||||
enum class Shape {
|
||||
Point, // 点发射
|
||||
Circle, // 圆形区域
|
||||
Rectangle, // 矩形区域
|
||||
Cone // 锥形
|
||||
};
|
||||
Shape shape = Shape::Point;
|
||||
float shapeRadius = 50.0f; // 圆形/锥形半径
|
||||
Vec2 shapeSize = Vec2(100.0f, 100.0f); // 矩形大小
|
||||
float coneAngle = 45.0f; // 锥形角度
|
||||
|
||||
// 纹理
|
||||
Ptr<Texture> texture = nullptr;
|
||||
|
||||
// 混合模式
|
||||
BlendMode blendMode = BlendMode::Additive;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 粒子发射器
|
||||
// ============================================================================
|
||||
class ParticleEmitter {
|
||||
public:
|
||||
ParticleEmitter();
|
||||
~ParticleEmitter() = default;
|
||||
|
||||
// 形状生成函数(公有,用于查找表)
|
||||
Vec2 randomPointShape();
|
||||
Vec2 randomCircleShape();
|
||||
Vec2 randomRectangleShape();
|
||||
Vec2 randomConeShape();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 初始化和关闭
|
||||
// ------------------------------------------------------------------------
|
||||
bool init(size_t maxParticles);
|
||||
void shutdown();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 配置
|
||||
// ------------------------------------------------------------------------
|
||||
void setConfig(const EmitterConfig &config) { config_ = config; }
|
||||
const EmitterConfig &getConfig() const { return config_; }
|
||||
|
||||
// 链式配置API
|
||||
ParticleEmitter &withEmissionRate(float rate) {
|
||||
config_.emissionRate = rate;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withLife(float minLife, float maxLife) {
|
||||
config_.minLife = minLife;
|
||||
config_.maxLife = maxLife;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withSize(float minStart, float maxStart, float minEnd = 0.0f,
|
||||
float maxEnd = 0.0f) {
|
||||
config_.minStartSize = minStart;
|
||||
config_.maxStartSize = maxStart;
|
||||
config_.minEndSize = minEnd;
|
||||
config_.maxEndSize = maxEnd;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withVelocity(const Vec2 &minVel, const Vec2 &maxVel) {
|
||||
config_.minVelocity = minVel;
|
||||
config_.maxVelocity = maxVel;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withAcceleration(const Vec2 &accel) {
|
||||
config_.acceleration = accel;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withColor(const Color &start, const Color &end) {
|
||||
config_.startColor = start;
|
||||
config_.endColor = end;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withTexture(Ptr<Texture> texture) {
|
||||
config_.texture = texture;
|
||||
return *this;
|
||||
}
|
||||
ParticleEmitter &withBlendMode(BlendMode mode) {
|
||||
config_.blendMode = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 发射控制
|
||||
// ------------------------------------------------------------------------
|
||||
void start();
|
||||
void stop();
|
||||
void burst(int count); // 爆发发射
|
||||
void reset();
|
||||
bool isEmitting() const { return emitting_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 更新和渲染
|
||||
// ------------------------------------------------------------------------
|
||||
void update(float dt);
|
||||
void render(RenderBackend &renderer);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 状态查询
|
||||
// ------------------------------------------------------------------------
|
||||
size_t getActiveParticleCount() const { return activeCount_; }
|
||||
size_t getMaxParticles() const { return particles_.size(); }
|
||||
bool isActive() const { return activeCount_ > 0 || emitting_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 变换
|
||||
// ------------------------------------------------------------------------
|
||||
void setPosition(const Vec2 &pos) { position_ = pos; }
|
||||
void setRotation(float rot) { rotation_ = rot; }
|
||||
Vec2 getPosition() const { return position_; }
|
||||
float getRotation() const { return rotation_; }
|
||||
|
||||
private:
|
||||
EmitterConfig config_;
|
||||
std::vector<Particle> particles_;
|
||||
size_t activeCount_ = 0;
|
||||
|
||||
Vec2 position_ = Vec2::Zero();
|
||||
float rotation_ = 0.0f;
|
||||
|
||||
bool emitting_ = false;
|
||||
float emissionTimer_ = 0.0f;
|
||||
float emissionTime_ = 0.0f;
|
||||
|
||||
FastRNG rng_; // 使用快速 RNG 替代 std::mt19937
|
||||
|
||||
void emitParticle();
|
||||
float randomFloat(float min, float max);
|
||||
Vec2 randomPointInShape();
|
||||
Vec2 randomVelocity();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 粒子系统 - 管理多个发射器
|
||||
// ============================================================================
|
||||
class ParticleSystem : public Node {
|
||||
public:
|
||||
ParticleSystem();
|
||||
~ParticleSystem() override = default;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 静态创建方法
|
||||
// ------------------------------------------------------------------------
|
||||
static Ptr<ParticleSystem> create();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 发射器管理
|
||||
// ------------------------------------------------------------------------
|
||||
Ptr<ParticleEmitter> addEmitter(const EmitterConfig &config = {});
|
||||
void removeEmitter(Ptr<ParticleEmitter> emitter);
|
||||
void removeAllEmitters();
|
||||
size_t getEmitterCount() const { return emitters_.size(); }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 全局控制
|
||||
// ------------------------------------------------------------------------
|
||||
void startAll();
|
||||
void stopAll();
|
||||
void resetAll();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 预设
|
||||
// ------------------------------------------------------------------------
|
||||
static EmitterConfig PresetFire();
|
||||
static EmitterConfig PresetSmoke();
|
||||
static EmitterConfig PresetExplosion();
|
||||
static EmitterConfig PresetSparkle();
|
||||
static EmitterConfig PresetRain();
|
||||
static EmitterConfig PresetSnow();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 重写Node方法
|
||||
// ------------------------------------------------------------------------
|
||||
void onUpdate(float dt) override;
|
||||
void onDraw(RenderBackend &renderer) override;
|
||||
|
||||
private:
|
||||
std::vector<Ptr<ParticleEmitter>> emitters_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 粒子预设(便捷类)
|
||||
// ============================================================================
|
||||
class ParticlePreset {
|
||||
public:
|
||||
static EmitterConfig Fire();
|
||||
static EmitterConfig Smoke();
|
||||
static EmitterConfig Explosion();
|
||||
static EmitterConfig Sparkle();
|
||||
static EmitterConfig Rain();
|
||||
static EmitterConfig Snow();
|
||||
static EmitterConfig Magic();
|
||||
static EmitterConfig Bubbles();
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/graphics/opengl/gl_shader.h>
|
||||
#include <extra2d/graphics/texture.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 前向声明
|
||||
// ============================================================================
|
||||
class RenderTarget;
|
||||
class RenderBackend;
|
||||
|
||||
// ============================================================================
|
||||
// 后处理效果基类
|
||||
// ============================================================================
|
||||
class PostProcessEffect {
|
||||
public:
|
||||
PostProcessEffect(const std::string &name);
|
||||
virtual ~PostProcessEffect() = default;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 初始化效果
|
||||
*/
|
||||
virtual bool init();
|
||||
|
||||
/**
|
||||
* @brief 关闭效果
|
||||
*/
|
||||
virtual void shutdown();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 渲染
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 应用效果
|
||||
* @param source 输入纹理
|
||||
* @param target 输出渲染目标
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
virtual void apply(const Texture &source, RenderTarget &target,
|
||||
RenderBackend &renderer);
|
||||
|
||||
/**
|
||||
* @brief 设置Shader参数(子类重写)
|
||||
*/
|
||||
virtual void onShaderBind(GLShader &shader) {}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 状态
|
||||
// ------------------------------------------------------------------------
|
||||
const std::string &getName() const { return name_; }
|
||||
bool isEnabled() const { return enabled_; }
|
||||
void setEnabled(bool enabled) { enabled_ = enabled; }
|
||||
bool isValid() const { return valid_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 链式API
|
||||
// ------------------------------------------------------------------------
|
||||
PostProcessEffect &withEnabled(bool enabled) {
|
||||
enabled_ = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
bool enabled_ = true;
|
||||
bool valid_ = false;
|
||||
Ptr<GLShader> shader_;
|
||||
|
||||
/**
|
||||
* @brief 加载自定义Shader
|
||||
*/
|
||||
bool loadShader(const std::string &vertSource, const std::string &fragSource);
|
||||
bool loadShaderFromFile(const std::string &vertPath,
|
||||
const std::string &fragPath);
|
||||
|
||||
/**
|
||||
* @brief 渲染全屏四边形
|
||||
*/
|
||||
void renderFullscreenQuad();
|
||||
|
||||
private:
|
||||
static GLuint quadVao_;
|
||||
static GLuint quadVbo_;
|
||||
static bool quadInitialized_;
|
||||
|
||||
void initQuad();
|
||||
void destroyQuad();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 后处理栈 - 管理多个后处理效果
|
||||
// ============================================================================
|
||||
class PostProcessStack {
|
||||
public:
|
||||
PostProcessStack();
|
||||
~PostProcessStack();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 初始化和关闭
|
||||
// ------------------------------------------------------------------------
|
||||
bool init(int width, int height);
|
||||
void shutdown();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 效果管理
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 添加效果到栈
|
||||
*/
|
||||
void addEffect(Ptr<PostProcessEffect> effect);
|
||||
|
||||
/**
|
||||
* @brief 插入效果到指定位置
|
||||
*/
|
||||
void insertEffect(size_t index, Ptr<PostProcessEffect> effect);
|
||||
|
||||
/**
|
||||
* @brief 移除效果
|
||||
*/
|
||||
void removeEffect(const std::string &name);
|
||||
void removeEffect(size_t index);
|
||||
|
||||
/**
|
||||
* @brief 获取效果
|
||||
*/
|
||||
Ptr<PostProcessEffect> getEffect(const std::string &name);
|
||||
Ptr<PostProcessEffect> getEffect(size_t index);
|
||||
|
||||
/**
|
||||
* @brief 清空所有效果
|
||||
*/
|
||||
void clearEffects();
|
||||
|
||||
/**
|
||||
* @brief 获取效果数量
|
||||
*/
|
||||
size_t getEffectCount() const { return effects_.size(); }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 渲染
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 开始捕获场景
|
||||
*/
|
||||
void beginCapture();
|
||||
|
||||
/**
|
||||
* @brief 结束捕获并应用所有效果
|
||||
*/
|
||||
void endCapture(RenderBackend &renderer);
|
||||
|
||||
/**
|
||||
* @brief 直接应用效果到纹理
|
||||
*/
|
||||
void process(const Texture &source, RenderTarget &target,
|
||||
RenderBackend &renderer);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 配置
|
||||
// ------------------------------------------------------------------------
|
||||
void resize(int width, int height);
|
||||
bool isValid() const { return valid_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 便捷方法 - 添加内置效果
|
||||
// ------------------------------------------------------------------------
|
||||
PostProcessStack &addBloom(float intensity = 1.0f, float threshold = 0.8f);
|
||||
PostProcessStack &addBlur(float radius = 2.0f);
|
||||
PostProcessStack &addColorGrading(const Color &tint);
|
||||
PostProcessStack &addVignette(float intensity = 0.5f);
|
||||
PostProcessStack &addChromaticAberration(float amount = 1.0f);
|
||||
|
||||
private:
|
||||
std::vector<Ptr<PostProcessEffect>> effects_;
|
||||
Ptr<RenderTarget> renderTargetA_;
|
||||
Ptr<RenderTarget> renderTargetB_;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
bool valid_ = false;
|
||||
bool capturing_ = false;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 全局后处理管理
|
||||
// ============================================================================
|
||||
class PostProcessManager {
|
||||
public:
|
||||
static PostProcessManager &getInstance();
|
||||
|
||||
void init(int width, int height);
|
||||
void shutdown();
|
||||
|
||||
PostProcessStack &getMainStack() { return mainStack_; }
|
||||
|
||||
void resize(int width, int height);
|
||||
void beginFrame();
|
||||
void endFrame(RenderBackend &renderer);
|
||||
|
||||
private:
|
||||
PostProcessManager() = default;
|
||||
~PostProcessManager() = default;
|
||||
PostProcessManager(const PostProcessManager &) = delete;
|
||||
PostProcessManager &operator=(const PostProcessManager &) = delete;
|
||||
|
||||
PostProcessStack mainStack_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 便捷宏
|
||||
// ============================================================================
|
||||
#define E2D_POST_PROCESS() ::extra2d::PostProcessManager::getInstance()
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -36,24 +36,6 @@
|
|||
#include <extra2d/scene/transition_flip_scene.h>
|
||||
#include <extra2d/scene/transition_box_scene.h>
|
||||
|
||||
// Animation
|
||||
#include <extra2d/animation/sprite_frame.h>
|
||||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
#include <extra2d/animation/frame_property.h>
|
||||
#include <extra2d/animation/animation_frame.h>
|
||||
#include <extra2d/animation/animation_clip.h>
|
||||
#include <extra2d/animation/animation_controller.h>
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/animation/interpolation_engine.h>
|
||||
#include <extra2d/animation/animated_sprite.h>
|
||||
#include <extra2d/animation/frame_renderer.h>
|
||||
#include <extra2d/animation/animation_event.h>
|
||||
#include <extra2d/animation/animation_node.h>
|
||||
#include <extra2d/animation/composite_animation.h>
|
||||
#include <extra2d/animation/ani_parser.h>
|
||||
#include <extra2d/animation/ani_binary_parser.h>
|
||||
#include <extra2d/animation/als_parser.h>
|
||||
|
||||
// UI
|
||||
#include <extra2d/ui/widget.h>
|
||||
#include <extra2d/ui/button.h>
|
||||
|
|
@ -64,18 +46,6 @@
|
|||
#include <extra2d/ui/radio_button.h>
|
||||
#include <extra2d/ui/slider.h>
|
||||
|
||||
// Action
|
||||
#include <extra2d/action/action.h>
|
||||
#include <extra2d/action/finite_time_action.h>
|
||||
#include <extra2d/action/action_interval.h>
|
||||
#include <extra2d/action/action_instant.h>
|
||||
#include <extra2d/action/action_interval_actions.h>
|
||||
#include <extra2d/action/action_instant_actions.h>
|
||||
#include <extra2d/action/action_ease.h>
|
||||
#include <extra2d/action/action_special.h>
|
||||
#include <extra2d/action/action_manager.h>
|
||||
#include <extra2d/action/ease.h>
|
||||
|
||||
// Event
|
||||
#include <extra2d/event/event.h>
|
||||
#include <extra2d/event/event_queue.h>
|
||||
|
|
@ -101,14 +71,9 @@
|
|||
#include <extra2d/spatial/spatial_hash.h>
|
||||
#include <extra2d/spatial/spatial_manager.h>
|
||||
|
||||
// Effects
|
||||
#include <extra2d/effects/post_process.h>
|
||||
#include <extra2d/effects/particle_system.h>
|
||||
#include <extra2d/effects/custom_effect_manager.h>
|
||||
|
||||
// Application
|
||||
#include <extra2d/app/application.h>
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ namespace extra2d {
|
|||
|
||||
// 前向声明
|
||||
class Scene;
|
||||
class Action;
|
||||
class RenderBackend;
|
||||
struct RenderCommand;
|
||||
|
||||
|
|
@ -155,58 +154,6 @@ public:
|
|||
// 更新空间索引(手动调用,通常在边界框变化后)
|
||||
void updateSpatialIndex();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 动作系统
|
||||
// ------------------------------------------------------------------------
|
||||
/**
|
||||
* @brief 运行动作
|
||||
* @param action 动作指针(所有权转移)
|
||||
* @return 动作指针
|
||||
*/
|
||||
Action* runAction(Action* action);
|
||||
|
||||
/**
|
||||
* @brief 停止所有动作
|
||||
*/
|
||||
void stopAllActions();
|
||||
|
||||
/**
|
||||
* @brief 停止指定动作
|
||||
* @param action 动作指针
|
||||
*/
|
||||
void stopAction(Action* action);
|
||||
|
||||
/**
|
||||
* @brief 根据标签停止动作
|
||||
* @param tag 标签值
|
||||
*/
|
||||
void stopActionByTag(int tag);
|
||||
|
||||
/**
|
||||
* @brief 根据标志位停止动作
|
||||
* @param flags 标志位
|
||||
*/
|
||||
void stopActionsByFlags(unsigned int flags);
|
||||
|
||||
/**
|
||||
* @brief 根据标签获取动作
|
||||
* @param tag 标签值
|
||||
* @return 动作指针,未找到返回 nullptr
|
||||
*/
|
||||
Action* getActionByTag(int tag);
|
||||
|
||||
/**
|
||||
* @brief 获取运行中的动作数量
|
||||
* @return 动作数量
|
||||
*/
|
||||
size_t getActionCount() const;
|
||||
|
||||
/**
|
||||
* @brief 检查是否有动作在运行
|
||||
* @return true 如果有动作在运行
|
||||
*/
|
||||
bool isRunningActions() const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 事件系统
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
#include "extra2d/action/action.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
Action::Action() : tag_(-1), flags_(0) {}
|
||||
|
||||
bool Action::isDone() const {
|
||||
return state_ == ActionState::Completed;
|
||||
}
|
||||
|
||||
void Action::startWithTarget(Node* target) {
|
||||
target_ = target;
|
||||
originalTarget_ = target;
|
||||
state_ = ActionState::Running;
|
||||
onStart();
|
||||
}
|
||||
|
||||
void Action::stop() {
|
||||
target_ = nullptr;
|
||||
state_ = ActionState::Completed;
|
||||
}
|
||||
|
||||
void Action::step(float dt) {
|
||||
(void)dt;
|
||||
}
|
||||
|
||||
void Action::update(float time) {
|
||||
(void)time;
|
||||
}
|
||||
|
||||
void Action::pause() {
|
||||
if (state_ == ActionState::Running) {
|
||||
state_ = ActionState::Paused;
|
||||
}
|
||||
}
|
||||
|
||||
void Action::resume() {
|
||||
if (state_ == ActionState::Paused) {
|
||||
state_ = ActionState::Running;
|
||||
}
|
||||
}
|
||||
|
||||
void Action::restart() {
|
||||
state_ = ActionState::Running;
|
||||
onStart();
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,683 +0,0 @@
|
|||
#include "extra2d/action/action_ease.h"
|
||||
#include <cmath>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846f
|
||||
#endif
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// ActionEase 基类
|
||||
// ============================================================================
|
||||
|
||||
ActionEase::~ActionEase() {
|
||||
delete innerAction_;
|
||||
}
|
||||
|
||||
bool ActionEase::initWithAction(ActionInterval* action) {
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
innerAction_ = action;
|
||||
duration_ = action->getDuration();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ActionEase::startWithTarget(Node* target) {
|
||||
ActionInterval::startWithTarget(target);
|
||||
innerAction_->startWithTarget(target);
|
||||
}
|
||||
|
||||
void ActionEase::stop() {
|
||||
innerAction_->stop();
|
||||
ActionInterval::stop();
|
||||
}
|
||||
|
||||
void ActionEase::update(float time) {
|
||||
innerAction_->update(time);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 指数缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseExponentialIn* EaseExponentialIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseExponentialIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseExponentialIn::update(float time) {
|
||||
innerAction_->update(easeInExpo(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialIn::clone() const {
|
||||
return EaseExponentialIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialIn::reverse() const {
|
||||
return EaseExponentialOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseExponentialOut* EaseExponentialOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseExponentialOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseExponentialOut::update(float time) {
|
||||
innerAction_->update(easeOutExpo(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialOut::clone() const {
|
||||
return EaseExponentialOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialOut::reverse() const {
|
||||
return EaseExponentialIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseExponentialInOut* EaseExponentialInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseExponentialInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseExponentialInOut::update(float time) {
|
||||
innerAction_->update(easeInOutExpo(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialInOut::clone() const {
|
||||
return EaseExponentialInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseExponentialInOut::reverse() const {
|
||||
return EaseExponentialInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 正弦缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseSineIn* EaseSineIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseSineIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseSineIn::update(float time) {
|
||||
innerAction_->update(easeInSine(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineIn::clone() const {
|
||||
return EaseSineIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineIn::reverse() const {
|
||||
return EaseSineOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseSineOut* EaseSineOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseSineOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseSineOut::update(float time) {
|
||||
innerAction_->update(easeOutSine(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineOut::clone() const {
|
||||
return EaseSineOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineOut::reverse() const {
|
||||
return EaseSineIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseSineInOut* EaseSineInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseSineInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseSineInOut::update(float time) {
|
||||
innerAction_->update(easeInOutSine(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineInOut::clone() const {
|
||||
return EaseSineInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseSineInOut::reverse() const {
|
||||
return EaseSineInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 弹性缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseElasticIn* EaseElasticIn::create(ActionInterval* action, float period) {
|
||||
auto* ease = new EaseElasticIn();
|
||||
ease->initWithAction(action);
|
||||
ease->period_ = period;
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseElasticIn::update(float time) {
|
||||
float newT = 0.0f;
|
||||
if (time == 0.0f || time == 1.0f) {
|
||||
newT = time;
|
||||
} else {
|
||||
float s = period_ / 4.0f;
|
||||
time = time - 1.0f;
|
||||
newT = -std::pow(2.0f, 10.0f * time) * std::sin((time - s) * M_PI * 2.0f / period_);
|
||||
}
|
||||
innerAction_->update(newT);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticIn::clone() const {
|
||||
return EaseElasticIn::create(innerAction_->clone(), period_);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticIn::reverse() const {
|
||||
return EaseElasticOut::create(innerAction_->reverse(), period_);
|
||||
}
|
||||
|
||||
EaseElasticOut* EaseElasticOut::create(ActionInterval* action, float period) {
|
||||
auto* ease = new EaseElasticOut();
|
||||
ease->initWithAction(action);
|
||||
ease->period_ = period;
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseElasticOut::update(float time) {
|
||||
float newT = 0.0f;
|
||||
if (time == 0.0f || time == 1.0f) {
|
||||
newT = time;
|
||||
} else {
|
||||
float s = period_ / 4.0f;
|
||||
newT = std::pow(2.0f, -10.0f * time) * std::sin((time - s) * M_PI * 2.0f / period_) + 1.0f;
|
||||
}
|
||||
innerAction_->update(newT);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticOut::clone() const {
|
||||
return EaseElasticOut::create(innerAction_->clone(), period_);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticOut::reverse() const {
|
||||
return EaseElasticIn::create(innerAction_->reverse(), period_);
|
||||
}
|
||||
|
||||
EaseElasticInOut* EaseElasticInOut::create(ActionInterval* action, float period) {
|
||||
auto* ease = new EaseElasticInOut();
|
||||
ease->initWithAction(action);
|
||||
ease->period_ = period;
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseElasticInOut::update(float time) {
|
||||
float newT = 0.0f;
|
||||
if (time == 0.0f || time == 1.0f) {
|
||||
newT = time;
|
||||
} else {
|
||||
time = time * 2.0f;
|
||||
if (period_ == 0.0f) {
|
||||
period_ = 0.3f * 1.5f;
|
||||
}
|
||||
float s = period_ / 4.0f;
|
||||
if (time < 1.0f) {
|
||||
time -= 1.0f;
|
||||
newT = -0.5f * std::pow(2.0f, 10.0f * time) * std::sin((time - s) * M_PI * 2.0f / period_);
|
||||
} else {
|
||||
time -= 1.0f;
|
||||
newT = std::pow(2.0f, -10.0f * time) * std::sin((time - s) * M_PI * 2.0f / period_) * 0.5f + 1.0f;
|
||||
}
|
||||
}
|
||||
innerAction_->update(newT);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticInOut::clone() const {
|
||||
return EaseElasticInOut::create(innerAction_->clone(), period_);
|
||||
}
|
||||
|
||||
ActionInterval* EaseElasticInOut::reverse() const {
|
||||
return EaseElasticInOut::create(innerAction_->reverse(), period_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 弹跳缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseBounceIn* EaseBounceIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBounceIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBounceIn::update(float time) {
|
||||
innerAction_->update(easeInBounce(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceIn::clone() const {
|
||||
return EaseBounceIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceIn::reverse() const {
|
||||
return EaseBounceOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseBounceOut* EaseBounceOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBounceOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBounceOut::update(float time) {
|
||||
innerAction_->update(easeOutBounce(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceOut::clone() const {
|
||||
return EaseBounceOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceOut::reverse() const {
|
||||
return EaseBounceIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseBounceInOut* EaseBounceInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBounceInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBounceInOut::update(float time) {
|
||||
innerAction_->update(easeInOutBounce(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceInOut::clone() const {
|
||||
return EaseBounceInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBounceInOut::reverse() const {
|
||||
return EaseBounceInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 回震缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseBackIn* EaseBackIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBackIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBackIn::update(float time) {
|
||||
innerAction_->update(easeInBack(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackIn::clone() const {
|
||||
return EaseBackIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackIn::reverse() const {
|
||||
return EaseBackOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseBackOut* EaseBackOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBackOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBackOut::update(float time) {
|
||||
innerAction_->update(easeOutBack(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackOut::clone() const {
|
||||
return EaseBackOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackOut::reverse() const {
|
||||
return EaseBackIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseBackInOut* EaseBackInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseBackInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseBackInOut::update(float time) {
|
||||
innerAction_->update(easeInOutBack(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackInOut::clone() const {
|
||||
return EaseBackInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseBackInOut::reverse() const {
|
||||
return EaseBackInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 二次缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseQuadIn* EaseQuadIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuadIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuadIn::update(float time) {
|
||||
innerAction_->update(easeInQuad(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadIn::clone() const {
|
||||
return EaseQuadIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadIn::reverse() const {
|
||||
return EaseQuadOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuadOut* EaseQuadOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuadOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuadOut::update(float time) {
|
||||
innerAction_->update(easeOutQuad(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadOut::clone() const {
|
||||
return EaseQuadOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadOut::reverse() const {
|
||||
return EaseQuadIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuadInOut* EaseQuadInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuadInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuadInOut::update(float time) {
|
||||
innerAction_->update(easeInOutQuad(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadInOut::clone() const {
|
||||
return EaseQuadInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuadInOut::reverse() const {
|
||||
return EaseQuadInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 三次缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseCubicIn* EaseCubicIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCubicIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCubicIn::update(float time) {
|
||||
innerAction_->update(easeInCubic(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicIn::clone() const {
|
||||
return EaseCubicIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicIn::reverse() const {
|
||||
return EaseCubicOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseCubicOut* EaseCubicOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCubicOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCubicOut::update(float time) {
|
||||
innerAction_->update(easeOutCubic(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicOut::clone() const {
|
||||
return EaseCubicOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicOut::reverse() const {
|
||||
return EaseCubicIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseCubicInOut* EaseCubicInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCubicInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCubicInOut::update(float time) {
|
||||
innerAction_->update(easeInOutCubic(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicInOut::clone() const {
|
||||
return EaseCubicInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCubicInOut::reverse() const {
|
||||
return EaseCubicInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 四次缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseQuartIn* EaseQuartIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuartIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuartIn::update(float time) {
|
||||
innerAction_->update(easeInQuart(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartIn::clone() const {
|
||||
return EaseQuartIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartIn::reverse() const {
|
||||
return EaseQuartOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuartOut* EaseQuartOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuartOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuartOut::update(float time) {
|
||||
innerAction_->update(easeOutQuart(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartOut::clone() const {
|
||||
return EaseQuartOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartOut::reverse() const {
|
||||
return EaseQuartIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuartInOut* EaseQuartInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuartInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuartInOut::update(float time) {
|
||||
innerAction_->update(easeInOutQuart(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartInOut::clone() const {
|
||||
return EaseQuartInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuartInOut::reverse() const {
|
||||
return EaseQuartInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 五次缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseQuintIn* EaseQuintIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuintIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuintIn::update(float time) {
|
||||
innerAction_->update(easeInQuint(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintIn::clone() const {
|
||||
return EaseQuintIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintIn::reverse() const {
|
||||
return EaseQuintOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuintOut* EaseQuintOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuintOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuintOut::update(float time) {
|
||||
innerAction_->update(easeOutQuint(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintOut::clone() const {
|
||||
return EaseQuintOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintOut::reverse() const {
|
||||
return EaseQuintIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseQuintInOut* EaseQuintInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseQuintInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseQuintInOut::update(float time) {
|
||||
innerAction_->update(easeInOutQuint(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintInOut::clone() const {
|
||||
return EaseQuintInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseQuintInOut::reverse() const {
|
||||
return EaseQuintInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 圆形缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseCircleIn* EaseCircleIn::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCircleIn();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCircleIn::update(float time) {
|
||||
innerAction_->update(easeInCirc(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleIn::clone() const {
|
||||
return EaseCircleIn::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleIn::reverse() const {
|
||||
return EaseCircleOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseCircleOut* EaseCircleOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCircleOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCircleOut::update(float time) {
|
||||
innerAction_->update(easeOutCirc(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleOut::clone() const {
|
||||
return EaseCircleOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleOut::reverse() const {
|
||||
return EaseCircleIn::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
EaseCircleInOut* EaseCircleInOut::create(ActionInterval* action) {
|
||||
auto* ease = new EaseCircleInOut();
|
||||
ease->initWithAction(action);
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCircleInOut::update(float time) {
|
||||
innerAction_->update(easeInOutCirc(time));
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleInOut::clone() const {
|
||||
return EaseCircleInOut::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* EaseCircleInOut::reverse() const {
|
||||
return EaseCircleInOut::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 自定义缓动
|
||||
// ============================================================================
|
||||
|
||||
EaseCustom* EaseCustom::create(ActionInterval* action, EaseFunction easeFunc) {
|
||||
auto* ease = new EaseCustom();
|
||||
ease->initWithAction(action);
|
||||
ease->easeFunc_ = easeFunc;
|
||||
return ease;
|
||||
}
|
||||
|
||||
void EaseCustom::update(float time) {
|
||||
if (easeFunc_) {
|
||||
innerAction_->update(easeFunc_(time));
|
||||
} else {
|
||||
innerAction_->update(time);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInterval* EaseCustom::clone() const {
|
||||
return EaseCustom::create(innerAction_->clone(), easeFunc_);
|
||||
}
|
||||
|
||||
ActionInterval* EaseCustom::reverse() const {
|
||||
return EaseCustom::create(innerAction_->reverse(), easeFunc_);
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#include "extra2d/action/action_instant.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
ActionInstant::ActionInstant() {
|
||||
duration_ = 0.0f;
|
||||
}
|
||||
|
||||
bool ActionInstant::isDone() const {
|
||||
return done_;
|
||||
}
|
||||
|
||||
void ActionInstant::startWithTarget(Node* target) {
|
||||
FiniteTimeAction::startWithTarget(target);
|
||||
done_ = false;
|
||||
}
|
||||
|
||||
void ActionInstant::step(float dt) {
|
||||
(void)dt;
|
||||
if (state_ != ActionState::Running) {
|
||||
return;
|
||||
}
|
||||
execute();
|
||||
done_ = true;
|
||||
state_ = ActionState::Completed;
|
||||
onComplete();
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
#include "extra2d/action/action_instant_actions.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 回调动作
|
||||
// ============================================================================
|
||||
|
||||
CallFunc* CallFunc::create(const Callback& callback) {
|
||||
auto* action = new CallFunc();
|
||||
action->callback_ = callback;
|
||||
return action;
|
||||
}
|
||||
|
||||
void CallFunc::execute() {
|
||||
if (callback_) {
|
||||
callback_();
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* CallFunc::clone() const {
|
||||
return CallFunc::create(callback_);
|
||||
}
|
||||
|
||||
ActionInstant* CallFunc::reverse() const {
|
||||
return CallFunc::create(callback_);
|
||||
}
|
||||
|
||||
// CallFuncN
|
||||
CallFuncN* CallFuncN::create(const Callback& callback) {
|
||||
auto* action = new CallFuncN();
|
||||
action->callback_ = callback;
|
||||
return action;
|
||||
}
|
||||
|
||||
void CallFuncN::execute() {
|
||||
if (callback_ && target_) {
|
||||
callback_(target_);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* CallFuncN::clone() const {
|
||||
return CallFuncN::create(callback_);
|
||||
}
|
||||
|
||||
ActionInstant* CallFuncN::reverse() const {
|
||||
return CallFuncN::create(callback_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 位置动作
|
||||
// ============================================================================
|
||||
|
||||
Place* Place::create(const Vec2& position) {
|
||||
auto* action = new Place();
|
||||
action->position_ = position;
|
||||
return action;
|
||||
}
|
||||
|
||||
void Place::execute() {
|
||||
if (target_) {
|
||||
target_->setPosition(position_);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* Place::clone() const {
|
||||
return Place::create(position_);
|
||||
}
|
||||
|
||||
ActionInstant* Place::reverse() const {
|
||||
return Place::create(position_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 翻转动作
|
||||
// ============================================================================
|
||||
|
||||
FlipX* FlipX::create(bool flipX) {
|
||||
auto* action = new FlipX();
|
||||
action->flipX_ = flipX;
|
||||
return action;
|
||||
}
|
||||
|
||||
void FlipX::execute() {
|
||||
if (target_) {
|
||||
target_->setFlipX(flipX_);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* FlipX::clone() const {
|
||||
return FlipX::create(flipX_);
|
||||
}
|
||||
|
||||
ActionInstant* FlipX::reverse() const {
|
||||
return FlipX::create(!flipX_);
|
||||
}
|
||||
|
||||
// FlipY
|
||||
FlipY* FlipY::create(bool flipY) {
|
||||
auto* action = new FlipY();
|
||||
action->flipY_ = flipY;
|
||||
return action;
|
||||
}
|
||||
|
||||
void FlipY::execute() {
|
||||
if (target_) {
|
||||
target_->setFlipY(flipY_);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* FlipY::clone() const {
|
||||
return FlipY::create(flipY_);
|
||||
}
|
||||
|
||||
ActionInstant* FlipY::reverse() const {
|
||||
return FlipY::create(!flipY_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 可见性动作
|
||||
// ============================================================================
|
||||
|
||||
Show* Show::create() {
|
||||
return new Show();
|
||||
}
|
||||
|
||||
void Show::execute() {
|
||||
if (target_) {
|
||||
target_->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* Show::clone() const {
|
||||
return Show::create();
|
||||
}
|
||||
|
||||
ActionInstant* Show::reverse() const {
|
||||
return Hide::create();
|
||||
}
|
||||
|
||||
// Hide
|
||||
Hide* Hide::create() {
|
||||
return new Hide();
|
||||
}
|
||||
|
||||
void Hide::execute() {
|
||||
if (target_) {
|
||||
target_->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* Hide::clone() const {
|
||||
return Hide::create();
|
||||
}
|
||||
|
||||
ActionInstant* Hide::reverse() const {
|
||||
return Show::create();
|
||||
}
|
||||
|
||||
// ToggleVisibility
|
||||
ToggleVisibility* ToggleVisibility::create() {
|
||||
return new ToggleVisibility();
|
||||
}
|
||||
|
||||
void ToggleVisibility::execute() {
|
||||
if (target_) {
|
||||
target_->setVisible(!target_->isVisible());
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* ToggleVisibility::clone() const {
|
||||
return ToggleVisibility::create();
|
||||
}
|
||||
|
||||
ActionInstant* ToggleVisibility::reverse() const {
|
||||
return ToggleVisibility::create();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 节点管理动作
|
||||
// ============================================================================
|
||||
|
||||
RemoveSelf* RemoveSelf::create() {
|
||||
return new RemoveSelf();
|
||||
}
|
||||
|
||||
void RemoveSelf::execute() {
|
||||
if (target_) {
|
||||
target_->removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
ActionInstant* RemoveSelf::clone() const {
|
||||
return RemoveSelf::create();
|
||||
}
|
||||
|
||||
ActionInstant* RemoveSelf::reverse() const {
|
||||
return RemoveSelf::create();
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#include "extra2d/action/action_interval.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
ActionInterval::ActionInterval(float duration)
|
||||
: FiniteTimeAction(duration) {
|
||||
}
|
||||
|
||||
bool ActionInterval::isDone() const {
|
||||
return elapsed_ >= duration_;
|
||||
}
|
||||
|
||||
void ActionInterval::startWithTarget(Node* target) {
|
||||
FiniteTimeAction::startWithTarget(target);
|
||||
elapsed_ = 0.0f;
|
||||
firstTick_ = true;
|
||||
onStart();
|
||||
}
|
||||
|
||||
void ActionInterval::stop() {
|
||||
FiniteTimeAction::stop();
|
||||
}
|
||||
|
||||
void ActionInterval::step(float dt) {
|
||||
if (state_ != ActionState::Running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTick_) {
|
||||
firstTick_ = false;
|
||||
elapsed_ = 0.0f;
|
||||
} else {
|
||||
elapsed_ += dt;
|
||||
}
|
||||
|
||||
float progress = 0.0f;
|
||||
if (duration_ > 0.0f) {
|
||||
progress = std::min(1.0f, elapsed_ / duration_);
|
||||
} else {
|
||||
progress = 1.0f;
|
||||
}
|
||||
|
||||
if (easeFunc_) {
|
||||
progress = easeFunc_(progress);
|
||||
}
|
||||
|
||||
onUpdate(progress);
|
||||
|
||||
if (progress >= 1.0f) {
|
||||
state_ = ActionState::Completed;
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,787 +0,0 @@
|
|||
#include "extra2d/action/action_interval_actions.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 移动动作
|
||||
// ============================================================================
|
||||
|
||||
MoveBy* MoveBy::create(float duration, const Vec2& delta) {
|
||||
auto* action = new MoveBy();
|
||||
action->duration_ = duration;
|
||||
action->delta_ = delta;
|
||||
return action;
|
||||
}
|
||||
|
||||
void MoveBy::onStart() {
|
||||
startPosition_ = target_->getPosition();
|
||||
}
|
||||
|
||||
void MoveBy::onUpdate(float progress) {
|
||||
Vec2 newPos = startPosition_ + delta_ * progress;
|
||||
target_->setPosition(newPos);
|
||||
}
|
||||
|
||||
ActionInterval* MoveBy::clone() const {
|
||||
return MoveBy::create(duration_, delta_);
|
||||
}
|
||||
|
||||
ActionInterval* MoveBy::reverse() const {
|
||||
return MoveBy::create(duration_, -delta_);
|
||||
}
|
||||
|
||||
// MoveTo
|
||||
MoveTo* MoveTo::create(float duration, const Vec2& position) {
|
||||
auto* action = new MoveTo();
|
||||
action->duration_ = duration;
|
||||
action->endPosition_ = position;
|
||||
return action;
|
||||
}
|
||||
|
||||
void MoveTo::onStart() {
|
||||
startPosition_ = target_->getPosition();
|
||||
delta_ = endPosition_ - startPosition_;
|
||||
}
|
||||
|
||||
void MoveTo::onUpdate(float progress) {
|
||||
Vec2 newPos = startPosition_ + delta_ * progress;
|
||||
target_->setPosition(newPos);
|
||||
}
|
||||
|
||||
ActionInterval* MoveTo::clone() const {
|
||||
return MoveTo::create(duration_, endPosition_);
|
||||
}
|
||||
|
||||
ActionInterval* MoveTo::reverse() const {
|
||||
return MoveTo::create(duration_, startPosition_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 跳跃动作
|
||||
// ============================================================================
|
||||
|
||||
JumpBy* JumpBy::create(float duration, const Vec2& position, float height, int jumps) {
|
||||
auto* action = new JumpBy();
|
||||
action->duration_ = duration;
|
||||
action->delta_ = position;
|
||||
action->height_ = height;
|
||||
action->jumps_ = jumps;
|
||||
return action;
|
||||
}
|
||||
|
||||
void JumpBy::onStart() {
|
||||
startPosition_ = target_->getPosition();
|
||||
}
|
||||
|
||||
void JumpBy::onUpdate(float progress) {
|
||||
float frac = (progress * jumps_) - static_cast<int>(progress * jumps_);
|
||||
float y = height_ * 4.0f * frac * (1.0f - frac);
|
||||
y += delta_.y * progress;
|
||||
float x = delta_.x * progress;
|
||||
target_->setPosition(startPosition_ + Vec2(x, y));
|
||||
}
|
||||
|
||||
ActionInterval* JumpBy::clone() const {
|
||||
return JumpBy::create(duration_, delta_, height_, jumps_);
|
||||
}
|
||||
|
||||
ActionInterval* JumpBy::reverse() const {
|
||||
return JumpBy::create(duration_, -delta_, height_, jumps_);
|
||||
}
|
||||
|
||||
// JumpTo
|
||||
JumpTo* JumpTo::create(float duration, const Vec2& position, float height, int jumps) {
|
||||
auto* action = new JumpTo();
|
||||
action->duration_ = duration;
|
||||
action->endPosition_ = position;
|
||||
action->height_ = height;
|
||||
action->jumps_ = jumps;
|
||||
return action;
|
||||
}
|
||||
|
||||
void JumpTo::onStart() {
|
||||
JumpBy::onStart();
|
||||
delta_ = endPosition_ - startPosition_;
|
||||
}
|
||||
|
||||
ActionInterval* JumpTo::clone() const {
|
||||
return JumpTo::create(duration_, endPosition_, height_, jumps_);
|
||||
}
|
||||
|
||||
ActionInterval* JumpTo::reverse() const {
|
||||
return JumpTo::create(duration_, startPosition_, height_, jumps_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 贝塞尔曲线动作
|
||||
// ============================================================================
|
||||
|
||||
BezierBy* BezierBy::create(float duration, const BezierConfig& config) {
|
||||
auto* action = new BezierBy();
|
||||
action->duration_ = duration;
|
||||
action->config_ = config;
|
||||
return action;
|
||||
}
|
||||
|
||||
void BezierBy::onStart() {
|
||||
startPosition_ = target_->getPosition();
|
||||
}
|
||||
|
||||
void BezierBy::onUpdate(float progress) {
|
||||
float xa = startPosition_.x;
|
||||
float xb = config_.controlPoint1.x;
|
||||
float xc = config_.controlPoint2.x;
|
||||
float xd = config_.endPosition.x;
|
||||
|
||||
float ya = startPosition_.y;
|
||||
float yb = config_.controlPoint1.y;
|
||||
float yc = config_.controlPoint2.y;
|
||||
float yd = config_.endPosition.y;
|
||||
|
||||
float x = bezierat(xa, xb, xc, xd, progress);
|
||||
float y = bezierat(ya, yb, yc, yd, progress);
|
||||
|
||||
target_->setPosition(Vec2(x, y));
|
||||
}
|
||||
|
||||
ActionInterval* BezierBy::clone() const {
|
||||
return BezierBy::create(duration_, config_);
|
||||
}
|
||||
|
||||
ActionInterval* BezierBy::reverse() const {
|
||||
BezierConfig rev;
|
||||
rev.controlPoint1 = config_.controlPoint2 + config_.endPosition;
|
||||
rev.controlPoint2 = config_.controlPoint1 + config_.endPosition;
|
||||
rev.endPosition = config_.endPosition;
|
||||
return BezierBy::create(duration_, rev);
|
||||
}
|
||||
|
||||
float BezierBy::bezierat(float a, float b, float c, float d, float t) {
|
||||
return (powf(1 - t, 3) * a +
|
||||
3.0f * t * powf(1 - t, 2) * b +
|
||||
3.0f * t * t * (1 - t) * c +
|
||||
t * t * t * d);
|
||||
}
|
||||
|
||||
// BezierTo
|
||||
BezierTo* BezierTo::create(float duration, const BezierConfig& config) {
|
||||
auto* action = new BezierTo();
|
||||
action->duration_ = duration;
|
||||
action->originalConfig_ = config;
|
||||
return action;
|
||||
}
|
||||
|
||||
void BezierTo::onStart() {
|
||||
BezierBy::onStart();
|
||||
config_.controlPoint1 = originalConfig_.controlPoint1 - startPosition_;
|
||||
config_.controlPoint2 = originalConfig_.controlPoint2 - startPosition_;
|
||||
config_.endPosition = originalConfig_.endPosition - startPosition_;
|
||||
}
|
||||
|
||||
ActionInterval* BezierTo::clone() const {
|
||||
return BezierTo::create(duration_, originalConfig_);
|
||||
}
|
||||
|
||||
ActionInterval* BezierTo::reverse() const {
|
||||
BezierConfig rev;
|
||||
rev.controlPoint1 = originalConfig_.controlPoint2;
|
||||
rev.controlPoint2 = originalConfig_.controlPoint1;
|
||||
rev.endPosition = startPosition_;
|
||||
return BezierTo::create(duration_, rev);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 缩放动作
|
||||
// ============================================================================
|
||||
|
||||
ScaleBy* ScaleBy::create(float duration, float scale) {
|
||||
return create(duration, scale, scale);
|
||||
}
|
||||
|
||||
ScaleBy* ScaleBy::create(float duration, float scaleX, float scaleY) {
|
||||
auto* action = new ScaleBy();
|
||||
action->duration_ = duration;
|
||||
action->deltaScale_ = Vec2(scaleX - 1.0f, scaleY - 1.0f);
|
||||
return action;
|
||||
}
|
||||
|
||||
ScaleBy* ScaleBy::create(float duration, const Vec2& scale) {
|
||||
return create(duration, scale.x, scale.y);
|
||||
}
|
||||
|
||||
void ScaleBy::onStart() {
|
||||
startScale_ = target_->getScale();
|
||||
}
|
||||
|
||||
void ScaleBy::onUpdate(float progress) {
|
||||
Vec2 newScale = startScale_ + deltaScale_ * progress;
|
||||
target_->setScale(newScale);
|
||||
}
|
||||
|
||||
ActionInterval* ScaleBy::clone() const {
|
||||
return ScaleBy::create(duration_, Vec2(startScale_.x + deltaScale_.x, startScale_.y + deltaScale_.y));
|
||||
}
|
||||
|
||||
ActionInterval* ScaleBy::reverse() const {
|
||||
return ScaleBy::create(duration_, Vec2(startScale_.x - deltaScale_.x, startScale_.y - deltaScale_.y));
|
||||
}
|
||||
|
||||
// ScaleTo
|
||||
ScaleTo* ScaleTo::create(float duration, float scale) {
|
||||
return create(duration, scale, scale);
|
||||
}
|
||||
|
||||
ScaleTo* ScaleTo::create(float duration, float scaleX, float scaleY) {
|
||||
auto* action = new ScaleTo();
|
||||
action->duration_ = duration;
|
||||
action->endScale_ = Vec2(scaleX, scaleY);
|
||||
return action;
|
||||
}
|
||||
|
||||
ScaleTo* ScaleTo::create(float duration, const Vec2& scale) {
|
||||
return create(duration, scale.x, scale.y);
|
||||
}
|
||||
|
||||
void ScaleTo::onStart() {
|
||||
startScale_ = target_->getScale();
|
||||
delta_ = endScale_ - startScale_;
|
||||
}
|
||||
|
||||
void ScaleTo::onUpdate(float progress) {
|
||||
Vec2 newScale = startScale_ + delta_ * progress;
|
||||
target_->setScale(newScale);
|
||||
}
|
||||
|
||||
ActionInterval* ScaleTo::clone() const {
|
||||
return ScaleTo::create(duration_, endScale_);
|
||||
}
|
||||
|
||||
ActionInterval* ScaleTo::reverse() const {
|
||||
return ScaleTo::create(duration_, startScale_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 旋转动作
|
||||
// ============================================================================
|
||||
|
||||
RotateBy* RotateBy::create(float duration, float deltaAngle) {
|
||||
auto* action = new RotateBy();
|
||||
action->duration_ = duration;
|
||||
action->deltaAngle_ = deltaAngle;
|
||||
return action;
|
||||
}
|
||||
|
||||
void RotateBy::onStart() {
|
||||
startAngle_ = target_->getRotation();
|
||||
}
|
||||
|
||||
void RotateBy::onUpdate(float progress) {
|
||||
float newAngle = startAngle_ + deltaAngle_ * progress;
|
||||
target_->setRotation(newAngle);
|
||||
}
|
||||
|
||||
ActionInterval* RotateBy::clone() const {
|
||||
return RotateBy::create(duration_, deltaAngle_);
|
||||
}
|
||||
|
||||
ActionInterval* RotateBy::reverse() const {
|
||||
return RotateBy::create(duration_, -deltaAngle_);
|
||||
}
|
||||
|
||||
// RotateTo
|
||||
RotateTo* RotateTo::create(float duration, float angle) {
|
||||
auto* action = new RotateTo();
|
||||
action->duration_ = duration;
|
||||
action->endAngle_ = angle;
|
||||
return action;
|
||||
}
|
||||
|
||||
void RotateTo::onStart() {
|
||||
startAngle_ = target_->getRotation();
|
||||
deltaAngle_ = endAngle_ - startAngle_;
|
||||
|
||||
if (deltaAngle_ > 180.0f) deltaAngle_ -= 360.0f;
|
||||
if (deltaAngle_ < -180.0f) deltaAngle_ += 360.0f;
|
||||
}
|
||||
|
||||
void RotateTo::onUpdate(float progress) {
|
||||
float newAngle = startAngle_ + deltaAngle_ * progress;
|
||||
target_->setRotation(newAngle);
|
||||
}
|
||||
|
||||
ActionInterval* RotateTo::clone() const {
|
||||
return RotateTo::create(duration_, endAngle_);
|
||||
}
|
||||
|
||||
ActionInterval* RotateTo::reverse() const {
|
||||
return RotateTo::create(duration_, startAngle_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 淡入淡出动作
|
||||
// ============================================================================
|
||||
|
||||
FadeIn* FadeIn::create(float duration) {
|
||||
auto* action = new FadeIn();
|
||||
action->duration_ = duration;
|
||||
return action;
|
||||
}
|
||||
|
||||
void FadeIn::onStart() {
|
||||
startOpacity_ = target_->getOpacity();
|
||||
target_->setOpacity(0.0f);
|
||||
}
|
||||
|
||||
void FadeIn::onUpdate(float progress) {
|
||||
target_->setOpacity(progress);
|
||||
}
|
||||
|
||||
ActionInterval* FadeIn::clone() const {
|
||||
return FadeIn::create(duration_);
|
||||
}
|
||||
|
||||
ActionInterval* FadeIn::reverse() const {
|
||||
return FadeOut::create(duration_);
|
||||
}
|
||||
|
||||
// FadeOut
|
||||
FadeOut* FadeOut::create(float duration) {
|
||||
auto* action = new FadeOut();
|
||||
action->duration_ = duration;
|
||||
return action;
|
||||
}
|
||||
|
||||
void FadeOut::onStart() {
|
||||
startOpacity_ = target_->getOpacity();
|
||||
target_->setOpacity(1.0f);
|
||||
}
|
||||
|
||||
void FadeOut::onUpdate(float progress) {
|
||||
target_->setOpacity(1.0f - progress);
|
||||
}
|
||||
|
||||
ActionInterval* FadeOut::clone() const {
|
||||
return FadeOut::create(duration_);
|
||||
}
|
||||
|
||||
ActionInterval* FadeOut::reverse() const {
|
||||
return FadeIn::create(duration_);
|
||||
}
|
||||
|
||||
// FadeTo
|
||||
FadeTo* FadeTo::create(float duration, float opacity) {
|
||||
auto* action = new FadeTo();
|
||||
action->duration_ = duration;
|
||||
action->endOpacity_ = opacity;
|
||||
return action;
|
||||
}
|
||||
|
||||
void FadeTo::onStart() {
|
||||
startOpacity_ = target_->getOpacity();
|
||||
deltaOpacity_ = endOpacity_ - startOpacity_;
|
||||
}
|
||||
|
||||
void FadeTo::onUpdate(float progress) {
|
||||
target_->setOpacity(startOpacity_ + deltaOpacity_ * progress);
|
||||
}
|
||||
|
||||
ActionInterval* FadeTo::clone() const {
|
||||
return FadeTo::create(duration_, endOpacity_);
|
||||
}
|
||||
|
||||
ActionInterval* FadeTo::reverse() const {
|
||||
return FadeTo::create(duration_, startOpacity_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 闪烁动作
|
||||
// ============================================================================
|
||||
|
||||
Blink* Blink::create(float duration, int times) {
|
||||
auto* action = new Blink();
|
||||
action->duration_ = duration;
|
||||
action->times_ = times;
|
||||
return action;
|
||||
}
|
||||
|
||||
void Blink::onStart() {
|
||||
originalVisible_ = target_->isVisible();
|
||||
currentTimes_ = 0;
|
||||
}
|
||||
|
||||
void Blink::onUpdate(float progress) {
|
||||
float slice = 1.0f / times_;
|
||||
float m = fmodf(progress, slice);
|
||||
target_->setVisible(m > slice / 2.0f);
|
||||
}
|
||||
|
||||
ActionInterval* Blink::clone() const {
|
||||
return Blink::create(duration_, times_);
|
||||
}
|
||||
|
||||
ActionInterval* Blink::reverse() const {
|
||||
return Blink::create(duration_, times_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 色调动作
|
||||
// ============================================================================
|
||||
|
||||
TintTo* TintTo::create(float duration, uint8_t red, uint8_t green, uint8_t blue) {
|
||||
auto* action = new TintTo();
|
||||
action->duration_ = duration;
|
||||
action->endColor_ = Color3B(red, green, blue);
|
||||
return action;
|
||||
}
|
||||
|
||||
void TintTo::onStart() {
|
||||
startColor_ = target_->getColor();
|
||||
deltaColor_ = Color3B(
|
||||
static_cast<int16_t>(endColor_.r) - static_cast<int16_t>(startColor_.r),
|
||||
static_cast<int16_t>(endColor_.g) - static_cast<int16_t>(startColor_.g),
|
||||
static_cast<int16_t>(endColor_.b) - static_cast<int16_t>(startColor_.b)
|
||||
);
|
||||
}
|
||||
|
||||
void TintTo::onUpdate(float progress) {
|
||||
Color3B newColor(
|
||||
static_cast<uint8_t>(startColor_.r + deltaColor_.r * progress),
|
||||
static_cast<uint8_t>(startColor_.g + deltaColor_.g * progress),
|
||||
static_cast<uint8_t>(startColor_.b + deltaColor_.b * progress)
|
||||
);
|
||||
target_->setColor(newColor);
|
||||
}
|
||||
|
||||
ActionInterval* TintTo::clone() const {
|
||||
return TintTo::create(duration_, endColor_.r, endColor_.g, endColor_.b);
|
||||
}
|
||||
|
||||
ActionInterval* TintTo::reverse() const {
|
||||
return TintTo::create(duration_, startColor_.r, startColor_.g, startColor_.b);
|
||||
}
|
||||
|
||||
// TintBy
|
||||
TintBy* TintBy::create(float duration, int16_t deltaRed, int16_t deltaGreen, int16_t deltaBlue) {
|
||||
auto* action = new TintBy();
|
||||
action->duration_ = duration;
|
||||
action->deltaR_ = deltaRed;
|
||||
action->deltaG_ = deltaGreen;
|
||||
action->deltaB_ = deltaBlue;
|
||||
return action;
|
||||
}
|
||||
|
||||
void TintBy::onStart() {
|
||||
startColor_ = target_->getColor();
|
||||
}
|
||||
|
||||
void TintBy::onUpdate(float progress) {
|
||||
Color3B newColor(
|
||||
static_cast<uint8_t>(startColor_.r + deltaR_ * progress),
|
||||
static_cast<uint8_t>(startColor_.g + deltaG_ * progress),
|
||||
static_cast<uint8_t>(startColor_.b + deltaB_ * progress)
|
||||
);
|
||||
target_->setColor(newColor);
|
||||
}
|
||||
|
||||
ActionInterval* TintBy::clone() const {
|
||||
return TintBy::create(duration_, deltaR_, deltaG_, deltaB_);
|
||||
}
|
||||
|
||||
ActionInterval* TintBy::reverse() const {
|
||||
return TintBy::create(duration_, -deltaR_, -deltaG_, -deltaB_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 组合动作
|
||||
// ============================================================================
|
||||
|
||||
Sequence* Sequence::create(ActionInterval* action1, ...) {
|
||||
std::vector<ActionInterval*> actions;
|
||||
actions.push_back(action1);
|
||||
|
||||
va_list args;
|
||||
va_start(args, action1);
|
||||
ActionInterval* action = nullptr;
|
||||
while ((action = va_arg(args, ActionInterval*)) != nullptr) {
|
||||
actions.push_back(action);
|
||||
}
|
||||
va_end(args);
|
||||
|
||||
return create(actions);
|
||||
}
|
||||
|
||||
Sequence* Sequence::create(const std::vector<ActionInterval*>& actions) {
|
||||
auto* seq = new Sequence();
|
||||
seq->duration_ = 0.0f;
|
||||
|
||||
for (auto* action : actions) {
|
||||
if (action) {
|
||||
seq->actions_.push_back(action);
|
||||
seq->duration_ += action->getDuration();
|
||||
}
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
Sequence::~Sequence() {
|
||||
for (auto* action : actions_) {
|
||||
delete action;
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::onStart() {
|
||||
currentIndex_ = 0;
|
||||
split_ = 0.0f;
|
||||
last_ = -1.0f;
|
||||
|
||||
if (!actions_.empty()) {
|
||||
actions_[0]->startWithTarget(target_);
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::onUpdate(float progress) {
|
||||
float newTime = progress * duration_;
|
||||
|
||||
if (newTime < last_) {
|
||||
for (auto* action : actions_) {
|
||||
action->stop();
|
||||
}
|
||||
}
|
||||
last_ = newTime;
|
||||
|
||||
float foundSplit = 0.0f;
|
||||
size_t found = 0;
|
||||
|
||||
for (size_t i = 0; i < actions_.size(); ++i) {
|
||||
foundSplit += actions_[i]->getDuration();
|
||||
if (foundSplit > newTime) {
|
||||
found = i;
|
||||
break;
|
||||
} else if (foundSplit == newTime) {
|
||||
found = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found != currentIndex_) {
|
||||
if (currentIndex_ < actions_.size()) {
|
||||
actions_[currentIndex_]->update(1.0f);
|
||||
}
|
||||
if (found < actions_.size()) {
|
||||
actions_[found]->startWithTarget(target_);
|
||||
}
|
||||
currentIndex_ = found;
|
||||
}
|
||||
|
||||
if (currentIndex_ < actions_.size()) {
|
||||
float localTime = newTime - (foundSplit - actions_[currentIndex_]->getDuration());
|
||||
float localProgress = actions_[currentIndex_]->getDuration() > 0.0f
|
||||
? localTime / actions_[currentIndex_]->getDuration() : 1.0f;
|
||||
actions_[currentIndex_]->update(localProgress);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInterval* Sequence::clone() const {
|
||||
std::vector<ActionInterval*> cloned;
|
||||
for (auto* action : actions_) {
|
||||
cloned.push_back(action->clone());
|
||||
}
|
||||
return Sequence::create(cloned);
|
||||
}
|
||||
|
||||
ActionInterval* Sequence::reverse() const {
|
||||
std::vector<ActionInterval*> rev;
|
||||
for (auto it = actions_.rbegin(); it != actions_.rend(); ++it) {
|
||||
rev.push_back((*it)->reverse());
|
||||
}
|
||||
return Sequence::create(rev);
|
||||
}
|
||||
|
||||
// Spawn
|
||||
Spawn* Spawn::create(ActionInterval* action1, ...) {
|
||||
std::vector<ActionInterval*> actions;
|
||||
actions.push_back(action1);
|
||||
|
||||
va_list args;
|
||||
va_start(args, action1);
|
||||
ActionInterval* action = nullptr;
|
||||
while ((action = va_arg(args, ActionInterval*)) != nullptr) {
|
||||
actions.push_back(action);
|
||||
}
|
||||
va_end(args);
|
||||
|
||||
return create(actions);
|
||||
}
|
||||
|
||||
Spawn* Spawn::create(const std::vector<ActionInterval*>& actions) {
|
||||
auto* spawn = new Spawn();
|
||||
spawn->duration_ = 0.0f;
|
||||
|
||||
for (auto* action : actions) {
|
||||
if (action) {
|
||||
spawn->actions_.push_back(action);
|
||||
spawn->duration_ = std::max(spawn->duration_, action->getDuration());
|
||||
}
|
||||
}
|
||||
return spawn;
|
||||
}
|
||||
|
||||
Spawn::~Spawn() {
|
||||
for (auto* action : actions_) {
|
||||
delete action;
|
||||
}
|
||||
}
|
||||
|
||||
void Spawn::onStart() {
|
||||
for (auto* action : actions_) {
|
||||
action->startWithTarget(target_);
|
||||
}
|
||||
}
|
||||
|
||||
void Spawn::onUpdate(float progress) {
|
||||
for (auto* action : actions_) {
|
||||
float localProgress = action->getDuration() > 0.0f
|
||||
? std::min(1.0f, (progress * duration_) / action->getDuration())
|
||||
: 1.0f;
|
||||
action->update(localProgress);
|
||||
}
|
||||
}
|
||||
|
||||
ActionInterval* Spawn::clone() const {
|
||||
std::vector<ActionInterval*> cloned;
|
||||
for (auto* action : actions_) {
|
||||
cloned.push_back(action->clone());
|
||||
}
|
||||
return Spawn::create(cloned);
|
||||
}
|
||||
|
||||
ActionInterval* Spawn::reverse() const {
|
||||
std::vector<ActionInterval*> rev;
|
||||
for (auto* action : actions_) {
|
||||
rev.push_back(action->reverse());
|
||||
}
|
||||
return Spawn::create(rev);
|
||||
}
|
||||
|
||||
// Repeat
|
||||
Repeat* Repeat::create(ActionInterval* action, int times) {
|
||||
auto* repeat = new Repeat();
|
||||
repeat->innerAction_ = action;
|
||||
repeat->times_ = times;
|
||||
repeat->duration_ = action->getDuration() * times;
|
||||
return repeat;
|
||||
}
|
||||
|
||||
ActionInterval* Repeat::clone() const {
|
||||
return Repeat::create(innerAction_->clone(), times_);
|
||||
}
|
||||
|
||||
ActionInterval* Repeat::reverse() const {
|
||||
return Repeat::create(innerAction_->reverse(), times_);
|
||||
}
|
||||
|
||||
bool Repeat::isDone() const {
|
||||
return currentTimes_ >= times_;
|
||||
}
|
||||
|
||||
void Repeat::onStart() {
|
||||
currentTimes_ = 0;
|
||||
innerAction_->startWithTarget(target_);
|
||||
}
|
||||
|
||||
void Repeat::onUpdate(float progress) {
|
||||
float t = progress * times_;
|
||||
int current = static_cast<int>(t);
|
||||
|
||||
if (current > currentTimes_) {
|
||||
innerAction_->update(1.0f);
|
||||
currentTimes_++;
|
||||
if (currentTimes_ < times_) {
|
||||
innerAction_->startWithTarget(target_);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTimes_ < times_) {
|
||||
innerAction_->update(t - current);
|
||||
}
|
||||
}
|
||||
|
||||
// RepeatForever
|
||||
RepeatForever* RepeatForever::create(ActionInterval* action) {
|
||||
auto* repeat = new RepeatForever();
|
||||
repeat->innerAction_ = action;
|
||||
repeat->duration_ = action->getDuration();
|
||||
return repeat;
|
||||
}
|
||||
|
||||
ActionInterval* RepeatForever::clone() const {
|
||||
return RepeatForever::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* RepeatForever::reverse() const {
|
||||
return RepeatForever::create(innerAction_->reverse());
|
||||
}
|
||||
|
||||
bool RepeatForever::isDone() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void RepeatForever::onStart() {
|
||||
innerAction_->startWithTarget(target_);
|
||||
}
|
||||
|
||||
void RepeatForever::onUpdate(float progress) {
|
||||
innerAction_->update(progress);
|
||||
if (innerAction_->isDone()) {
|
||||
innerAction_->startWithTarget(target_);
|
||||
elapsed_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// DelayTime
|
||||
DelayTime* DelayTime::create(float duration) {
|
||||
auto* delay = new DelayTime();
|
||||
delay->duration_ = duration;
|
||||
return delay;
|
||||
}
|
||||
|
||||
ActionInterval* DelayTime::clone() const {
|
||||
return DelayTime::create(duration_);
|
||||
}
|
||||
|
||||
ActionInterval* DelayTime::reverse() const {
|
||||
return DelayTime::create(duration_);
|
||||
}
|
||||
|
||||
// ReverseTime
|
||||
ReverseTime* ReverseTime::create(ActionInterval* action) {
|
||||
auto* rev = new ReverseTime();
|
||||
rev->innerAction_ = action;
|
||||
rev->duration_ = action->getDuration();
|
||||
return rev;
|
||||
}
|
||||
|
||||
ReverseTime::~ReverseTime() {
|
||||
delete innerAction_;
|
||||
}
|
||||
|
||||
ActionInterval* ReverseTime::clone() const {
|
||||
return ReverseTime::create(innerAction_->clone());
|
||||
}
|
||||
|
||||
ActionInterval* ReverseTime::reverse() const {
|
||||
return innerAction_->clone();
|
||||
}
|
||||
|
||||
void ReverseTime::onStart() {
|
||||
innerAction_->startWithTarget(target_);
|
||||
}
|
||||
|
||||
void ReverseTime::onUpdate(float progress) {
|
||||
innerAction_->update(1.0f - progress);
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
#include "extra2d/action/action_manager.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
#include "extra2d/utils/logger.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
ActionManager* ActionManager::instance_ = nullptr;
|
||||
|
||||
ActionManager::ActionManager() {}
|
||||
|
||||
ActionManager::~ActionManager() {
|
||||
removeAllActions();
|
||||
}
|
||||
|
||||
ActionManager* ActionManager::getInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new ActionManager();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void ActionManager::destroyInstance() {
|
||||
if (instance_) {
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::addAction(Action* action, Node* target, bool paused) {
|
||||
if (!action || !target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
ActionElement element;
|
||||
element.target = target;
|
||||
element.paused = paused;
|
||||
targets_[target] = element;
|
||||
it = targets_.find(target);
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
element.actions.push_back(action);
|
||||
action->startWithTarget(target);
|
||||
}
|
||||
|
||||
void ActionManager::removeAction(Action* action) {
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
Node* target = action->getOriginalTarget();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
for (size_t i = 0; i < element.actions.size(); ++i) {
|
||||
if (element.actions[i] == action) {
|
||||
removeActionAt(i, element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::removeActionByTag(int tag, Node* target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
for (size_t i = 0; i < element.actions.size(); ++i) {
|
||||
if (element.actions[i]->getTag() == tag) {
|
||||
removeActionAt(i, element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::removeActionsByFlags(unsigned int flags, Node* target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
for (int i = static_cast<int>(element.actions.size()) - 1; i >= 0; --i) {
|
||||
if (element.actions[i]->getFlags() & flags) {
|
||||
removeActionAt(i, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::removeAllActionsFromTarget(Node* target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
for (auto* action : element.actions) {
|
||||
action->stop();
|
||||
deleteAction(action);
|
||||
}
|
||||
element.actions.clear();
|
||||
targets_.erase(it);
|
||||
}
|
||||
|
||||
void ActionManager::removeAllActions() {
|
||||
for (auto& pair : targets_) {
|
||||
auto& element = pair.second;
|
||||
for (auto* action : element.actions) {
|
||||
action->stop();
|
||||
deleteAction(action);
|
||||
}
|
||||
}
|
||||
targets_.clear();
|
||||
}
|
||||
|
||||
Action* ActionManager::getActionByTag(int tag, Node* target) {
|
||||
if (!target) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& element = it->second;
|
||||
for (auto* action : element.actions) {
|
||||
if (action->getTag() == tag) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t ActionManager::getActionCount(Node* target) const {
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return 0;
|
||||
}
|
||||
return it->second.actions.size();
|
||||
}
|
||||
|
||||
void ActionManager::pauseTarget(Node* target) {
|
||||
auto it = targets_.find(target);
|
||||
if (it != targets_.end()) {
|
||||
it->second.paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::resumeTarget(Node* target) {
|
||||
auto it = targets_.find(target);
|
||||
if (it != targets_.end()) {
|
||||
it->second.paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ActionManager::isPaused(Node* target) const {
|
||||
auto it = targets_.find(target);
|
||||
if (it == targets_.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second.paused;
|
||||
}
|
||||
|
||||
void ActionManager::update(float dt) {
|
||||
for (auto it = targets_.begin(); it != targets_.end(); ) {
|
||||
auto& element = it->second;
|
||||
Node* target = element.target;
|
||||
|
||||
if (!element.paused && target) {
|
||||
element.actionIndex = 0;
|
||||
while (static_cast<size_t>(element.actionIndex) < element.actions.size()) {
|
||||
element.currentAction = element.actions[element.actionIndex];
|
||||
element.currentActionSalvaged = false;
|
||||
|
||||
if (element.currentAction->getState() != ActionState::Paused) {
|
||||
element.currentAction->step(dt);
|
||||
}
|
||||
|
||||
if (element.currentActionSalvaged) {
|
||||
deleteAction(element.currentAction);
|
||||
} else if (element.currentAction->isDone()) {
|
||||
element.currentAction->stop();
|
||||
deleteAction(element.currentAction);
|
||||
element.actions.erase(element.actions.begin() + element.actionIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
element.actionIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.actions.empty()) {
|
||||
it = targets_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActionManager::removeActionAt(size_t index, ActionElement& element) {
|
||||
if (index >= element.actions.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Action* action = element.actions[index];
|
||||
if (action == element.currentAction) {
|
||||
element.currentActionSalvaged = true;
|
||||
}
|
||||
action->stop();
|
||||
deleteAction(action);
|
||||
element.actions.erase(element.actions.begin() + index);
|
||||
}
|
||||
|
||||
void ActionManager::deleteAction(Action* action) {
|
||||
if (action) {
|
||||
delete action;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
#include "extra2d/action/action_special.h"
|
||||
#include "extra2d/scene/node.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// Speed
|
||||
// ============================================================================
|
||||
|
||||
Speed* Speed::create(ActionInterval* action, float speed) {
|
||||
auto* speedAction = new Speed();
|
||||
speedAction->innerAction_ = action;
|
||||
speedAction->speed_ = speed;
|
||||
return speedAction;
|
||||
}
|
||||
|
||||
Speed::~Speed() {
|
||||
delete innerAction_;
|
||||
}
|
||||
|
||||
void Speed::startWithTarget(Node* target) {
|
||||
Action::startWithTarget(target);
|
||||
innerAction_->startWithTarget(target);
|
||||
}
|
||||
|
||||
void Speed::stop() {
|
||||
innerAction_->stop();
|
||||
Action::stop();
|
||||
}
|
||||
|
||||
void Speed::step(float dt) {
|
||||
if (state_ != ActionState::Running) {
|
||||
return;
|
||||
}
|
||||
innerAction_->step(dt * speed_);
|
||||
if (innerAction_->isDone()) {
|
||||
state_ = ActionState::Completed;
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
bool Speed::isDone() const {
|
||||
return innerAction_->isDone();
|
||||
}
|
||||
|
||||
Action* Speed::clone() const {
|
||||
return Speed::create(innerAction_->clone(), speed_);
|
||||
}
|
||||
|
||||
Action* Speed::reverse() const {
|
||||
return Speed::create(innerAction_->reverse(), speed_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Follow
|
||||
// ============================================================================
|
||||
|
||||
Follow* Follow::create(Node* followedNode) {
|
||||
return create(followedNode, Rect::Zero());
|
||||
}
|
||||
|
||||
Follow* Follow::create(Node* followedNode, const Rect& boundary) {
|
||||
auto* follow = new Follow();
|
||||
follow->followedNode_ = followedNode;
|
||||
follow->boundary_ = boundary;
|
||||
follow->boundarySet_ = (boundary != Rect::Zero());
|
||||
return follow;
|
||||
}
|
||||
|
||||
Follow::~Follow() {
|
||||
followedNode_ = nullptr;
|
||||
}
|
||||
|
||||
void Follow::startWithTarget(Node* target) {
|
||||
Action::startWithTarget(target);
|
||||
if (target && followedNode_) {
|
||||
halfScreenSize_ = Vec2(0, 0);
|
||||
fullScreenSize_ = Vec2(0, 0);
|
||||
|
||||
if (boundarySet_) {
|
||||
leftBoundary_ = Vec2(boundary_.origin.x, 0);
|
||||
rightBoundary_ = Vec2(boundary_.origin.x + boundary_.size.width, 0);
|
||||
topBoundary_ = Vec2(0, boundary_.origin.y);
|
||||
bottomBoundary_ = Vec2(0, boundary_.origin.y + boundary_.size.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Follow::stop() {
|
||||
followedNode_ = nullptr;
|
||||
Action::stop();
|
||||
}
|
||||
|
||||
void Follow::step(float dt) {
|
||||
(void)dt;
|
||||
if (state_ != ActionState::Running || !followedNode_ || !target_) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vec2 pos = followedNode_->getPosition();
|
||||
|
||||
if (boundarySet_) {
|
||||
pos.x = std::clamp(pos.x, leftBoundary_.x, rightBoundary_.x);
|
||||
pos.y = std::clamp(pos.y, bottomBoundary_.y, topBoundary_.y);
|
||||
}
|
||||
|
||||
target_->setPosition(pos);
|
||||
}
|
||||
|
||||
bool Follow::isDone() const {
|
||||
return followedNode_ == nullptr || !followedNode_->isRunning();
|
||||
}
|
||||
|
||||
Action* Follow::clone() const {
|
||||
return Follow::create(followedNode_, boundary_);
|
||||
}
|
||||
|
||||
Action* Follow::reverse() const {
|
||||
return Follow::create(followedNode_, boundary_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TargetedAction
|
||||
// ============================================================================
|
||||
|
||||
TargetedAction* TargetedAction::create(Node* target, FiniteTimeAction* action) {
|
||||
auto* targeted = new TargetedAction();
|
||||
targeted->targetNode_ = target;
|
||||
targeted->innerAction_ = action;
|
||||
return targeted;
|
||||
}
|
||||
|
||||
TargetedAction::~TargetedAction() {
|
||||
delete innerAction_;
|
||||
}
|
||||
|
||||
void TargetedAction::startWithTarget(Node* target) {
|
||||
Action::startWithTarget(target);
|
||||
if (targetNode_) {
|
||||
innerAction_->startWithTarget(targetNode_);
|
||||
}
|
||||
}
|
||||
|
||||
void TargetedAction::stop() {
|
||||
innerAction_->stop();
|
||||
Action::stop();
|
||||
}
|
||||
|
||||
void TargetedAction::step(float dt) {
|
||||
if (state_ != ActionState::Running) {
|
||||
return;
|
||||
}
|
||||
innerAction_->step(dt);
|
||||
if (innerAction_->isDone()) {
|
||||
state_ = ActionState::Completed;
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
bool TargetedAction::isDone() const {
|
||||
return innerAction_->isDone();
|
||||
}
|
||||
|
||||
Action* TargetedAction::clone() const {
|
||||
return TargetedAction::create(targetNode_, innerAction_->clone());
|
||||
}
|
||||
|
||||
Action* TargetedAction::reverse() const {
|
||||
return TargetedAction::create(targetNode_, innerAction_->reverse());
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
#include "extra2d/action/ease.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846f
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// 线性缓动
|
||||
// ============================================================================
|
||||
|
||||
float easeLinear(float t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 二次缓动 (Quad)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuad(float t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
float easeOutQuad(float t) {
|
||||
return 1.0f - (1.0f - t) * (1.0f - t);
|
||||
}
|
||||
|
||||
float easeInOutQuad(float t) {
|
||||
return t < 0.5f ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 三次缓动 (Cubic)
|
||||
// ============================================================================
|
||||
|
||||
float easeInCubic(float t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
float easeOutCubic(float t) {
|
||||
return 1.0f - std::pow(1.0f - t, 3.0f);
|
||||
}
|
||||
|
||||
float easeInOutCubic(float t) {
|
||||
return t < 0.5f ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 四次缓动 (Quart)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuart(float t) {
|
||||
return t * t * t * t;
|
||||
}
|
||||
|
||||
float easeOutQuart(float t) {
|
||||
return 1.0f - std::pow(1.0f - t, 4.0f);
|
||||
}
|
||||
|
||||
float easeInOutQuart(float t) {
|
||||
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 4.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 五次缓动 (Quint)
|
||||
// ============================================================================
|
||||
|
||||
float easeInQuint(float t) {
|
||||
return t * t * t * t * t;
|
||||
}
|
||||
|
||||
float easeOutQuint(float t) {
|
||||
return 1.0f - std::pow(1.0f - t, 5.0f);
|
||||
}
|
||||
|
||||
float easeInOutQuint(float t) {
|
||||
return t < 0.5f ? 16.0f * t * t * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 5.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 正弦缓动 (Sine)
|
||||
// ============================================================================
|
||||
|
||||
float easeInSine(float t) {
|
||||
return 1.0f - std::cos((t * M_PI) / 2.0f);
|
||||
}
|
||||
|
||||
float easeOutSine(float t) {
|
||||
return std::sin((t * M_PI) / 2.0f);
|
||||
}
|
||||
|
||||
float easeInOutSine(float t) {
|
||||
return -(std::cos(M_PI * t) - 1.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 指数缓动 (Exponential)
|
||||
// ============================================================================
|
||||
|
||||
float easeInExpo(float t) {
|
||||
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
|
||||
}
|
||||
|
||||
float easeOutExpo(float t) {
|
||||
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
|
||||
}
|
||||
|
||||
float easeInOutExpo(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
return t < 0.5f
|
||||
? std::pow(2.0f, 20.0f * t - 10.0f) / 2.0f
|
||||
: (2.0f - std::pow(2.0f, -20.0f * t + 10.0f)) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 圆形缓动 (Circular)
|
||||
// ============================================================================
|
||||
|
||||
float easeInCirc(float t) {
|
||||
return 1.0f - std::sqrt(1.0f - std::pow(t, 2.0f));
|
||||
}
|
||||
|
||||
float easeOutCirc(float t) {
|
||||
return std::sqrt(1.0f - std::pow(t - 1.0f, 2.0f));
|
||||
}
|
||||
|
||||
float easeInOutCirc(float t) {
|
||||
return t < 0.5f
|
||||
? (1.0f - std::sqrt(1.0f - std::pow(2.0f * t, 2.0f))) / 2.0f
|
||||
: (std::sqrt(1.0f - std::pow(-2.0f * t + 2.0f, 2.0f)) + 1.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 回震缓动 (Back)
|
||||
// ============================================================================
|
||||
|
||||
float easeInBack(float t) {
|
||||
const float c1 = 1.70158f;
|
||||
const float c3 = c1 + 1.0f;
|
||||
return c3 * t * t * t - c1 * t * t;
|
||||
}
|
||||
|
||||
float easeOutBack(float t) {
|
||||
const float c1 = 1.70158f;
|
||||
const float c3 = c1 + 1.0f;
|
||||
return 1.0f + c3 * std::pow(t - 1.0f, 3.0f) + c1 * std::pow(t - 1.0f, 2.0f);
|
||||
}
|
||||
|
||||
float easeInOutBack(float t) {
|
||||
const float c1 = 1.70158f;
|
||||
const float c2 = c1 * 1.525f;
|
||||
return t < 0.5f
|
||||
? (std::pow(2.0f * t, 2.0f) * ((c2 + 1.0f) * 2.0f * t - c2)) / 2.0f
|
||||
: (std::pow(2.0f * t - 2.0f, 2.0f) * ((c2 + 1.0f) * (t * 2.0f - 2.0f) + c2) + 2.0f) / 2.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 弹性缓动 (Elastic)
|
||||
// ============================================================================
|
||||
|
||||
float easeInElastic(float t) {
|
||||
const float c4 = (2.0f * M_PI) / 3.0f;
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
return -std::pow(2.0f, 10.0f * t - 10.0f) * std::sin((t * 10.0f - 10.75f) * c4);
|
||||
}
|
||||
|
||||
float easeOutElastic(float t) {
|
||||
const float c4 = (2.0f * M_PI) / 3.0f;
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
return std::pow(2.0f, -10.0f * t) * std::sin((t * 10.0f - 0.75f) * c4) + 1.0f;
|
||||
}
|
||||
|
||||
float easeInOutElastic(float t) {
|
||||
const float c5 = (2.0f * M_PI) / 4.5f;
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
return t < 0.5f
|
||||
? -(std::pow(2.0f, 20.0f * t - 10.0f) * std::sin((20.0f * t - 11.125f) * c5)) / 2.0f
|
||||
: (std::pow(2.0f, -20.0f * t + 10.0f) * std::sin((20.0f * t - 11.125f) * c5)) / 2.0f + 1.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 弹跳缓动 (Bounce)
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
float easeOutBounceInternal(float t) {
|
||||
const float n1 = 7.5625f;
|
||||
const float d1 = 2.75f;
|
||||
|
||||
if (t < 1.0f / d1) {
|
||||
return n1 * t * t;
|
||||
} else if (t < 2.0f / d1) {
|
||||
t -= 1.5f / d1;
|
||||
return n1 * t * t + 0.75f;
|
||||
} else if (t < 2.5f / d1) {
|
||||
t -= 2.25f / d1;
|
||||
return n1 * t * t + 0.9375f;
|
||||
} else {
|
||||
t -= 2.625f / d1;
|
||||
return n1 * t * t + 0.984375f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float easeInBounce(float t) {
|
||||
return 1.0f - easeOutBounceInternal(1.0f - t);
|
||||
}
|
||||
|
||||
float easeOutBounce(float t) {
|
||||
return easeOutBounceInternal(t);
|
||||
}
|
||||
|
||||
float easeInOutBounce(float t) {
|
||||
return t < 0.5f
|
||||
? (1.0f - easeOutBounceInternal(1.0f - 2.0f * t)) / 2.0f
|
||||
: (1.0f + easeOutBounceInternal(2.0f * t - 1.0f)) / 2.0f;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#include "extra2d/action/finite_time_action.h"
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
FiniteTimeAction::FiniteTimeAction(float duration)
|
||||
: duration_(duration) {
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#include <extra2d/animation/als_parser.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
AlsParseResult AlsParser::parse(const std::string &filePath) {
|
||||
AlsParseResult result;
|
||||
|
||||
// TODO: 实现实际的 ALS 文件格式解析
|
||||
// 当前为框架实现,需要根据实际 ALS 文件格式补充解析逻辑
|
||||
//
|
||||
// 解析流程:
|
||||
// 1. 读取 ALS 文件内容
|
||||
// 2. 解析子动画列表:
|
||||
// - 子动画 ANI 文件路径
|
||||
// - 层级 zOrder
|
||||
// - 偏移量
|
||||
// 3. 组装 AlsLayerInfo 数组
|
||||
|
||||
result.success = true;
|
||||
result.errorMessage =
|
||||
"ALS parser framework ready - actual format parsing to be implemented";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AlsParseResult AlsParser::parseFromMemory(const std::string &content,
|
||||
const std::string &basePath) {
|
||||
AlsParseResult result;
|
||||
|
||||
// TODO: 从内存内容解析
|
||||
result.success = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string AlsParser::resolvePath(const std::string &relativePath) const {
|
||||
if (!basePath_.empty() && !relativePath.empty() && relativePath[0] != '/') {
|
||||
return basePath_ + "/" + relativePath;
|
||||
}
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <extra2d/animation/ani_binary_parser.h>
|
||||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
namespace {
|
||||
|
||||
// 简易二进制缓冲区读取器
|
||||
class BufferReader {
|
||||
public:
|
||||
BufferReader(const uint8_t *data, size_t length)
|
||||
: data_(data), length_(length), pos_(0) {}
|
||||
|
||||
template <typename T> T read() {
|
||||
if (pos_ + sizeof(T) > length_) {
|
||||
return T{};
|
||||
}
|
||||
T value;
|
||||
std::memcpy(&value, data_ + pos_, sizeof(T));
|
||||
pos_ += sizeof(T);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string readAsciiString(int32_t len) {
|
||||
if (len <= 0 || pos_ + static_cast<size_t>(len) > length_) {
|
||||
return "";
|
||||
}
|
||||
std::string result(reinterpret_cast<const char *>(data_ + pos_), len);
|
||||
pos_ += len;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasRemaining() const { return pos_ < length_; }
|
||||
size_t remaining() const { return length_ - pos_; }
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t length_;
|
||||
size_t pos_;
|
||||
};
|
||||
|
||||
// 字符串转小写
|
||||
void toLower(std::string &s) {
|
||||
for (auto &c : s) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
AniParseResult AniBinaryParser::parse(const uint8_t *data, size_t length) {
|
||||
AniParseResult result;
|
||||
result.clip = AnimationClip::create();
|
||||
|
||||
if (!data || length < 4) {
|
||||
result.success = false;
|
||||
result.errorMessage = "Invalid binary ANI data";
|
||||
return result;
|
||||
}
|
||||
|
||||
BufferReader reader(data, length);
|
||||
|
||||
// 读取帧数和资源数
|
||||
uint16_t frameCount = reader.read<uint16_t>();
|
||||
uint16_t resourceCount = reader.read<uint16_t>();
|
||||
|
||||
// 读取精灵路径列表
|
||||
std::vector<std::string> sprites;
|
||||
sprites.reserve(resourceCount);
|
||||
for (uint16_t i = 0; i < resourceCount; ++i) {
|
||||
int32_t len = reader.read<int32_t>();
|
||||
std::string path = reader.readAsciiString(len);
|
||||
toLower(path);
|
||||
sprites.push_back(std::move(path));
|
||||
}
|
||||
|
||||
// 读取全局参数
|
||||
uint16_t globalParamCount = reader.read<uint16_t>();
|
||||
for (uint16_t j = 0; j < globalParamCount; ++j) {
|
||||
uint16_t type = reader.read<uint16_t>();
|
||||
switch (type) {
|
||||
case static_cast<uint16_t>(AniNodeType::Loop):
|
||||
if (reader.read<int8_t>()) {
|
||||
result.clip->setLooping(true);
|
||||
}
|
||||
break;
|
||||
case static_cast<uint16_t>(AniNodeType::Shadow):
|
||||
if (reader.read<int8_t>()) {
|
||||
result.clip->globalProperties().set(FramePropertyKey::Shadow, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 逐帧解析
|
||||
for (uint16_t i = 0; i < frameCount; ++i) {
|
||||
AnimationFrame frame;
|
||||
|
||||
// 碰撞盒
|
||||
uint16_t boxCount = reader.read<uint16_t>();
|
||||
for (uint16_t j = 0; j < boxCount; ++j) {
|
||||
uint16_t boxType = reader.read<uint16_t>();
|
||||
std::array<int32_t, 6> box;
|
||||
for (int m = 0; m < 6; ++m) {
|
||||
box[m] = reader.read<int32_t>();
|
||||
}
|
||||
if (boxType == static_cast<uint16_t>(AniNodeType::DamageBox)) {
|
||||
frame.damageBoxes.push_back(box);
|
||||
} else if (boxType == static_cast<uint16_t>(AniNodeType::AttackBox)) {
|
||||
frame.attackBoxes.push_back(box);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片 ID 和参数
|
||||
uint16_t imgId = reader.read<uint16_t>();
|
||||
uint16_t imgParam = reader.read<uint16_t>();
|
||||
(void)imgParam;
|
||||
|
||||
if (imgId < sprites.size()) {
|
||||
frame.texturePath = sprites[imgId];
|
||||
frame.textureIndex = imgId;
|
||||
|
||||
std::string resolvedPath = resolvePath(frame.texturePath);
|
||||
auto spriteFrame = SpriteFrameCache::getInstance().getOrCreateFromFile(
|
||||
resolvedPath, frame.textureIndex);
|
||||
frame.spriteFrame = spriteFrame;
|
||||
}
|
||||
|
||||
// 位置
|
||||
frame.offset = Vec2(static_cast<float>(reader.read<int32_t>()),
|
||||
static_cast<float>(reader.read<int32_t>()));
|
||||
|
||||
// 帧属性
|
||||
uint16_t propertyCount = reader.read<uint16_t>();
|
||||
for (uint16_t m = 0; m < propertyCount; ++m) {
|
||||
uint16_t propType = reader.read<uint16_t>();
|
||||
AniNodeType nodeType = static_cast<AniNodeType>(propType);
|
||||
|
||||
switch (nodeType) {
|
||||
case AniNodeType::Loop:
|
||||
frame.properties.set(FramePropertyKey::Loop,
|
||||
static_cast<bool>(reader.read<int8_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::Shadow:
|
||||
frame.properties.set(FramePropertyKey::Shadow,
|
||||
static_cast<bool>(reader.read<int8_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::Interpolation:
|
||||
frame.properties.withInterpolation(
|
||||
static_cast<bool>(reader.read<int8_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::Coord:
|
||||
frame.properties.set(FramePropertyKey::Coord,
|
||||
static_cast<int>(reader.read<uint16_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::ImageRate: {
|
||||
float rateX = reader.read<float>();
|
||||
float rateY = reader.read<float>();
|
||||
frame.properties.withImageRate(Vec2(rateX, rateY));
|
||||
break;
|
||||
}
|
||||
|
||||
case AniNodeType::ImageRotate:
|
||||
frame.properties.withImageRotate(
|
||||
static_cast<float>(reader.read<int32_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::RGBA: {
|
||||
uint32_t rgba = reader.read<uint32_t>();
|
||||
uint8_t a = static_cast<uint8_t>((rgba >> 24) & 0xFF);
|
||||
uint8_t r = static_cast<uint8_t>((rgba >> 16) & 0xFF);
|
||||
uint8_t g = static_cast<uint8_t>((rgba >> 8) & 0xFF);
|
||||
uint8_t b = static_cast<uint8_t>((rgba) & 0xFF);
|
||||
frame.properties.withColorTint(Color::fromRGBA(r, g, b, a));
|
||||
break;
|
||||
}
|
||||
|
||||
case AniNodeType::GraphicEffect: {
|
||||
uint16_t effectType = reader.read<uint16_t>();
|
||||
frame.properties.set(FramePropertyKey::GraphicEffect,
|
||||
static_cast<int>(effectType));
|
||||
// MONOCHROME 额外读取 rgb
|
||||
if (effectType == 5) {
|
||||
reader.read<uint8_t>(); // r
|
||||
reader.read<uint8_t>(); // g
|
||||
reader.read<uint8_t>(); // b
|
||||
}
|
||||
// SPACEDISTORT 额外读取 pos
|
||||
else if (effectType == 6) {
|
||||
reader.read<uint16_t>(); // x
|
||||
reader.read<uint16_t>(); // y
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AniNodeType::Delay:
|
||||
frame.delay = static_cast<float>(reader.read<int32_t>());
|
||||
break;
|
||||
|
||||
case AniNodeType::DamageType:
|
||||
frame.properties.set(FramePropertyKey::DamageType,
|
||||
static_cast<int>(reader.read<uint16_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::PlaySound: {
|
||||
int32_t len = reader.read<int32_t>();
|
||||
std::string soundPath = reader.readAsciiString(len);
|
||||
frame.properties.withPlaySound(resolvePath(soundPath));
|
||||
break;
|
||||
}
|
||||
|
||||
case AniNodeType::SetFlag:
|
||||
frame.properties.withSetFlag(reader.read<int32_t>());
|
||||
break;
|
||||
|
||||
case AniNodeType::FlipType:
|
||||
frame.properties.set(FramePropertyKey::FlipType,
|
||||
static_cast<int>(reader.read<uint16_t>()));
|
||||
break;
|
||||
|
||||
case AniNodeType::LoopStart:
|
||||
frame.properties.set(FramePropertyKey::LoopStart, true);
|
||||
break;
|
||||
|
||||
case AniNodeType::LoopEnd:
|
||||
frame.properties.set(FramePropertyKey::LoopEnd, reader.read<int32_t>());
|
||||
break;
|
||||
|
||||
case AniNodeType::Clip: {
|
||||
std::vector<int> clipRegion = {
|
||||
static_cast<int>(reader.read<int16_t>()),
|
||||
static_cast<int>(reader.read<int16_t>()),
|
||||
static_cast<int>(reader.read<int16_t>()),
|
||||
static_cast<int>(reader.read<int16_t>())};
|
||||
frame.properties.set(FramePropertyKey::ClipRegion, clipRegion);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// 未知类型 - 跳过
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.clip->addFrame(std::move(frame));
|
||||
}
|
||||
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
AniParseResult AniBinaryParser::parseFromFile(const std::string &filePath) {
|
||||
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
AniParseResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = "Cannot open binary ANI file: " + filePath;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> buffer(static_cast<size_t>(size));
|
||||
file.read(reinterpret_cast<char *>(buffer.data()), size);
|
||||
file.close();
|
||||
|
||||
// 提取基础路径
|
||||
auto lastSlash = filePath.find_last_of("/\\");
|
||||
if (lastSlash != std::string::npos && basePath_.empty()) {
|
||||
basePath_ = filePath.substr(0, lastSlash);
|
||||
}
|
||||
|
||||
auto result = parse(buffer.data(), buffer.size());
|
||||
|
||||
if (result.clip) {
|
||||
result.clip->setSourcePath(filePath);
|
||||
std::string name = (lastSlash != std::string::npos)
|
||||
? filePath.substr(lastSlash + 1)
|
||||
: filePath;
|
||||
result.clip->setName(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
AniBinaryParser::resolvePath(const std::string &relativePath) const {
|
||||
std::string resolved = relativePath;
|
||||
if (pathResolver_) {
|
||||
resolved = pathResolver_(relativePath);
|
||||
}
|
||||
|
||||
if (!basePath_.empty() && !resolved.empty() && resolved[0] != '/') {
|
||||
if (resolved.size() < 2 || resolved[1] != ':') {
|
||||
resolved = basePath_ + "/" + resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
#include <extra2d/animation/ani_parser.h>
|
||||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string trim(const std::string &s) {
|
||||
auto start = s.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos)
|
||||
return "";
|
||||
auto end = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
std::string stripBackticks(const std::string &s) {
|
||||
std::string t = trim(s);
|
||||
if (t.size() >= 2 && t.front() == '`' && t.back() == '`') {
|
||||
return t.substr(1, t.size() - 2);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitWhitespace(const std::string &s) {
|
||||
std::vector<std::string> tokens;
|
||||
std::istringstream iss(s);
|
||||
std::string token;
|
||||
while (iss >> token) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
bool lineStartsWith(const std::string &trimmed, const std::string &tag) {
|
||||
return trimmed.find(tag) == 0;
|
||||
}
|
||||
|
||||
std::string valueAfterTag(const std::string &trimmed, const std::string &tag) {
|
||||
auto pos = trimmed.find(tag);
|
||||
if (pos == std::string::npos)
|
||||
return "";
|
||||
return trim(trimmed.substr(pos + tag.size()));
|
||||
}
|
||||
|
||||
// 读取标签后的值,如果同行没有则从流中读取下一行
|
||||
std::string readValue(const std::string &trimmed, const std::string &tag,
|
||||
std::istringstream &stream) {
|
||||
std::string val = valueAfterTag(trimmed, tag);
|
||||
if (val.empty()) {
|
||||
std::string nextLine;
|
||||
if (std::getline(stream, nextLine)) {
|
||||
val = trim(nextLine);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
AniParseResult AniParser::parse(const std::string &filePath) {
|
||||
AniParseResult result;
|
||||
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
result.success = false;
|
||||
result.errorMessage = "Cannot open ANI file: " + filePath;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
// 提取基础路径
|
||||
auto lastSlash = filePath.find_last_of("/\\");
|
||||
std::string basePath;
|
||||
if (lastSlash != std::string::npos) {
|
||||
basePath = filePath.substr(0, lastSlash);
|
||||
}
|
||||
|
||||
if (basePath_.empty() && !basePath.empty()) {
|
||||
basePath_ = basePath;
|
||||
}
|
||||
|
||||
result = parseFromMemory(content, basePath_.empty() ? basePath : basePath_);
|
||||
|
||||
if (result.clip) {
|
||||
result.clip->setSourcePath(filePath);
|
||||
std::string name = (lastSlash != std::string::npos)
|
||||
? filePath.substr(lastSlash + 1)
|
||||
: filePath;
|
||||
result.clip->setName(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AniParseResult AniParser::parseFromMemory(const std::string &content,
|
||||
const std::string &basePath) {
|
||||
AniParseResult result;
|
||||
result.clip = AnimationClip::create();
|
||||
|
||||
if (!basePath.empty() && basePath_.empty()) {
|
||||
basePath_ = basePath;
|
||||
}
|
||||
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
|
||||
AnimationFrame currentFrame;
|
||||
bool inFrame = false;
|
||||
bool hasCurrentFrame = false;
|
||||
|
||||
// [IMAGE] 标签的多行读取状态
|
||||
bool pendingImage = false;
|
||||
std::string imagePath;
|
||||
|
||||
auto finalizeFrame = [&]() {
|
||||
if (!hasCurrentFrame)
|
||||
return;
|
||||
if (!currentFrame.texturePath.empty()) {
|
||||
std::string resolvedPath = resolvePath(currentFrame.texturePath);
|
||||
auto spriteFrame = SpriteFrameCache::getInstance().getOrCreateFromFile(
|
||||
resolvedPath, currentFrame.textureIndex);
|
||||
currentFrame.spriteFrame = spriteFrame;
|
||||
}
|
||||
result.clip->addFrame(std::move(currentFrame));
|
||||
currentFrame = AnimationFrame();
|
||||
hasCurrentFrame = false;
|
||||
};
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
std::string trimmed = trim(line);
|
||||
|
||||
if (trimmed.empty())
|
||||
continue;
|
||||
if (trimmed[0] == '#')
|
||||
continue;
|
||||
|
||||
// [FRAME MAX]
|
||||
if (lineStartsWith(trimmed, "[FRAME MAX]")) {
|
||||
readValue(trimmed, "[FRAME MAX]", stream);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [FRAMEXXX] - 新帧开始
|
||||
if (trimmed.size() >= 7 && trimmed.substr(0, 6) == "[FRAME" &&
|
||||
trimmed.back() == ']' && !lineStartsWith(trimmed, "[FRAME MAX]")) {
|
||||
finalizeFrame();
|
||||
currentFrame = AnimationFrame();
|
||||
inFrame = true;
|
||||
hasCurrentFrame = true;
|
||||
pendingImage = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inFrame) {
|
||||
// 全局属性
|
||||
if (lineStartsWith(trimmed, "[LOOP]")) {
|
||||
result.clip->setLooping(true);
|
||||
} else if (lineStartsWith(trimmed, "[SHADOW]")) {
|
||||
result.clip->globalProperties().set(FramePropertyKey::Shadow, true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 帧内属性解析 ===
|
||||
|
||||
// [IMAGE] - 图片路径和索引(跨行)
|
||||
if (lineStartsWith(trimmed, "[IMAGE]")) {
|
||||
std::string val = valueAfterTag(trimmed, "[IMAGE]");
|
||||
if (!val.empty()) {
|
||||
auto tokens = splitWhitespace(val);
|
||||
imagePath = stripBackticks(tokens[0]);
|
||||
if (tokens.size() >= 2) {
|
||||
currentFrame.texturePath = imagePath;
|
||||
currentFrame.textureIndex = std::stoi(tokens[1]);
|
||||
pendingImage = false;
|
||||
} else {
|
||||
pendingImage = true;
|
||||
}
|
||||
} else {
|
||||
pendingImage = true;
|
||||
imagePath.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [IMAGE] 后续行
|
||||
if (pendingImage) {
|
||||
if (imagePath.empty()) {
|
||||
imagePath = stripBackticks(trimmed);
|
||||
} else {
|
||||
currentFrame.texturePath = imagePath;
|
||||
currentFrame.textureIndex = std::stoi(trimmed);
|
||||
pendingImage = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [IMAGE POS]
|
||||
if (lineStartsWith(trimmed, "[IMAGE POS]")) {
|
||||
std::string val = readValue(trimmed, "[IMAGE POS]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 2) {
|
||||
currentFrame.offset = Vec2(static_cast<float>(std::stoi(tokens[0])),
|
||||
static_cast<float>(std::stoi(tokens[1])));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [DELAY]
|
||||
if (lineStartsWith(trimmed, "[DELAY]")) {
|
||||
std::string val = readValue(trimmed, "[DELAY]", stream);
|
||||
currentFrame.delay = static_cast<float>(std::stoi(trim(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// [DAMAGE TYPE]
|
||||
if (lineStartsWith(trimmed, "[DAMAGE TYPE]")) {
|
||||
std::string val = readValue(trimmed, "[DAMAGE TYPE]", stream);
|
||||
val = stripBackticks(val);
|
||||
int damageType = 0;
|
||||
if (val == "SUPERARMOR")
|
||||
damageType = 1;
|
||||
else if (val == "UNBREAKABLE")
|
||||
damageType = 2;
|
||||
currentFrame.properties.set(FramePropertyKey::DamageType, damageType);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [DAMAGE BOX]
|
||||
if (lineStartsWith(trimmed, "[DAMAGE BOX]")) {
|
||||
std::string val = readValue(trimmed, "[DAMAGE BOX]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 6) {
|
||||
std::array<int32_t, 6> box;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
box[i] = std::stoi(tokens[i]);
|
||||
}
|
||||
currentFrame.damageBoxes.push_back(box);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [ATTACK BOX]
|
||||
if (lineStartsWith(trimmed, "[ATTACK BOX]")) {
|
||||
std::string val = readValue(trimmed, "[ATTACK BOX]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 6) {
|
||||
std::array<int32_t, 6> box;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
box[i] = std::stoi(tokens[i]);
|
||||
}
|
||||
currentFrame.attackBoxes.push_back(box);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [SET FLAG]
|
||||
if (lineStartsWith(trimmed, "[SET FLAG]")) {
|
||||
std::string val = readValue(trimmed, "[SET FLAG]", stream);
|
||||
currentFrame.properties.withSetFlag(std::stoi(trim(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// [PLAY SOUND]
|
||||
if (lineStartsWith(trimmed, "[PLAY SOUND]")) {
|
||||
std::string val = readValue(trimmed, "[PLAY SOUND]", stream);
|
||||
currentFrame.properties.withPlaySound(resolvePath(stripBackticks(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// [IMAGE RATE]
|
||||
if (lineStartsWith(trimmed, "[IMAGE RATE]")) {
|
||||
std::string val = readValue(trimmed, "[IMAGE RATE]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 2) {
|
||||
currentFrame.properties.withImageRate(
|
||||
Vec2(std::stof(tokens[0]), std::stof(tokens[1])));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [IMAGE ROTATE]
|
||||
if (lineStartsWith(trimmed, "[IMAGE ROTATE]")) {
|
||||
std::string val = readValue(trimmed, "[IMAGE ROTATE]", stream);
|
||||
currentFrame.properties.withImageRotate(std::stof(trim(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// [RGBA]
|
||||
if (lineStartsWith(trimmed, "[RGBA]")) {
|
||||
std::string val = readValue(trimmed, "[RGBA]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 4) {
|
||||
Color color =
|
||||
Color::fromRGBA(static_cast<uint8_t>(std::stoi(tokens[0])),
|
||||
static_cast<uint8_t>(std::stoi(tokens[1])),
|
||||
static_cast<uint8_t>(std::stoi(tokens[2])),
|
||||
static_cast<uint8_t>(std::stoi(tokens[3])));
|
||||
currentFrame.properties.withColorTint(color);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [INTERPOLATION]
|
||||
if (lineStartsWith(trimmed, "[INTERPOLATION]")) {
|
||||
currentFrame.properties.withInterpolation(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [LOOP]
|
||||
if (lineStartsWith(trimmed, "[LOOP]")) {
|
||||
currentFrame.properties.set(FramePropertyKey::Loop, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [SHADOW]
|
||||
if (lineStartsWith(trimmed, "[SHADOW]")) {
|
||||
currentFrame.properties.set(FramePropertyKey::Shadow, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [FLIP TYPE]
|
||||
if (lineStartsWith(trimmed, "[FLIP TYPE]")) {
|
||||
std::string val = readValue(trimmed, "[FLIP TYPE]", stream);
|
||||
val = stripBackticks(val);
|
||||
int flipType = 0;
|
||||
if (val == "HORIZON")
|
||||
flipType = 1;
|
||||
else if (val == "VERTICAL")
|
||||
flipType = 2;
|
||||
else if (val == "ALL")
|
||||
flipType = 3;
|
||||
else
|
||||
flipType = std::stoi(val);
|
||||
currentFrame.properties.set(FramePropertyKey::FlipType, flipType);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [COORD]
|
||||
if (lineStartsWith(trimmed, "[COORD]")) {
|
||||
std::string val = readValue(trimmed, "[COORD]", stream);
|
||||
currentFrame.properties.set(FramePropertyKey::Coord,
|
||||
std::stoi(trim(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// [GRAPHIC EFFECT]
|
||||
if (lineStartsWith(trimmed, "[GRAPHIC EFFECT]")) {
|
||||
std::string val = readValue(trimmed, "[GRAPHIC EFFECT]", stream);
|
||||
val = stripBackticks(val);
|
||||
int effectType = 0;
|
||||
if (val == "DODGE")
|
||||
effectType = 1;
|
||||
else if (val == "LINEARDODGE")
|
||||
effectType = 2;
|
||||
else if (val == "DARK")
|
||||
effectType = 3;
|
||||
else if (val == "XOR")
|
||||
effectType = 4;
|
||||
else if (val == "MONOCHROME")
|
||||
effectType = 5;
|
||||
else if (val == "SPACEDISTORT")
|
||||
effectType = 6;
|
||||
else
|
||||
effectType = std::stoi(val);
|
||||
currentFrame.properties.set(FramePropertyKey::GraphicEffect, effectType);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [CLIP]
|
||||
if (lineStartsWith(trimmed, "[CLIP]")) {
|
||||
std::string val = readValue(trimmed, "[CLIP]", stream);
|
||||
auto tokens = splitWhitespace(val);
|
||||
if (tokens.size() >= 4) {
|
||||
std::vector<int> clipRegion = {
|
||||
std::stoi(tokens[0]), std::stoi(tokens[1]), std::stoi(tokens[2]),
|
||||
std::stoi(tokens[3])};
|
||||
currentFrame.properties.set(FramePropertyKey::ClipRegion, clipRegion);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// [LOOP START]
|
||||
if (lineStartsWith(trimmed, "[LOOP START]")) {
|
||||
currentFrame.properties.set(FramePropertyKey::LoopStart, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [LOOP END]
|
||||
if (lineStartsWith(trimmed, "[LOOP END]")) {
|
||||
std::string val = readValue(trimmed, "[LOOP END]", stream);
|
||||
currentFrame.properties.set(FramePropertyKey::LoopEnd,
|
||||
std::stoi(trim(val)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 未识别标签 - 静默忽略
|
||||
}
|
||||
|
||||
// 保存最后一帧
|
||||
finalizeFrame();
|
||||
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string AniParser::resolvePath(const std::string &relativePath) const {
|
||||
std::string resolved = relativePath;
|
||||
if (pathResolver_) {
|
||||
resolved = pathResolver_(relativePath);
|
||||
}
|
||||
|
||||
if (!basePath_.empty() && !resolved.empty() && resolved[0] != '/') {
|
||||
if (resolved.size() < 2 || resolved[1] != ':') {
|
||||
resolved = basePath_ + "/" + resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,290 +0,0 @@
|
|||
#include <extra2d/animation/animated_sprite.h>
|
||||
#include <extra2d/animation/interpolation_engine.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> AnimatedSprite::emptyBoxes_;
|
||||
const std::string AnimatedSprite::emptyString_;
|
||||
|
||||
AnimatedSprite::AnimatedSprite() {
|
||||
controller_.setFrameChangeCallback(
|
||||
[this](size_t oldIdx, size_t newIdx, const AnimationFrame &frame) {
|
||||
onFrameChanged(oldIdx, newIdx, frame);
|
||||
});
|
||||
}
|
||||
|
||||
// ------ 静态工厂 ------
|
||||
|
||||
Ptr<AnimatedSprite> AnimatedSprite::create() {
|
||||
return makePtr<AnimatedSprite>();
|
||||
}
|
||||
|
||||
Ptr<AnimatedSprite> AnimatedSprite::create(Ptr<AnimationClip> clip) {
|
||||
auto sprite = makePtr<AnimatedSprite>();
|
||||
sprite->setAnimationClip(std::move(clip));
|
||||
return sprite;
|
||||
}
|
||||
|
||||
Ptr<AnimatedSprite> AnimatedSprite::create(const std::string &aniFilePath) {
|
||||
auto sprite = makePtr<AnimatedSprite>();
|
||||
sprite->loadAnimation(aniFilePath);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// ------ 动画绑定 ------
|
||||
|
||||
void AnimatedSprite::setAnimationClip(Ptr<AnimationClip> clip) {
|
||||
controller_.setClip(clip);
|
||||
if (clip && !clip->empty()) {
|
||||
applyFrame(clip->getFrame(0));
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedSprite::loadAnimation(const std::string &aniFilePath) {
|
||||
auto clip = AnimationCache::getInstance().loadClip(aniFilePath);
|
||||
if (clip) {
|
||||
setAnimationClip(clip);
|
||||
}
|
||||
}
|
||||
|
||||
Ptr<AnimationClip> AnimatedSprite::getAnimationClip() const {
|
||||
return controller_.getClip();
|
||||
}
|
||||
|
||||
// ------ 动画字典 ------
|
||||
|
||||
void AnimatedSprite::addAnimation(const std::string &name,
|
||||
Ptr<AnimationClip> clip) {
|
||||
if (clip) {
|
||||
animations_[name] = std::move(clip);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedSprite::play(const std::string &name, bool loop) {
|
||||
auto it = animations_.find(name);
|
||||
if (it == animations_.end())
|
||||
return;
|
||||
|
||||
currentAnimationName_ = name;
|
||||
// 精灵图动画不应覆盖节点的 position/scale/rotation
|
||||
applyFrameTransform_ = false;
|
||||
setAnimationClip(it->second);
|
||||
setLooping(loop);
|
||||
play();
|
||||
}
|
||||
|
||||
bool AnimatedSprite::hasAnimation(const std::string &name) const {
|
||||
return animations_.find(name) != animations_.end();
|
||||
}
|
||||
|
||||
Ptr<AnimationClip> AnimatedSprite::getAnimation(const std::string &name) const {
|
||||
auto it = animations_.find(name);
|
||||
if (it != animations_.end())
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::string &AnimatedSprite::getCurrentAnimationName() const {
|
||||
return currentAnimationName_;
|
||||
}
|
||||
|
||||
// ------ 播放控制 ------
|
||||
|
||||
void AnimatedSprite::play() { controller_.play(); }
|
||||
void AnimatedSprite::pause() { controller_.pause(); }
|
||||
void AnimatedSprite::resume() { controller_.resume(); }
|
||||
void AnimatedSprite::stop() { controller_.stop(); }
|
||||
void AnimatedSprite::reset() { controller_.reset(); }
|
||||
bool AnimatedSprite::isPlaying() const { return controller_.isPlaying(); }
|
||||
bool AnimatedSprite::isPaused() const { return controller_.isPaused(); }
|
||||
bool AnimatedSprite::isStopped() const { return controller_.isStopped(); }
|
||||
|
||||
// ------ 属性控制 ------
|
||||
|
||||
void AnimatedSprite::setLooping(bool loop) { controller_.setLooping(loop); }
|
||||
bool AnimatedSprite::isLooping() const { return controller_.isLooping(); }
|
||||
void AnimatedSprite::setPlaybackSpeed(float speed) {
|
||||
controller_.setPlaybackSpeed(speed);
|
||||
}
|
||||
float AnimatedSprite::getPlaybackSpeed() const {
|
||||
return controller_.getPlaybackSpeed();
|
||||
}
|
||||
|
||||
// ------ 帧控制 ------
|
||||
|
||||
void AnimatedSprite::setFrameIndex(size_t index) {
|
||||
controller_.setFrameIndex(index);
|
||||
}
|
||||
size_t AnimatedSprite::getCurrentFrameIndex() const {
|
||||
return controller_.getCurrentFrameIndex();
|
||||
}
|
||||
size_t AnimatedSprite::getTotalFrames() const {
|
||||
return controller_.getTotalFrames();
|
||||
}
|
||||
void AnimatedSprite::nextFrame() { controller_.nextFrame(); }
|
||||
void AnimatedSprite::prevFrame() { controller_.prevFrame(); }
|
||||
|
||||
// ------ 帧范围限制 ------
|
||||
|
||||
void AnimatedSprite::setFrameRange(int start, int end) {
|
||||
frameRangeStart_ = start;
|
||||
frameRangeEnd_ = end;
|
||||
|
||||
// 确保当前帧在新的范围内
|
||||
auto clip = controller_.getClip();
|
||||
if (clip && !clip->empty()) {
|
||||
size_t currentFrame = controller_.getCurrentFrameIndex();
|
||||
size_t minFrame = static_cast<size_t>(start);
|
||||
size_t maxFrame =
|
||||
(end < 0) ? clip->getFrameCount() - 1 : static_cast<size_t>(end);
|
||||
|
||||
if (currentFrame < minFrame || currentFrame > maxFrame) {
|
||||
controller_.setFrameIndex(minFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<int, int> AnimatedSprite::getFrameRange() const {
|
||||
return {frameRangeStart_, frameRangeEnd_};
|
||||
}
|
||||
|
||||
void AnimatedSprite::clearFrameRange() {
|
||||
frameRangeStart_ = 0;
|
||||
frameRangeEnd_ = -1;
|
||||
}
|
||||
|
||||
bool AnimatedSprite::hasFrameRange() const { return frameRangeEnd_ >= 0; }
|
||||
|
||||
// ------ 回调 ------
|
||||
|
||||
void AnimatedSprite::setCompletionCallback(
|
||||
AnimationController::CompletionCallback cb) {
|
||||
controller_.setCompletionCallback(std::move(cb));
|
||||
}
|
||||
|
||||
void AnimatedSprite::setKeyframeCallback(
|
||||
AnimationController::KeyframeCallback cb) {
|
||||
controller_.setKeyframeCallback(std::move(cb));
|
||||
}
|
||||
|
||||
void AnimatedSprite::setSoundTriggerCallback(
|
||||
AnimationController::SoundTriggerCallback cb) {
|
||||
controller_.setSoundTriggerCallback(std::move(cb));
|
||||
}
|
||||
|
||||
// ------ 碰撞盒访问 ------
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> &
|
||||
AnimatedSprite::getCurrentDamageBoxes() const {
|
||||
auto clip = controller_.getClip();
|
||||
if (!clip || clip->empty())
|
||||
return emptyBoxes_;
|
||||
return clip->getFrame(controller_.getCurrentFrameIndex()).damageBoxes;
|
||||
}
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> &
|
||||
AnimatedSprite::getCurrentAttackBoxes() const {
|
||||
auto clip = controller_.getClip();
|
||||
if (!clip || clip->empty())
|
||||
return emptyBoxes_;
|
||||
return clip->getFrame(controller_.getCurrentFrameIndex()).attackBoxes;
|
||||
}
|
||||
|
||||
// ------ 生命周期 ------
|
||||
|
||||
void AnimatedSprite::onEnter() {
|
||||
Sprite::onEnter();
|
||||
if (autoPlay_ && controller_.getClip() && !controller_.getClip()->empty()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedSprite::onUpdate(float dt) {
|
||||
Sprite::onUpdate(dt);
|
||||
|
||||
// 保存更新前的帧索引
|
||||
size_t prevFrameIdx = controller_.getCurrentFrameIndex();
|
||||
|
||||
controller_.update(dt);
|
||||
|
||||
// 应用帧范围限制
|
||||
if (hasFrameRange() && controller_.isPlaying()) {
|
||||
size_t currentFrame = controller_.getCurrentFrameIndex();
|
||||
size_t minFrame = static_cast<size_t>(frameRangeStart_);
|
||||
size_t maxFrame = static_cast<size_t>(frameRangeEnd_);
|
||||
|
||||
auto clip = controller_.getClip();
|
||||
if (clip && !clip->empty()) {
|
||||
// 确保范围有效
|
||||
if (maxFrame >= clip->getFrameCount()) {
|
||||
maxFrame = clip->getFrameCount() - 1;
|
||||
}
|
||||
|
||||
// 如果超出范围,回到起始帧
|
||||
if (currentFrame < minFrame || currentFrame > maxFrame) {
|
||||
controller_.setFrameIndex(minFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 插值处理
|
||||
if (controller_.isInterpolating()) {
|
||||
auto clip = controller_.getClip();
|
||||
if (clip) {
|
||||
size_t idx = controller_.getCurrentFrameIndex();
|
||||
if (idx + 1 < clip->getFrameCount()) {
|
||||
auto props = InterpolationEngine::interpolate(
|
||||
clip->getFrame(idx), clip->getFrame(idx + 1),
|
||||
controller_.getInterpolationFactor());
|
||||
// 仅更新插值属性,不覆盖帧纹理
|
||||
Sprite::setColor(props.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------ 内部方法 ------
|
||||
|
||||
void AnimatedSprite::onFrameChanged(size_t /*oldIdx*/, size_t /*newIdx*/,
|
||||
const AnimationFrame &frame) {
|
||||
applyFrame(frame);
|
||||
}
|
||||
|
||||
void AnimatedSprite::applyFrame(const AnimationFrame &frame) {
|
||||
// 更新纹理
|
||||
if (frame.spriteFrame && frame.spriteFrame->isValid()) {
|
||||
Sprite::setTexture(frame.spriteFrame->getTexture());
|
||||
Sprite::setTextureRect(frame.spriteFrame->getRect());
|
||||
}
|
||||
|
||||
// 帧变换仅在 ANI 动画模式下应用;精灵图模式跳过,避免覆盖节点世界坐标
|
||||
if (applyFrameTransform_) {
|
||||
// 应用帧偏移(作为精灵位置)
|
||||
Node::setPosition(frame.offset);
|
||||
|
||||
// 应用缩放
|
||||
Vec2 scale = frame.getEffectiveScale();
|
||||
Node::setScale(scale);
|
||||
|
||||
// 应用旋转
|
||||
float rotation = frame.getEffectiveRotation();
|
||||
Node::setRotation(rotation);
|
||||
|
||||
// 应用翻转
|
||||
auto flipType = frame.properties.get<int>(FramePropertyKey::FlipType);
|
||||
if (flipType.has_value()) {
|
||||
int flip = flipType.value();
|
||||
Sprite::setFlipX(flip == 1 || flip == 3);
|
||||
Sprite::setFlipY(flip == 2 || flip == 3);
|
||||
} else {
|
||||
Sprite::setFlipX(false);
|
||||
Sprite::setFlipY(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 颜色始终应用
|
||||
Color color = frame.getEffectiveColor();
|
||||
Sprite::setColor(color);
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
#include <extra2d/animation/ani_binary_parser.h>
|
||||
#include <extra2d/animation/ani_parser.h>
|
||||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
namespace {
|
||||
|
||||
// 检测文件是否为文本格式 ANI
|
||||
// 文本格式以 '#' 或 '[' 开头(跳过空白后)
|
||||
bool isTextFormat(const std::string &filePath) {
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
return false;
|
||||
|
||||
// 读取前几个字节判断
|
||||
char buf[16] = {};
|
||||
file.read(buf, sizeof(buf));
|
||||
auto bytesRead = file.gcount();
|
||||
file.close();
|
||||
|
||||
// 跳过 BOM 和空白
|
||||
size_t pos = 0;
|
||||
// UTF-8 BOM
|
||||
if (bytesRead >= 3 && static_cast<uint8_t>(buf[0]) == 0xEF &&
|
||||
static_cast<uint8_t>(buf[1]) == 0xBB &&
|
||||
static_cast<uint8_t>(buf[2]) == 0xBF) {
|
||||
pos = 3;
|
||||
}
|
||||
|
||||
// 跳过空白
|
||||
while (pos < static_cast<size_t>(bytesRead) &&
|
||||
(buf[pos] == ' ' || buf[pos] == '\t' || buf[pos] == '\r' ||
|
||||
buf[pos] == '\n')) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
if (pos >= static_cast<size_t>(bytesRead))
|
||||
return false;
|
||||
|
||||
// 文本 ANI 文件以 '#' (注释/PVF_File) 或 '[' (标签) 开头
|
||||
return buf[pos] == '#' || buf[pos] == '[';
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
Ptr<AnimationClip> AnimationCache::loadClip(const std::string &aniFilePath) {
|
||||
// 先检查缓存
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = clips_.find(aniFilePath);
|
||||
if (it != clips_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取基础路径
|
||||
std::string basePath;
|
||||
auto lastSlash = aniFilePath.find_last_of("/\\");
|
||||
if (lastSlash != std::string::npos) {
|
||||
basePath = aniFilePath.substr(0, lastSlash);
|
||||
}
|
||||
|
||||
AniParseResult result;
|
||||
|
||||
if (isTextFormat(aniFilePath)) {
|
||||
// 文本格式
|
||||
AniParser parser;
|
||||
if (pathResolver_) {
|
||||
parser.setPathResolver(pathResolver_);
|
||||
}
|
||||
if (!basePath.empty()) {
|
||||
parser.setBasePath(basePath);
|
||||
}
|
||||
result = parser.parse(aniFilePath);
|
||||
} else {
|
||||
// 二进制格式
|
||||
AniBinaryParser parser;
|
||||
if (pathResolver_) {
|
||||
parser.setPathResolver(pathResolver_);
|
||||
}
|
||||
if (!basePath.empty()) {
|
||||
parser.setBasePath(basePath);
|
||||
}
|
||||
result = parser.parseFromFile(aniFilePath);
|
||||
}
|
||||
|
||||
if (!result.success || !result.clip) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
clips_[aniFilePath] = result.clip;
|
||||
}
|
||||
|
||||
return result.clip;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#include <extra2d/animation/animation_clip.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// AnimationClip 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
#include <cassert>
|
||||
#include <extra2d/animation/animation_controller.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
void AnimationController::setClip(Ptr<AnimationClip> clip) {
|
||||
clip_ = std::move(clip);
|
||||
currentFrameIndex_ = 0;
|
||||
accumulatedTime_ = 0.0f;
|
||||
interpolating_ = false;
|
||||
interpolationFactor_ = 0.0f;
|
||||
state_ = AnimPlayState::Stopped;
|
||||
}
|
||||
|
||||
void AnimationController::play() {
|
||||
if (!clip_ || clip_->empty())
|
||||
return;
|
||||
state_ = AnimPlayState::Playing;
|
||||
}
|
||||
|
||||
void AnimationController::pause() {
|
||||
if (state_ == AnimPlayState::Playing) {
|
||||
state_ = AnimPlayState::Paused;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationController::resume() {
|
||||
if (state_ == AnimPlayState::Paused) {
|
||||
state_ = AnimPlayState::Playing;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationController::stop() {
|
||||
state_ = AnimPlayState::Stopped;
|
||||
accumulatedTime_ = 0.0f;
|
||||
interpolating_ = false;
|
||||
interpolationFactor_ = 0.0f;
|
||||
}
|
||||
|
||||
void AnimationController::reset() {
|
||||
stop();
|
||||
if (clip_ && !clip_->empty()) {
|
||||
advanceFrame(0);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationController::setFrameIndex(size_t index) {
|
||||
if (!clip_ || index >= clip_->getFrameCount())
|
||||
return;
|
||||
accumulatedTime_ = 0.0f;
|
||||
advanceFrame(index);
|
||||
}
|
||||
|
||||
void AnimationController::nextFrame() {
|
||||
if (!clip_ || clip_->empty())
|
||||
return;
|
||||
size_t next = currentFrameIndex_ + 1;
|
||||
if (next >= clip_->getFrameCount()) {
|
||||
if (isLooping()) {
|
||||
next = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
accumulatedTime_ = 0.0f;
|
||||
advanceFrame(next);
|
||||
}
|
||||
|
||||
void AnimationController::prevFrame() {
|
||||
if (!clip_ || clip_->empty())
|
||||
return;
|
||||
if (currentFrameIndex_ > 0) {
|
||||
accumulatedTime_ = 0.0f;
|
||||
advanceFrame(currentFrameIndex_ - 1);
|
||||
} else if (isLooping()) {
|
||||
accumulatedTime_ = 0.0f;
|
||||
advanceFrame(clip_->getFrameCount() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationController::update(float dt) {
|
||||
if (state_ != AnimPlayState::Playing)
|
||||
return;
|
||||
if (!clip_ || clip_->empty())
|
||||
return;
|
||||
|
||||
// 累加时间(转换为毫秒)
|
||||
float dt_ms = dt * 1000.0f * playbackSpeed_;
|
||||
accumulatedTime_ += dt_ms;
|
||||
|
||||
const AnimationFrame ¤tFrame = clip_->getFrame(currentFrameIndex_);
|
||||
float frameDelay = currentFrame.delay;
|
||||
|
||||
// 更新插值状态
|
||||
updateInterpolation();
|
||||
|
||||
// 循环处理:支持一次跳过多帧(与原始 ANI 系统行为一致)
|
||||
while (accumulatedTime_ >= frameDelay) {
|
||||
accumulatedTime_ -= frameDelay;
|
||||
|
||||
size_t totalFrames = clip_->getFrameCount();
|
||||
|
||||
if (currentFrameIndex_ < totalFrames - 1) {
|
||||
// 推进到下一帧
|
||||
advanceFrame(currentFrameIndex_ + 1);
|
||||
} else {
|
||||
// 最后一帧播放完毕
|
||||
if (isLooping()) {
|
||||
advanceFrame(0);
|
||||
} else {
|
||||
// 动画结束
|
||||
state_ = AnimPlayState::Stopped;
|
||||
if (onComplete_) {
|
||||
onComplete_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新下一帧的延迟
|
||||
frameDelay = clip_->getFrame(currentFrameIndex_).delay;
|
||||
}
|
||||
|
||||
// 更新插值因子
|
||||
updateInterpolation();
|
||||
}
|
||||
|
||||
size_t AnimationController::getTotalFrames() const {
|
||||
return clip_ ? clip_->getFrameCount() : 0;
|
||||
}
|
||||
|
||||
const AnimationFrame &AnimationController::getCurrentFrame() const {
|
||||
assert(clip_ && currentFrameIndex_ < clip_->getFrameCount());
|
||||
return clip_->getFrame(currentFrameIndex_);
|
||||
}
|
||||
|
||||
bool AnimationController::isLooping() const {
|
||||
if (hasLoopOverride_)
|
||||
return loopOverride_;
|
||||
return clip_ ? clip_->isLooping() : false;
|
||||
}
|
||||
|
||||
void AnimationController::setLooping(bool loop) {
|
||||
hasLoopOverride_ = true;
|
||||
loopOverride_ = loop;
|
||||
}
|
||||
|
||||
void AnimationController::advanceFrame(size_t newIndex) {
|
||||
if (!clip_ || newIndex >= clip_->getFrameCount())
|
||||
return;
|
||||
|
||||
size_t oldIndex = currentFrameIndex_;
|
||||
currentFrameIndex_ = newIndex;
|
||||
|
||||
const AnimationFrame &frame = clip_->getFrame(newIndex);
|
||||
|
||||
// 触发帧变更回调
|
||||
if (onFrameChange_) {
|
||||
onFrameChange_(oldIndex, newIndex, frame);
|
||||
}
|
||||
|
||||
// 处理帧属性(关键帧、音效等)
|
||||
processFrameProperties(frame);
|
||||
}
|
||||
|
||||
void AnimationController::processFrameProperties(const AnimationFrame &frame) {
|
||||
const auto &props = frame.properties;
|
||||
|
||||
// 关键帧回调
|
||||
if (props.has(FramePropertyKey::SetFlag)) {
|
||||
auto flagIndex = props.get<int>(FramePropertyKey::SetFlag);
|
||||
if (flagIndex.has_value() && onKeyframe_) {
|
||||
onKeyframe_(flagIndex.value());
|
||||
}
|
||||
}
|
||||
|
||||
// 音效触发
|
||||
if (props.has(FramePropertyKey::PlaySound)) {
|
||||
auto soundPath = props.get<std::string>(FramePropertyKey::PlaySound);
|
||||
if (soundPath.has_value() && onSoundTrigger_) {
|
||||
onSoundTrigger_(soundPath.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationController::updateInterpolation() {
|
||||
if (!clip_ || clip_->empty()) {
|
||||
interpolating_ = false;
|
||||
interpolationFactor_ = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
const AnimationFrame ¤tFrame = clip_->getFrame(currentFrameIndex_);
|
||||
|
||||
if (currentFrame.hasInterpolation() &&
|
||||
currentFrameIndex_ + 1 < clip_->getFrameCount()) {
|
||||
interpolating_ = true;
|
||||
float frameDelay = currentFrame.delay;
|
||||
interpolationFactor_ =
|
||||
(frameDelay > 0.0f)
|
||||
? math::clamp(accumulatedTime_ / frameDelay, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
} else {
|
||||
interpolating_ = false;
|
||||
interpolationFactor_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#include <extra2d/animation/animation_frame.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// AnimationFrame 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
#include <extra2d/animation/animation_node.h>
|
||||
#include <extra2d/animation/interpolation_engine.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> AnimationNode::emptyBoxes_;
|
||||
|
||||
// ============================================================================
|
||||
// 构造
|
||||
// ============================================================================
|
||||
|
||||
AnimationNode::AnimationNode() { setupControllerCallbacks(); }
|
||||
|
||||
// ============================================================================
|
||||
// 静态工厂
|
||||
// ============================================================================
|
||||
|
||||
Ptr<AnimationNode> AnimationNode::create() { return makePtr<AnimationNode>(); }
|
||||
|
||||
Ptr<AnimationNode> AnimationNode::create(Ptr<AnimationClip> clip) {
|
||||
auto node = makePtr<AnimationNode>();
|
||||
node->setClip(std::move(clip));
|
||||
return node;
|
||||
}
|
||||
|
||||
Ptr<AnimationNode> AnimationNode::create(const std::string &aniFilePath) {
|
||||
auto node = makePtr<AnimationNode>();
|
||||
node->loadFromFile(aniFilePath);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 动画数据
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::setClip(Ptr<AnimationClip> clip) {
|
||||
controller_.setClip(clip);
|
||||
if (clip && !clip->empty()) {
|
||||
// 预加载所有帧的 SpriteFrame
|
||||
std::vector<AnimationFrame> frames;
|
||||
frames.reserve(clip->getFrameCount());
|
||||
for (size_t i = 0; i < clip->getFrameCount(); ++i) {
|
||||
frames.push_back(clip->getFrame(i));
|
||||
}
|
||||
frameRenderer_.preloadFrames(frames);
|
||||
} else {
|
||||
frameRenderer_.releaseFrames();
|
||||
}
|
||||
}
|
||||
|
||||
Ptr<AnimationClip> AnimationNode::getClip() const {
|
||||
return controller_.getClip();
|
||||
}
|
||||
|
||||
bool AnimationNode::loadFromFile(const std::string &aniFilePath) {
|
||||
auto clip = AnimationCache::getInstance().loadClip(aniFilePath);
|
||||
if (clip) {
|
||||
setClip(clip);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 播放控制
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::play() { controller_.play(); }
|
||||
void AnimationNode::pause() { controller_.pause(); }
|
||||
void AnimationNode::resume() { controller_.resume(); }
|
||||
void AnimationNode::stop() { controller_.stop(); }
|
||||
void AnimationNode::reset() { controller_.reset(); }
|
||||
|
||||
bool AnimationNode::isPlaying() const { return controller_.isPlaying(); }
|
||||
bool AnimationNode::isPaused() const { return controller_.isPaused(); }
|
||||
bool AnimationNode::isStopped() const { return controller_.isStopped(); }
|
||||
|
||||
void AnimationNode::setPlaybackSpeed(float speed) {
|
||||
controller_.setPlaybackSpeed(speed);
|
||||
}
|
||||
float AnimationNode::getPlaybackSpeed() const {
|
||||
return controller_.getPlaybackSpeed();
|
||||
}
|
||||
void AnimationNode::setLooping(bool loop) { controller_.setLooping(loop); }
|
||||
bool AnimationNode::isLooping() const { return controller_.isLooping(); }
|
||||
|
||||
// ============================================================================
|
||||
// 帧控制
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::setFrameIndex(size_t index) {
|
||||
controller_.setFrameIndex(index);
|
||||
}
|
||||
size_t AnimationNode::getCurrentFrameIndex() const {
|
||||
return controller_.getCurrentFrameIndex();
|
||||
}
|
||||
size_t AnimationNode::getTotalFrames() const {
|
||||
return controller_.getTotalFrames();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 事件回调
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::setKeyframeCallback(KeyframeHitCallback callback) {
|
||||
controller_.setKeyframeCallback(
|
||||
[this, cb = std::move(callback)](int flagIndex) {
|
||||
if (cb)
|
||||
cb(flagIndex);
|
||||
AnimationEvent evt;
|
||||
evt.type = AnimationEventType::KeyframeHit;
|
||||
evt.frameIndex = controller_.getCurrentFrameIndex();
|
||||
evt.keyframeFlag = flagIndex;
|
||||
evt.source = this;
|
||||
dispatchEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
void AnimationNode::setCompletionCallback(AnimationCompleteCallback callback) {
|
||||
controller_.setCompletionCallback([this, cb = std::move(callback)]() {
|
||||
if (cb)
|
||||
cb();
|
||||
AnimationEvent evt;
|
||||
evt.type = AnimationEventType::AnimationEnd;
|
||||
evt.frameIndex = controller_.getCurrentFrameIndex();
|
||||
evt.source = this;
|
||||
dispatchEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
void AnimationNode::setFrameChangeCallback(
|
||||
AnimationController::FrameChangeCallback callback) {
|
||||
// 保存外部回调,在 setupControllerCallbacks 中已设置内部回调
|
||||
// 需要重新绑定,将两者合并
|
||||
controller_.setFrameChangeCallback(
|
||||
[this, cb = std::move(callback)](size_t oldIdx, size_t newIdx,
|
||||
const AnimationFrame &frame) {
|
||||
if (cb)
|
||||
cb(oldIdx, newIdx, frame);
|
||||
AnimationEvent evt;
|
||||
evt.type = AnimationEventType::FrameChanged;
|
||||
evt.frameIndex = newIdx;
|
||||
evt.previousFrameIndex = oldIdx;
|
||||
evt.source = this;
|
||||
dispatchEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
void AnimationNode::addEventListener(AnimationEventCallback callback) {
|
||||
eventListeners_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 视觉属性
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::setTintColor(const Color &color) { tintColor_ = color; }
|
||||
|
||||
// ============================================================================
|
||||
// 碰撞盒
|
||||
// ============================================================================
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> &
|
||||
AnimationNode::getCurrentDamageBoxes() const {
|
||||
auto clip = controller_.getClip();
|
||||
if (!clip || clip->empty())
|
||||
return emptyBoxes_;
|
||||
return clip->getFrame(controller_.getCurrentFrameIndex()).damageBoxes;
|
||||
}
|
||||
|
||||
const std::vector<std::array<int32_t, 6>> &
|
||||
AnimationNode::getCurrentAttackBoxes() const {
|
||||
auto clip = controller_.getClip();
|
||||
if (!clip || clip->empty())
|
||||
return emptyBoxes_;
|
||||
return clip->getFrame(controller_.getCurrentFrameIndex()).attackBoxes;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 查询
|
||||
// ============================================================================
|
||||
|
||||
Size AnimationNode::getMaxFrameSize() const {
|
||||
return frameRenderer_.getMaxFrameSize();
|
||||
}
|
||||
|
||||
Rect AnimationNode::getBoundingBox() const {
|
||||
Size size = frameRenderer_.getMaxFrameSize();
|
||||
Vec2 pos = getPosition();
|
||||
Vec2 anchor = getAnchor();
|
||||
return Rect{{pos.x - size.width * anchor.x, pos.y - size.height * anchor.y},
|
||||
size};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::onEnter() {
|
||||
Node::onEnter();
|
||||
if (autoPlay_ && controller_.getClip() && !controller_.getClip()->empty()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationNode::onExit() { Node::onExit(); }
|
||||
|
||||
void AnimationNode::onUpdate(float dt) {
|
||||
Node::onUpdate(dt);
|
||||
controller_.update(dt);
|
||||
}
|
||||
|
||||
void AnimationNode::onDraw(RenderBackend &renderer) {
|
||||
auto clip = controller_.getClip();
|
||||
if (!clip || clip->empty())
|
||||
return;
|
||||
|
||||
size_t idx = controller_.getCurrentFrameIndex();
|
||||
const auto &frame = clip->getFrame(idx);
|
||||
Vec2 pos = getPosition();
|
||||
|
||||
if (controller_.isInterpolating() && idx + 1 < clip->getFrameCount()) {
|
||||
auto props = InterpolationEngine::interpolate(
|
||||
frame, clip->getFrame(idx + 1), controller_.getInterpolationFactor());
|
||||
|
||||
frameRenderer_.renderInterpolated(renderer, frame, idx, props, pos,
|
||||
getOpacity(), tintColor_, flipX_, flipY_);
|
||||
} else {
|
||||
frameRenderer_.renderFrame(renderer, frame, idx, pos, getOpacity(),
|
||||
tintColor_, flipX_, flipY_);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 内部方法
|
||||
// ============================================================================
|
||||
|
||||
void AnimationNode::setupControllerCallbacks() {
|
||||
controller_.setFrameChangeCallback(
|
||||
[this](size_t oldIdx, size_t newIdx, const AnimationFrame &frame) {
|
||||
AnimationEvent evt;
|
||||
evt.type = AnimationEventType::FrameChanged;
|
||||
evt.frameIndex = newIdx;
|
||||
evt.previousFrameIndex = oldIdx;
|
||||
evt.source = this;
|
||||
dispatchEvent(evt);
|
||||
});
|
||||
|
||||
controller_.setSoundTriggerCallback([this](const std::string &path) {
|
||||
AnimationEvent evt;
|
||||
evt.type = AnimationEventType::SoundTrigger;
|
||||
evt.frameIndex = controller_.getCurrentFrameIndex();
|
||||
evt.soundPath = path;
|
||||
evt.source = this;
|
||||
dispatchEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
void AnimationNode::dispatchEvent(const AnimationEvent &event) {
|
||||
for (auto &listener : eventListeners_) {
|
||||
if (listener)
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
#include <extra2d/animation/animation_cache.h>
|
||||
#include <extra2d/animation/composite_animation.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 静态工厂
|
||||
// ============================================================================
|
||||
|
||||
Ptr<CompositeAnimation> CompositeAnimation::create() {
|
||||
return makePtr<CompositeAnimation>();
|
||||
}
|
||||
|
||||
Ptr<CompositeAnimation>
|
||||
CompositeAnimation::create(const std::string &alsFilePath) {
|
||||
auto comp = makePtr<CompositeAnimation>();
|
||||
comp->loadFromFile(alsFilePath);
|
||||
return comp;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 加载 ALS 文件
|
||||
// ============================================================================
|
||||
|
||||
bool CompositeAnimation::loadFromFile(const std::string &alsFilePath) {
|
||||
AlsParser parser;
|
||||
auto result = parser.parse(alsFilePath);
|
||||
if (!result.success)
|
||||
return false;
|
||||
|
||||
// 清除现有图层
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node) {
|
||||
removeChild(entry.node);
|
||||
}
|
||||
}
|
||||
layers_.clear();
|
||||
|
||||
// 创建每个图层
|
||||
for (const auto &layer : result.layers) {
|
||||
auto node = AnimationNode::create();
|
||||
if (node->loadFromFile(layer.aniPath)) {
|
||||
node->setPosition(layer.offset);
|
||||
addLayer(node, layer.zOrder);
|
||||
}
|
||||
}
|
||||
|
||||
return !layers_.empty();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 图层管理
|
||||
// ============================================================================
|
||||
|
||||
void CompositeAnimation::addLayer(Ptr<AnimationNode> node, int zOrder) {
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
node->setZOrder(zOrder);
|
||||
addChild(node);
|
||||
layers_.push_back({node, zOrder});
|
||||
}
|
||||
|
||||
void CompositeAnimation::removeLayer(size_t index) {
|
||||
if (index >= layers_.size())
|
||||
return;
|
||||
|
||||
auto &entry = layers_[index];
|
||||
if (entry.node) {
|
||||
removeChild(entry.node);
|
||||
}
|
||||
layers_.erase(layers_.begin() + static_cast<ptrdiff_t>(index));
|
||||
}
|
||||
|
||||
Ptr<AnimationNode> CompositeAnimation::getLayer(size_t index) const {
|
||||
if (index >= layers_.size())
|
||||
return nullptr;
|
||||
return layers_[index].node;
|
||||
}
|
||||
|
||||
Ptr<AnimationNode> CompositeAnimation::getMainLayer() const {
|
||||
if (layers_.empty())
|
||||
return nullptr;
|
||||
return layers_[0].node;
|
||||
}
|
||||
|
||||
size_t CompositeAnimation::getLayerCount() const { return layers_.size(); }
|
||||
|
||||
// ============================================================================
|
||||
// 统一播放控制
|
||||
// ============================================================================
|
||||
|
||||
void CompositeAnimation::play() {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->play();
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::pause() {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->pause();
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::resume() {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->resume();
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::stop() {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::reset() {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::setPlaybackSpeed(float speed) {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->setPlaybackSpeed(speed);
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::setLooping(bool loop) {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->setLooping(loop);
|
||||
}
|
||||
}
|
||||
|
||||
bool CompositeAnimation::isPlaying() const {
|
||||
auto main = getMainLayer();
|
||||
return main ? main->isPlaying() : false;
|
||||
}
|
||||
|
||||
bool CompositeAnimation::isStopped() const {
|
||||
auto main = getMainLayer();
|
||||
return main ? main->isStopped() : true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 事件回调(绑定到主图层)
|
||||
// ============================================================================
|
||||
|
||||
void CompositeAnimation::setKeyframeCallback(KeyframeHitCallback callback) {
|
||||
auto main = getMainLayer();
|
||||
if (main)
|
||||
main->setKeyframeCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void CompositeAnimation::setCompletionCallback(
|
||||
AnimationCompleteCallback callback) {
|
||||
auto main = getMainLayer();
|
||||
if (main)
|
||||
main->setCompletionCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void CompositeAnimation::addEventListener(AnimationEventCallback callback) {
|
||||
auto main = getMainLayer();
|
||||
if (main)
|
||||
main->addEventListener(std::move(callback));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 视觉属性(应用到所有图层)
|
||||
// ============================================================================
|
||||
|
||||
void CompositeAnimation::setTintColor(const Color &color) {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->setTintColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::setFlipX(bool flip) {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->setFlipX(flip);
|
||||
}
|
||||
}
|
||||
|
||||
void CompositeAnimation::setFlipY(bool flip) {
|
||||
for (auto &entry : layers_) {
|
||||
if (entry.node)
|
||||
entry.node->setFlipY(flip);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
#include <extra2d/animation/frame_property.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// FramePropertySet 实现
|
||||
// ============================================================================
|
||||
|
||||
void FramePropertySet::set(FramePropertyKey key, FramePropertyValue value) {
|
||||
properties_[key] = value;
|
||||
}
|
||||
|
||||
void FramePropertySet::set(FramePropertyKey key, const std::string& value) {
|
||||
FramePropertyValue pv;
|
||||
pv.type = PropertyValueType::String;
|
||||
pv.data.stringIndex = allocateString(value);
|
||||
properties_[key] = pv;
|
||||
}
|
||||
|
||||
void FramePropertySet::set(FramePropertyKey key, const std::vector<int>& value) {
|
||||
FramePropertyValue pv;
|
||||
pv.type = PropertyValueType::IntVector;
|
||||
pv.data.vectorIndex = allocateVector(value);
|
||||
properties_[key] = pv;
|
||||
}
|
||||
|
||||
void FramePropertySet::setCustom(const std::string &key, std::any value) {
|
||||
customProperties_[key] = std::move(value);
|
||||
}
|
||||
|
||||
bool FramePropertySet::has(FramePropertyKey key) const {
|
||||
return properties_.find(key) != properties_.end();
|
||||
}
|
||||
|
||||
bool FramePropertySet::hasCustom(const std::string &key) const {
|
||||
return customProperties_.find(key) != customProperties_.end();
|
||||
}
|
||||
|
||||
void FramePropertySet::remove(FramePropertyKey key) {
|
||||
properties_.erase(key);
|
||||
}
|
||||
|
||||
void FramePropertySet::removeCustom(const std::string &key) {
|
||||
customProperties_.erase(key);
|
||||
}
|
||||
|
||||
void FramePropertySet::clear() {
|
||||
properties_.clear();
|
||||
customProperties_.clear();
|
||||
stringPool_.clear();
|
||||
vectorPool_.clear();
|
||||
nextStringIndex_ = 0;
|
||||
nextVectorIndex_ = 0;
|
||||
}
|
||||
|
||||
std::optional<std::any> FramePropertySet::getCustom(const std::string &key) const {
|
||||
auto it = customProperties_.find(key);
|
||||
if (it == customProperties_.end())
|
||||
return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 字符串池和vector池管理
|
||||
// ============================================================================
|
||||
|
||||
uint32_t FramePropertySet::allocateString(const std::string& str) {
|
||||
// 查找是否已存在相同字符串
|
||||
for (uint32_t i = 0; i < stringPool_.size(); ++i) {
|
||||
if (stringPool_[i] == str) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// 分配新字符串
|
||||
uint32_t index = static_cast<uint32_t>(stringPool_.size());
|
||||
stringPool_.push_back(str);
|
||||
return index;
|
||||
}
|
||||
|
||||
uint32_t FramePropertySet::allocateVector(const std::vector<int>& vec) {
|
||||
// 查找是否已存在相同vector
|
||||
for (uint32_t i = 0; i < vectorPool_.size(); ++i) {
|
||||
if (vectorPool_[i] == vec) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// 分配新vector
|
||||
uint32_t index = static_cast<uint32_t>(vectorPool_.size());
|
||||
vectorPool_.push_back(vec);
|
||||
return index;
|
||||
}
|
||||
|
||||
const std::string* FramePropertySet::getString(uint32_t index) const {
|
||||
if (index < stringPool_.size()) {
|
||||
return &stringPool_[index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<int>* FramePropertySet::getVector(uint32_t index) const {
|
||||
if (index < vectorPool_.size()) {
|
||||
return &vectorPool_[index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 模板特化实现
|
||||
// ============================================================================
|
||||
|
||||
template <> std::optional<bool> FramePropertySet::get<bool>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::Bool) {
|
||||
return it->second.data.boolValue;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<int> FramePropertySet::get<int>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::Int) {
|
||||
return it->second.data.intValue;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<float> FramePropertySet::get<float>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::Float) {
|
||||
return it->second.data.floatValue;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<Vec2> FramePropertySet::get<Vec2>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::Vec2) {
|
||||
return it->second.data.vec2Value;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<Color> FramePropertySet::get<Color>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::Color) {
|
||||
return it->second.data.colorValue;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<std::string> FramePropertySet::get<std::string>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::String) {
|
||||
const std::string* str = getString(it->second.data.stringIndex);
|
||||
if (str) return *str;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <> std::optional<std::vector<int>> FramePropertySet::get<std::vector<int>>(FramePropertyKey key) const {
|
||||
auto it = properties_.find(key);
|
||||
if (it == properties_.end()) return std::nullopt;
|
||||
if (it->second.type == PropertyValueType::IntVector) {
|
||||
const std::vector<int>* vec = getVector(it->second.data.vectorIndex);
|
||||
if (vec) return *vec;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 链式 API 实现
|
||||
// ============================================================================
|
||||
|
||||
FramePropertySet &FramePropertySet::withSetFlag(int index) {
|
||||
set(FramePropertyKey::SetFlag, FramePropertyValue(index));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withPlaySound(const std::string &path) {
|
||||
set(FramePropertyKey::PlaySound, path);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withImageRate(const Vec2 &scale) {
|
||||
set(FramePropertyKey::ImageRate, FramePropertyValue(scale));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withImageRotate(float degrees) {
|
||||
set(FramePropertyKey::ImageRotate, FramePropertyValue(degrees));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withColorTint(const Color &color) {
|
||||
set(FramePropertyKey::ColorTint, FramePropertyValue(color));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withInterpolation(bool enabled) {
|
||||
set(FramePropertyKey::Interpolation, FramePropertyValue(enabled));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withBlendLinearDodge(bool enabled) {
|
||||
set(FramePropertyKey::BlendLinearDodge, FramePropertyValue(enabled));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FramePropertySet &FramePropertySet::withLoop(bool enabled) {
|
||||
set(FramePropertyKey::Loop, FramePropertyValue(enabled));
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
#include <extra2d/animation/frame_renderer.h>
|
||||
#include <extra2d/graphics/texture.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 预加载
|
||||
// ============================================================================
|
||||
|
||||
bool FrameRenderer::preloadFrames(const std::vector<AnimationFrame> &frames) {
|
||||
releaseFrames();
|
||||
spriteFrames_.reserve(frames.size());
|
||||
maxFrameSize_ = Size{0.0f, 0.0f};
|
||||
|
||||
for (const auto &frame : frames) {
|
||||
Ptr<SpriteFrame> sf = frame.spriteFrame;
|
||||
|
||||
// 如果帧自身没有 SpriteFrame,尝试通过缓存获取
|
||||
if (!sf && !frame.texturePath.empty()) {
|
||||
sf = SpriteFrameCache::getInstance().getOrCreateFromFile(
|
||||
frame.texturePath, frame.textureIndex);
|
||||
}
|
||||
|
||||
spriteFrames_.push_back(sf);
|
||||
|
||||
// 更新最大帧尺寸
|
||||
if (sf && sf->isValid()) {
|
||||
const auto &rect = sf->getRect();
|
||||
if (rect.size.width > maxFrameSize_.width)
|
||||
maxFrameSize_.width = rect.size.width;
|
||||
if (rect.size.height > maxFrameSize_.height)
|
||||
maxFrameSize_.height = rect.size.height;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameRenderer::releaseFrames() {
|
||||
spriteFrames_.clear();
|
||||
maxFrameSize_ = Size{0.0f, 0.0f};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 渲染当前帧
|
||||
// ============================================================================
|
||||
|
||||
void FrameRenderer::renderFrame(RenderBackend &renderer,
|
||||
const AnimationFrame &frame, size_t frameIndex,
|
||||
const Vec2 &position, float nodeOpacity,
|
||||
const Color &tintColor, bool flipX,
|
||||
bool flipY) {
|
||||
if (frameIndex >= spriteFrames_.size())
|
||||
return;
|
||||
|
||||
auto sf = spriteFrames_[frameIndex];
|
||||
if (!sf || !sf->isValid())
|
||||
return;
|
||||
|
||||
BlendMode blend = mapBlendMode(frame.properties);
|
||||
Vec2 scale = frame.getEffectiveScale();
|
||||
float rotation = frame.getEffectiveRotation();
|
||||
Color frameColor = frame.getEffectiveColor();
|
||||
|
||||
// 合并帧颜色和节点染色
|
||||
Color finalTint{tintColor.r * frameColor.r, tintColor.g * frameColor.g,
|
||||
tintColor.b * frameColor.b,
|
||||
tintColor.a * frameColor.a * nodeOpacity};
|
||||
|
||||
drawSpriteFrame(renderer, sf, position, frame.offset, scale, rotation, 1.0f,
|
||||
finalTint, flipX, flipY, blend);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 渲染插值帧
|
||||
// ============================================================================
|
||||
|
||||
void FrameRenderer::renderInterpolated(
|
||||
RenderBackend &renderer, const AnimationFrame &fromFrame, size_t fromIndex,
|
||||
const InterpolatedProperties &props, const Vec2 &position,
|
||||
float nodeOpacity, const Color &tintColor, bool flipX, bool flipY) {
|
||||
if (fromIndex >= spriteFrames_.size())
|
||||
return;
|
||||
|
||||
auto sf = spriteFrames_[fromIndex];
|
||||
if (!sf || !sf->isValid())
|
||||
return;
|
||||
|
||||
BlendMode blend = mapBlendMode(fromFrame.properties);
|
||||
|
||||
Color finalTint{tintColor.r * props.color.r, tintColor.g * props.color.g,
|
||||
tintColor.b * props.color.b,
|
||||
tintColor.a * props.color.a * nodeOpacity};
|
||||
|
||||
drawSpriteFrame(renderer, sf, position, props.position, props.scale,
|
||||
props.rotation, 1.0f, finalTint, flipX, flipY, blend);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 混合模式映射
|
||||
// ============================================================================
|
||||
|
||||
BlendMode FrameRenderer::mapBlendMode(const FramePropertySet &props) {
|
||||
if (props.has(FramePropertyKey::BlendAdditive)) {
|
||||
auto val = props.get<bool>(FramePropertyKey::BlendAdditive);
|
||||
if (val.has_value() && val.value())
|
||||
return BlendMode::Additive;
|
||||
}
|
||||
if (props.has(FramePropertyKey::BlendLinearDodge)) {
|
||||
auto val = props.get<bool>(FramePropertyKey::BlendLinearDodge);
|
||||
if (val.has_value() && val.value())
|
||||
return BlendMode::Additive; // 线性减淡 ≈ 加法混合
|
||||
}
|
||||
return BlendMode::Alpha;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 查询
|
||||
// ============================================================================
|
||||
|
||||
Ptr<SpriteFrame> FrameRenderer::getSpriteFrame(size_t frameIndex) const {
|
||||
if (frameIndex >= spriteFrames_.size())
|
||||
return nullptr;
|
||||
return spriteFrames_[frameIndex];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 内部绘制
|
||||
// ============================================================================
|
||||
|
||||
void FrameRenderer::drawSpriteFrame(RenderBackend &renderer,
|
||||
Ptr<SpriteFrame> sf, const Vec2 &position,
|
||||
const Vec2 &offset, const Vec2 &scale,
|
||||
float rotation, float opacity,
|
||||
const Color &tint, bool flipX, bool flipY,
|
||||
BlendMode blend) {
|
||||
if (!sf || !sf->isValid())
|
||||
return;
|
||||
|
||||
auto texture = sf->getTexture();
|
||||
if (!texture)
|
||||
return;
|
||||
|
||||
renderer.setBlendMode(blend);
|
||||
|
||||
const Rect &srcRect = sf->getRect();
|
||||
|
||||
// 计算目标矩形
|
||||
float w = srcRect.size.width * std::abs(scale.x);
|
||||
float h = srcRect.size.height * std::abs(scale.y);
|
||||
|
||||
Vec2 finalPos{position.x + offset.x, position.y + offset.y};
|
||||
|
||||
// 处理翻转(通过缩放符号)
|
||||
float flipScaleX = flipX ? -1.0f : 1.0f;
|
||||
float flipScaleY = flipY ? -1.0f : 1.0f;
|
||||
|
||||
// 锚点由 RenderBackend 在绘制时处理,这里只传递位置和尺寸
|
||||
// 使用中心锚点(0.5, 0.5),所以 destRect 从 finalPos 开始,不预偏移
|
||||
Rect destRect{{finalPos.x, finalPos.y}, {w * flipScaleX, h * flipScaleY}};
|
||||
|
||||
Color finalColor{tint.r, tint.g, tint.b, tint.a * opacity};
|
||||
|
||||
renderer.drawSprite(*texture, destRect, srcRect, finalColor, rotation,
|
||||
Vec2{0.5f, 0.5f});
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#include <extra2d/animation/interpolation_engine.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// InterpolationEngine 的实现全部在头文件中以静态内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#include <extra2d/animation/sprite_frame.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// SpriteFrame 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#include <extra2d/animation/sprite_frame_cache.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// SpriteFrameCache 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
#include <extra2d/action/action_manager.h>
|
||||
#include <extra2d/app/application.h>
|
||||
#include <extra2d/audio/audio_engine.h>
|
||||
#include <extra2d/event/event_dispatcher.h>
|
||||
|
|
@ -342,9 +341,6 @@ void Application::mainLoop() {
|
|||
}
|
||||
|
||||
void Application::update() {
|
||||
// Update action manager (single update per frame)
|
||||
ActionManager::getInstance()->update(deltaTime_);
|
||||
|
||||
if (timerManager_) {
|
||||
timerManager_->update(deltaTime_);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,918 +0,0 @@
|
|||
#include <extra2d/effects/custom_effect_manager.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <fstream>
|
||||
#include <json/json.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ============================================================================
|
||||
// JSON序列化辅助函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 将Vec2转换为JSON数组
|
||||
*/
|
||||
static json vec2ToJson(const Vec2 &v) { return json::array({v.x, v.y}); }
|
||||
|
||||
/**
|
||||
* @brief 从JSON数组解析Vec2
|
||||
*/
|
||||
static Vec2 jsonToVec2(const json &j) {
|
||||
if (j.is_array() && j.size() >= 2) {
|
||||
return Vec2(j[0].get<float>(), j[1].get<float>());
|
||||
}
|
||||
return Vec2();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将Color转换为JSON数组
|
||||
*/
|
||||
static json colorToJson(const Color &c) {
|
||||
return json::array({c.r, c.g, c.b, c.a});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从JSON数组解析Color
|
||||
*/
|
||||
static Color jsonToColor(const json &j) {
|
||||
if (j.is_array() && j.size() >= 4) {
|
||||
return Color(j[0].get<float>(), j[1].get<float>(), j[2].get<float>(),
|
||||
j[3].get<float>());
|
||||
}
|
||||
return Colors::White;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将EmitterConfig转换为JSON
|
||||
*/
|
||||
static json emitterConfigToJson(const EmitterConfig &config) {
|
||||
json j;
|
||||
j["emissionRate"] = config.emissionRate;
|
||||
j["life"] = json::array({config.minLife, config.maxLife});
|
||||
j["startSize"] = json::array({config.minStartSize, config.maxStartSize});
|
||||
j["endSize"] = json::array({config.minEndSize, config.maxEndSize});
|
||||
j["velocity"] = json::object({{"min", vec2ToJson(config.minVelocity)},
|
||||
{"max", vec2ToJson(config.maxVelocity)}});
|
||||
j["acceleration"] = vec2ToJson(config.acceleration);
|
||||
j["startColor"] = colorToJson(config.startColor);
|
||||
j["endColor"] = colorToJson(config.endColor);
|
||||
j["blendMode"] = static_cast<int>(config.blendMode);
|
||||
j["shape"] = static_cast<int>(config.shape);
|
||||
j["shapeRadius"] = config.shapeRadius;
|
||||
return j;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从JSON解析EmitterConfig
|
||||
*/
|
||||
static EmitterConfig jsonToEmitterConfig(const json &j) {
|
||||
EmitterConfig config;
|
||||
|
||||
if (j.contains("emissionRate")) {
|
||||
config.emissionRate = j["emissionRate"].get<float>();
|
||||
}
|
||||
if (j.contains("life") && j["life"].is_array() && j["life"].size() >= 2) {
|
||||
config.minLife = j["life"][0].get<float>();
|
||||
config.maxLife = j["life"][1].get<float>();
|
||||
}
|
||||
if (j.contains("startSize") && j["startSize"].is_array() &&
|
||||
j["startSize"].size() >= 2) {
|
||||
config.minStartSize = j["startSize"][0].get<float>();
|
||||
config.maxStartSize = j["startSize"][1].get<float>();
|
||||
}
|
||||
if (j.contains("endSize") && j["endSize"].is_array() &&
|
||||
j["endSize"].size() >= 2) {
|
||||
config.minEndSize = j["endSize"][0].get<float>();
|
||||
config.maxEndSize = j["endSize"][1].get<float>();
|
||||
}
|
||||
if (j.contains("velocity")) {
|
||||
const auto &vel = j["velocity"];
|
||||
if (vel.contains("min")) {
|
||||
config.minVelocity = jsonToVec2(vel["min"]);
|
||||
}
|
||||
if (vel.contains("max")) {
|
||||
config.maxVelocity = jsonToVec2(vel["max"]);
|
||||
}
|
||||
}
|
||||
if (j.contains("acceleration")) {
|
||||
config.acceleration = jsonToVec2(j["acceleration"]);
|
||||
}
|
||||
if (j.contains("startColor")) {
|
||||
config.startColor = jsonToColor(j["startColor"]);
|
||||
}
|
||||
if (j.contains("endColor")) {
|
||||
config.endColor = jsonToColor(j["endColor"]);
|
||||
}
|
||||
if (j.contains("blendMode")) {
|
||||
config.blendMode = static_cast<BlendMode>(j["blendMode"].get<int>());
|
||||
}
|
||||
if (j.contains("shape")) {
|
||||
config.shape = static_cast<EmitterConfig::Shape>(j["shape"].get<int>());
|
||||
}
|
||||
if (j.contains("shapeRadius")) {
|
||||
config.shapeRadius = j["shapeRadius"].get<float>();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将CustomEffectConfig转换为JSON
|
||||
*/
|
||||
static json effectConfigToJson(const CustomEffectConfig &config) {
|
||||
json j;
|
||||
j["name"] = config.name;
|
||||
j["type"] = static_cast<int>(config.type);
|
||||
j["description"] = config.description;
|
||||
j["duration"] = config.duration;
|
||||
j["loop"] = config.loop;
|
||||
j["delay"] = config.delay;
|
||||
|
||||
if (config.type == CustomEffectType::Particle) {
|
||||
j["emitter"] = emitterConfigToJson(config.emitterConfig);
|
||||
} else if (config.type == CustomEffectType::PostProcess) {
|
||||
j["shaderVert"] = config.shaderVertPath;
|
||||
j["shaderFrag"] = config.shaderFragPath;
|
||||
j["params"] = config.shaderParams;
|
||||
}
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从JSON解析CustomEffectConfig
|
||||
*/
|
||||
static CustomEffectConfig jsonToEffectConfig(const json &j) {
|
||||
CustomEffectConfig config;
|
||||
|
||||
if (j.contains("name")) {
|
||||
config.name = j["name"].get<std::string>();
|
||||
}
|
||||
if (j.contains("type")) {
|
||||
config.type = static_cast<CustomEffectType>(j["type"].get<int>());
|
||||
}
|
||||
if (j.contains("description")) {
|
||||
config.description = j["description"].get<std::string>();
|
||||
}
|
||||
if (j.contains("duration")) {
|
||||
config.duration = j["duration"].get<float>();
|
||||
}
|
||||
if (j.contains("loop")) {
|
||||
config.loop = j["loop"].get<bool>();
|
||||
}
|
||||
if (j.contains("delay")) {
|
||||
config.delay = j["delay"].get<float>();
|
||||
}
|
||||
if (j.contains("emitter")) {
|
||||
config.emitterConfig = jsonToEmitterConfig(j["emitter"]);
|
||||
}
|
||||
if (j.contains("shaderVert")) {
|
||||
config.shaderVertPath = j["shaderVert"].get<std::string>();
|
||||
}
|
||||
if (j.contains("shaderFrag")) {
|
||||
config.shaderFragPath = j["shaderFrag"].get<std::string>();
|
||||
}
|
||||
if (j.contains("params")) {
|
||||
for (auto &[key, value] : j["params"].items()) {
|
||||
config.shaderParams[key] = value.get<float>();
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomEffect实现
|
||||
// ============================================================================
|
||||
|
||||
CustomEffect::CustomEffect(const CustomEffectConfig &config)
|
||||
: config_(config) {}
|
||||
|
||||
bool CustomEffect::init() { return true; }
|
||||
|
||||
void CustomEffect::update(float dt) {
|
||||
if (!playing_ || paused_ || finished_)
|
||||
return;
|
||||
|
||||
// 处理延迟
|
||||
if (delayTimer_ < config_.delay) {
|
||||
delayTimer_ += dt;
|
||||
return;
|
||||
}
|
||||
|
||||
elapsedTime_ += dt;
|
||||
|
||||
// 检查是否结束
|
||||
if (config_.duration > 0 && elapsedTime_ >= config_.duration) {
|
||||
if (config_.loop) {
|
||||
elapsedTime_ = 0.0f;
|
||||
} else {
|
||||
finished_ = true;
|
||||
playing_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CustomEffect::render(RenderBackend &renderer) {
|
||||
// 基类不渲染任何内容
|
||||
}
|
||||
|
||||
void CustomEffect::shutdown() {
|
||||
playing_ = false;
|
||||
paused_ = false;
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
void CustomEffect::play() {
|
||||
if (finished_) {
|
||||
reset();
|
||||
}
|
||||
playing_ = true;
|
||||
paused_ = false;
|
||||
}
|
||||
|
||||
void CustomEffect::pause() { paused_ = true; }
|
||||
|
||||
void CustomEffect::stop() {
|
||||
playing_ = false;
|
||||
paused_ = false;
|
||||
}
|
||||
|
||||
void CustomEffect::reset() {
|
||||
elapsedTime_ = 0.0f;
|
||||
delayTimer_ = 0.0f;
|
||||
finished_ = false;
|
||||
playing_ = false;
|
||||
paused_ = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomParticleEffect实现
|
||||
// ============================================================================
|
||||
|
||||
CustomParticleEffect::CustomParticleEffect(const CustomEffectConfig &config)
|
||||
: CustomEffect(config) {}
|
||||
|
||||
bool CustomParticleEffect::init() {
|
||||
particleSystem_ = ParticleSystem::create();
|
||||
if (!particleSystem_) {
|
||||
E2D_ERROR("创建粒子系统失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
emitter_ = particleSystem_->addEmitter(config_.emitterConfig);
|
||||
if (!emitter_) {
|
||||
E2D_ERROR("创建粒子发射器失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化时启动发射器
|
||||
emitter_->start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CustomParticleEffect::play() {
|
||||
CustomEffect::play();
|
||||
if (emitter_) {
|
||||
emitter_->start();
|
||||
}
|
||||
}
|
||||
|
||||
void CustomParticleEffect::stop() {
|
||||
CustomEffect::stop();
|
||||
if (emitter_) {
|
||||
emitter_->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void CustomParticleEffect::update(float dt) {
|
||||
CustomEffect::update(dt);
|
||||
|
||||
if (particleSystem_) {
|
||||
particleSystem_->setPosition(position_);
|
||||
particleSystem_->onUpdate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomParticleEffect::render(RenderBackend &renderer) {
|
||||
if (particleSystem_) {
|
||||
particleSystem_->onDraw(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomParticleEffect::shutdown() {
|
||||
if (emitter_) {
|
||||
emitter_->stop();
|
||||
emitter_.reset();
|
||||
}
|
||||
if (particleSystem_) {
|
||||
particleSystem_->removeAllEmitters();
|
||||
particleSystem_.reset();
|
||||
}
|
||||
CustomEffect::shutdown();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomPostProcessEffect实现
|
||||
// ============================================================================
|
||||
|
||||
CustomPostProcessEffect::CustomPostProcessEffect(
|
||||
const CustomEffectConfig &config)
|
||||
: CustomEffect(config), PostProcessEffect(config.name) {}
|
||||
|
||||
bool CustomPostProcessEffect::init() {
|
||||
if (!config_.shaderVertPath.empty() && !config_.shaderFragPath.empty()) {
|
||||
if (!loadShaderFromFile(config_.shaderVertPath, config_.shaderFragPath)) {
|
||||
E2D_ERROR("加载后处理Shader失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
runtimeParams_ = config_.shaderParams;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CustomPostProcessEffect::update(float dt) { CustomEffect::update(dt); }
|
||||
|
||||
void CustomPostProcessEffect::shutdown() {
|
||||
PostProcessEffect::shutdown();
|
||||
CustomEffect::shutdown();
|
||||
}
|
||||
|
||||
void CustomPostProcessEffect::onShaderBind(GLShader &shader) {
|
||||
for (const auto &[name, value] : runtimeParams_) {
|
||||
shader.setFloat(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomPostProcessEffect::setParam(const std::string &name, float value) {
|
||||
runtimeParams_[name] = value;
|
||||
}
|
||||
|
||||
float CustomPostProcessEffect::getParam(const std::string &name) const {
|
||||
auto it = runtimeParams_.find(name);
|
||||
if (it != runtimeParams_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomEffectFactory实现
|
||||
// ============================================================================
|
||||
|
||||
CustomEffectFactory &CustomEffectFactory::getInstance() {
|
||||
static CustomEffectFactory instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void CustomEffectFactory::registerEffect(const std::string &typeName,
|
||||
EffectCreator creator) {
|
||||
creators_[typeName] = creator;
|
||||
E2D_INFO("注册自定义特效类型: {}", typeName);
|
||||
}
|
||||
|
||||
Ptr<CustomEffect>
|
||||
CustomEffectFactory::create(const std::string &typeName,
|
||||
const CustomEffectConfig &config) {
|
||||
auto it = creators_.find(typeName);
|
||||
if (it != creators_.end()) {
|
||||
return it->second(config);
|
||||
}
|
||||
|
||||
// 默认创建器
|
||||
if (typeName == "Particle") {
|
||||
return std::make_shared<CustomParticleEffect>(config);
|
||||
} else if (typeName == "PostProcess") {
|
||||
return std::make_shared<CustomPostProcessEffect>(config);
|
||||
}
|
||||
|
||||
E2D_ERROR("未知的特效类型: {}", typeName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool CustomEffectFactory::isRegistered(const std::string &typeName) const {
|
||||
return creators_.find(typeName) != creators_.end();
|
||||
}
|
||||
|
||||
std::vector<std::string> CustomEffectFactory::getRegisteredTypes() const {
|
||||
std::vector<std::string> types;
|
||||
for (const auto &[name, _] : creators_) {
|
||||
types.push_back(name);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomEffectManager实现
|
||||
// ============================================================================
|
||||
|
||||
CustomEffectManager &CustomEffectManager::getInstance() {
|
||||
static CustomEffectManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool CustomEffectManager::init() {
|
||||
E2D_INFO("初始化自定义特效管理器...");
|
||||
|
||||
// 注册默认特效类型
|
||||
auto &factory = E2D_CUSTOM_EFFECT_FACTORY();
|
||||
factory.registerEffect("Particle", [](const CustomEffectConfig &config) {
|
||||
return std::make_shared<CustomParticleEffect>(config);
|
||||
});
|
||||
factory.registerEffect("PostProcess", [](const CustomEffectConfig &config) {
|
||||
return std::make_shared<CustomPostProcessEffect>(config);
|
||||
});
|
||||
|
||||
E2D_INFO("自定义特效管理器初始化完成");
|
||||
return true;
|
||||
}
|
||||
|
||||
void CustomEffectManager::shutdown() {
|
||||
E2D_INFO("关闭自定义特效管理器...");
|
||||
stopAll();
|
||||
activeEffects_.clear();
|
||||
configs_.clear();
|
||||
}
|
||||
|
||||
bool CustomEffectManager::loadFromFile(const std::string &filepath) {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_ERROR("无法打开特效配置文件: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为JSON
|
||||
json j;
|
||||
file >> j;
|
||||
file.close();
|
||||
|
||||
if (j.is_array()) {
|
||||
// 多个特效配置数组
|
||||
for (const auto &effectJson : j) {
|
||||
auto config = jsonToEffectConfig(effectJson);
|
||||
if (!config.name.empty()) {
|
||||
registerConfig(config.name, config);
|
||||
}
|
||||
}
|
||||
} else if (j.is_object()) {
|
||||
// 单个特效配置
|
||||
auto config = jsonToEffectConfig(j);
|
||||
if (!config.name.empty()) {
|
||||
registerConfig(config.name, config);
|
||||
}
|
||||
}
|
||||
|
||||
E2D_INFO("从JSON文件加载特效配置: {}", filepath);
|
||||
return true;
|
||||
|
||||
} catch (const json::exception &e) {
|
||||
// JSON解析失败,回退到文本格式
|
||||
file.close();
|
||||
return loadFromTextFile(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
bool CustomEffectManager::loadFromTextFile(const std::string &filepath) {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_ERROR("无法打开特效配置文件: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简化格式:每行一个特效配置
|
||||
// 格式: EFFECT name type
|
||||
// PARAM key value
|
||||
// END
|
||||
|
||||
std::string line;
|
||||
CustomEffectConfig currentConfig;
|
||||
bool inEffect = false;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
// 跳过空行和注释
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
std::istringstream iss(line);
|
||||
std::string cmd;
|
||||
iss >> cmd;
|
||||
|
||||
if (cmd == "EFFECT") {
|
||||
// 开始新特效
|
||||
if (inEffect) {
|
||||
// 保存上一个特效
|
||||
registerConfig(currentConfig.name, currentConfig);
|
||||
}
|
||||
inEffect = true;
|
||||
currentConfig = CustomEffectConfig();
|
||||
|
||||
std::string type;
|
||||
iss >> currentConfig.name >> type;
|
||||
|
||||
if (type == "Particle") {
|
||||
currentConfig.type = CustomEffectType::Particle;
|
||||
} else if (type == "PostProcess") {
|
||||
currentConfig.type = CustomEffectType::PostProcess;
|
||||
} else {
|
||||
currentConfig.type = CustomEffectType::Particle;
|
||||
}
|
||||
} else if (cmd == "DESC") {
|
||||
std::getline(iss, currentConfig.description);
|
||||
// 去除前导空格
|
||||
if (!currentConfig.description.empty() &&
|
||||
currentConfig.description[0] == ' ') {
|
||||
currentConfig.description = currentConfig.description.substr(1);
|
||||
}
|
||||
} else if (cmd == "DURATION") {
|
||||
iss >> currentConfig.duration;
|
||||
} else if (cmd == "LOOP") {
|
||||
std::string val;
|
||||
iss >> val;
|
||||
currentConfig.loop = (val == "true" || val == "1");
|
||||
} else if (cmd == "EMISSION") {
|
||||
iss >> currentConfig.emitterConfig.emissionRate;
|
||||
} else if (cmd == "LIFE") {
|
||||
iss >> currentConfig.emitterConfig.minLife >>
|
||||
currentConfig.emitterConfig.maxLife;
|
||||
} else if (cmd == "SIZE_START") {
|
||||
iss >> currentConfig.emitterConfig.minStartSize >>
|
||||
currentConfig.emitterConfig.maxStartSize;
|
||||
} else if (cmd == "SIZE_END") {
|
||||
iss >> currentConfig.emitterConfig.minEndSize >>
|
||||
currentConfig.emitterConfig.maxEndSize;
|
||||
} else if (cmd == "VELOCITY") {
|
||||
iss >> currentConfig.emitterConfig.minVelocity.x >>
|
||||
currentConfig.emitterConfig.minVelocity.y >>
|
||||
currentConfig.emitterConfig.maxVelocity.x >>
|
||||
currentConfig.emitterConfig.maxVelocity.y;
|
||||
} else if (cmd == "ACCEL") {
|
||||
iss >> currentConfig.emitterConfig.acceleration.x >>
|
||||
currentConfig.emitterConfig.acceleration.y;
|
||||
} else if (cmd == "COLOR_START") {
|
||||
iss >> currentConfig.emitterConfig.startColor.r >>
|
||||
currentConfig.emitterConfig.startColor.g >>
|
||||
currentConfig.emitterConfig.startColor.b >>
|
||||
currentConfig.emitterConfig.startColor.a;
|
||||
} else if (cmd == "COLOR_END") {
|
||||
iss >> currentConfig.emitterConfig.endColor.r >>
|
||||
currentConfig.emitterConfig.endColor.g >>
|
||||
currentConfig.emitterConfig.endColor.b >>
|
||||
currentConfig.emitterConfig.endColor.a;
|
||||
} else if (cmd == "BLEND") {
|
||||
std::string mode;
|
||||
iss >> mode;
|
||||
if (mode == "Additive") {
|
||||
currentConfig.emitterConfig.blendMode = BlendMode::Additive;
|
||||
} else if (mode == "Alpha") {
|
||||
currentConfig.emitterConfig.blendMode = BlendMode::Alpha;
|
||||
} else {
|
||||
currentConfig.emitterConfig.blendMode = BlendMode::None;
|
||||
}
|
||||
} else if (cmd == "END") {
|
||||
// 结束当前特效
|
||||
if (inEffect) {
|
||||
registerConfig(currentConfig.name, currentConfig);
|
||||
inEffect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个特效
|
||||
if (inEffect) {
|
||||
registerConfig(currentConfig.name, currentConfig);
|
||||
}
|
||||
|
||||
file.close();
|
||||
E2D_INFO("从文本文件加载特效配置: {}", filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CustomEffectManager::saveToFile(const std::string &name,
|
||||
const std::string &filepath,
|
||||
bool useJson) {
|
||||
auto it = configs_.find(name);
|
||||
if (it == configs_.end()) {
|
||||
E2D_ERROR("特效配置不存在: {}", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_ERROR("无法创建文件: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useJson) {
|
||||
// 保存为JSON格式
|
||||
json j = effectConfigToJson(it->second);
|
||||
file << j.dump(2); // 缩进2个空格,更易读
|
||||
E2D_INFO("保存特效配置到JSON文件: {}", filepath);
|
||||
} else {
|
||||
// 保存为文本格式
|
||||
const auto &config = it->second;
|
||||
|
||||
file << "# Easy2D Custom Effect Config\n";
|
||||
file << "# Generated automatically\n\n";
|
||||
|
||||
file << "EFFECT " << config.name << " ";
|
||||
if (config.type == CustomEffectType::Particle) {
|
||||
file << "Particle\n";
|
||||
} else if (config.type == CustomEffectType::PostProcess) {
|
||||
file << "PostProcess\n";
|
||||
} else {
|
||||
file << "Particle\n";
|
||||
}
|
||||
|
||||
file << "DESC " << config.description << "\n";
|
||||
file << "DURATION " << config.duration << "\n";
|
||||
file << "LOOP " << (config.loop ? "true" : "false") << "\n";
|
||||
|
||||
if (config.type == CustomEffectType::Particle) {
|
||||
const auto &ec = config.emitterConfig;
|
||||
file << "EMISSION " << ec.emissionRate << "\n";
|
||||
file << "LIFE " << ec.minLife << " " << ec.maxLife << "\n";
|
||||
file << "SIZE_START " << ec.minStartSize << " " << ec.maxStartSize
|
||||
<< "\n";
|
||||
file << "SIZE_END " << ec.minEndSize << " " << ec.maxEndSize << "\n";
|
||||
file << "VELOCITY " << ec.minVelocity.x << " " << ec.minVelocity.y << " "
|
||||
<< ec.maxVelocity.x << " " << ec.maxVelocity.y << "\n";
|
||||
file << "ACCEL " << ec.acceleration.x << " " << ec.acceleration.y << "\n";
|
||||
file << "COLOR_START " << ec.startColor.r << " " << ec.startColor.g << " "
|
||||
<< ec.startColor.b << " " << ec.startColor.a << "\n";
|
||||
file << "COLOR_END " << ec.endColor.r << " " << ec.endColor.g << " "
|
||||
<< ec.endColor.b << " " << ec.endColor.a << "\n";
|
||||
|
||||
file << "BLEND ";
|
||||
switch (ec.blendMode) {
|
||||
case BlendMode::Additive:
|
||||
file << "Additive\n";
|
||||
break;
|
||||
case BlendMode::Alpha:
|
||||
file << "Alpha\n";
|
||||
break;
|
||||
default:
|
||||
file << "None\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file << "END\n";
|
||||
E2D_INFO("保存特效配置到文本文件: {}", filepath);
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CustomEffectManager::saveAllToFile(const std::string &filepath) {
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_ERROR("无法创建文件: {}", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
json effectsArray = json::array();
|
||||
for (const auto &[name, config] : configs_) {
|
||||
effectsArray.push_back(effectConfigToJson(config));
|
||||
}
|
||||
|
||||
file << effectsArray.dump(2);
|
||||
file.close();
|
||||
|
||||
E2D_INFO("保存所有特效配置到: {}", filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CustomEffectManager::registerConfig(const std::string &name,
|
||||
const CustomEffectConfig &config) {
|
||||
configs_[name] = config;
|
||||
E2D_INFO("注册特效配置: {}", name);
|
||||
}
|
||||
|
||||
CustomEffectConfig *CustomEffectManager::getConfig(const std::string &name) {
|
||||
auto it = configs_.find(name);
|
||||
if (it != configs_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CustomEffectManager::removeConfig(const std::string &name) {
|
||||
configs_.erase(name);
|
||||
}
|
||||
|
||||
std::vector<std::string> CustomEffectManager::getConfigNames() const {
|
||||
std::vector<std::string> names;
|
||||
for (const auto &[name, _] : configs_) {
|
||||
names.push_back(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
Ptr<CustomEffect> CustomEffectManager::createEffect(const std::string &name) {
|
||||
auto config = getConfig(name);
|
||||
if (!config) {
|
||||
E2D_ERROR("特效配置不存在: {}", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return createEffectFromConfig(*config);
|
||||
}
|
||||
|
||||
Ptr<CustomEffect>
|
||||
CustomEffectManager::createEffectFromConfig(const CustomEffectConfig &config) {
|
||||
std::string typeName;
|
||||
switch (config.type) {
|
||||
case CustomEffectType::Particle:
|
||||
typeName = "Particle";
|
||||
break;
|
||||
case CustomEffectType::PostProcess:
|
||||
typeName = "PostProcess";
|
||||
break;
|
||||
default:
|
||||
typeName = "Particle";
|
||||
break;
|
||||
}
|
||||
|
||||
auto effect = E2D_CUSTOM_EFFECT_FACTORY().create(typeName, config);
|
||||
if (effect && effect->init()) {
|
||||
activeEffects_.push_back(effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CustomEffectManager::destroyEffect(Ptr<CustomEffect> effect) {
|
||||
if (!effect)
|
||||
return;
|
||||
|
||||
effect->shutdown();
|
||||
|
||||
auto it = std::find(activeEffects_.begin(), activeEffects_.end(), effect);
|
||||
if (it != activeEffects_.end()) {
|
||||
activeEffects_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomEffectManager::update(float dt) {
|
||||
for (auto &effect : activeEffects_) {
|
||||
if (effect->isPlaying()) {
|
||||
effect->update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已完成的特效
|
||||
activeEffects_.erase(std::remove_if(activeEffects_.begin(),
|
||||
activeEffects_.end(),
|
||||
[](const Ptr<CustomEffect> &effect) {
|
||||
return effect->isFinished();
|
||||
}),
|
||||
activeEffects_.end());
|
||||
}
|
||||
|
||||
void CustomEffectManager::render(RenderBackend &renderer) {
|
||||
for (auto &effect : activeEffects_) {
|
||||
if (effect->isPlaying()) {
|
||||
effect->render(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CustomEffectManager::stopAll() {
|
||||
for (auto &effect : activeEffects_) {
|
||||
effect->stop();
|
||||
}
|
||||
}
|
||||
|
||||
Ptr<CustomEffect> CustomEffectManager::play(const std::string &name,
|
||||
const Vec2 &position) {
|
||||
auto effect = createEffect(name);
|
||||
if (effect) {
|
||||
effect->setPosition(position);
|
||||
effect->play();
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
|
||||
void CustomEffectManager::playOneShot(const std::string &name,
|
||||
const Vec2 &position) {
|
||||
auto effect = play(name, position);
|
||||
if (effect) {
|
||||
// 设置非循环,播放一次后自动销毁
|
||||
effect->play();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EffectBuilder实现
|
||||
// ============================================================================
|
||||
|
||||
CustomEffectConfig EffectBuilder::Particle(const std::string &name) {
|
||||
CustomEffectConfig config;
|
||||
config.name = name;
|
||||
config.type = CustomEffectType::Particle;
|
||||
config.duration = -1.0f;
|
||||
config.loop = true;
|
||||
config.delay = 0.0f;
|
||||
|
||||
// 默认粒子配置
|
||||
config.emitterConfig.emissionRate = 100.0f;
|
||||
config.emitterConfig.minLife = 1.0f;
|
||||
config.emitterConfig.maxLife = 2.0f;
|
||||
config.emitterConfig.minStartSize = 10.0f;
|
||||
config.emitterConfig.maxStartSize = 20.0f;
|
||||
config.emitterConfig.minEndSize = 0.0f;
|
||||
config.emitterConfig.maxEndSize = 5.0f;
|
||||
config.emitterConfig.minVelocity = Vec2(-50.0f, -50.0f);
|
||||
config.emitterConfig.maxVelocity = Vec2(50.0f, 50.0f);
|
||||
config.emitterConfig.acceleration = Vec2(0.0f, 0.0f);
|
||||
config.emitterConfig.startColor = Colors::White;
|
||||
config.emitterConfig.endColor = Colors::Transparent;
|
||||
config.emitterConfig.blendMode = BlendMode::Additive;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Fire(const std::string &name) {
|
||||
CustomEffectConfig config = Particle(name);
|
||||
config.emitterConfig = ParticlePreset::Fire();
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Smoke(const std::string &name) {
|
||||
CustomEffectConfig config = Particle(name);
|
||||
config.emitterConfig = ParticlePreset::Smoke();
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Explosion(const std::string &name) {
|
||||
CustomEffectConfig config = Particle(name);
|
||||
config.emitterConfig = ParticlePreset::Explosion();
|
||||
config.duration = 2.0f;
|
||||
config.loop = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Magic(const std::string &name) {
|
||||
CustomEffectConfig config = Particle(name);
|
||||
config.emitterConfig = ParticlePreset::Magic();
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Sparkle(const std::string &name) {
|
||||
CustomEffectConfig config = Particle(name);
|
||||
config.emitterConfig = ParticlePreset::Sparkle();
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Bloom(const std::string &name) {
|
||||
CustomEffectConfig config;
|
||||
config.name = name;
|
||||
config.type = CustomEffectType::PostProcess;
|
||||
config.duration = -1.0f;
|
||||
config.loop = true;
|
||||
config.shaderParams["intensity"] = 1.5f;
|
||||
config.shaderParams["threshold"] = 0.8f;
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Blur(const std::string &name) {
|
||||
CustomEffectConfig config;
|
||||
config.name = name;
|
||||
config.type = CustomEffectType::PostProcess;
|
||||
config.duration = -1.0f;
|
||||
config.loop = true;
|
||||
config.shaderParams["radius"] = 2.0f;
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::Vignette(const std::string &name) {
|
||||
CustomEffectConfig config;
|
||||
config.name = name;
|
||||
config.type = CustomEffectType::PostProcess;
|
||||
config.duration = -1.0f;
|
||||
config.loop = true;
|
||||
config.shaderParams["intensity"] = 0.5f;
|
||||
return config;
|
||||
}
|
||||
|
||||
CustomEffectConfig EffectBuilder::ColorGrading(const std::string &name) {
|
||||
CustomEffectConfig config;
|
||||
config.name = name;
|
||||
config.type = CustomEffectType::PostProcess;
|
||||
config.duration = -1.0f;
|
||||
config.loop = true;
|
||||
config.shaderParams["brightness"] = 1.0f;
|
||||
config.shaderParams["contrast"] = 1.0f;
|
||||
config.shaderParams["saturation"] = 1.0f;
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,468 +0,0 @@
|
|||
#include <cmath>
|
||||
#include <extra2d/effects/particle_system.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 形状生成函数类型和查找表
|
||||
// ============================================================================
|
||||
using ShapeGenerator = Vec2 (ParticleEmitter::*)();
|
||||
|
||||
// 形状生成函数指针数组,索引对应 EmitterConfig::Shape 枚举值
|
||||
static constexpr ShapeGenerator SHAPE_GENERATORS[] = {
|
||||
&ParticleEmitter::randomPointShape, // Shape::Point = 0
|
||||
&ParticleEmitter::randomCircleShape, // Shape::Circle = 1
|
||||
&ParticleEmitter::randomRectangleShape, // Shape::Rectangle = 2
|
||||
&ParticleEmitter::randomConeShape // Shape::Cone = 3
|
||||
};
|
||||
|
||||
static constexpr size_t SHAPE_GENERATOR_COUNT = sizeof(SHAPE_GENERATORS) / sizeof(SHAPE_GENERATORS[0]);
|
||||
|
||||
// ============================================================================
|
||||
// ParticleEmitter实现
|
||||
// ============================================================================
|
||||
|
||||
ParticleEmitter::ParticleEmitter() : rng_(static_cast<uint32_t>(std::random_device{}())) {}
|
||||
|
||||
bool ParticleEmitter::init(size_t maxParticles) {
|
||||
particles_.resize(maxParticles);
|
||||
activeCount_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParticleEmitter::shutdown() {
|
||||
particles_.clear();
|
||||
activeCount_ = 0;
|
||||
}
|
||||
|
||||
void ParticleEmitter::start() {
|
||||
emitting_ = true;
|
||||
emissionTime_ = 0.0f;
|
||||
}
|
||||
|
||||
void ParticleEmitter::stop() { emitting_ = false; }
|
||||
|
||||
void ParticleEmitter::burst(int count) {
|
||||
for (int i = 0; i < count && activeCount_ < particles_.size(); ++i) {
|
||||
emitParticle();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleEmitter::reset() {
|
||||
for (auto &particle : particles_) {
|
||||
particle.active = false;
|
||||
}
|
||||
activeCount_ = 0;
|
||||
emissionTimer_ = 0.0f;
|
||||
emissionTime_ = 0.0f;
|
||||
}
|
||||
|
||||
void ParticleEmitter::update(float dt) {
|
||||
// 发射新粒子
|
||||
if (emitting_) {
|
||||
emissionTime_ += dt;
|
||||
|
||||
// 检查持续时间
|
||||
if (config_.emissionDuration > 0 &&
|
||||
emissionTime_ >= config_.emissionDuration) {
|
||||
emitting_ = false;
|
||||
}
|
||||
|
||||
emissionTimer_ += dt;
|
||||
float emissionInterval = 1.0f / config_.emissionRate;
|
||||
|
||||
while (emissionTimer_ >= emissionInterval &&
|
||||
activeCount_ < particles_.size()) {
|
||||
emitParticle();
|
||||
emissionTimer_ -= emissionInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新活跃粒子
|
||||
size_t newActiveCount = 0;
|
||||
for (size_t i = 0; i < activeCount_; ++i) {
|
||||
auto &p = particles_[i];
|
||||
|
||||
if (!p.active)
|
||||
continue;
|
||||
|
||||
// 更新生命周期
|
||||
p.life -= dt;
|
||||
if (p.life <= 0.0f) {
|
||||
p.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 更新物理
|
||||
p.velocity += p.acceleration * dt;
|
||||
p.position += p.velocity * dt;
|
||||
p.rotation += p.angularVelocity * dt;
|
||||
|
||||
// 更新大小
|
||||
p.size += p.sizeDelta * dt;
|
||||
if (p.size < 0.0f)
|
||||
p.size = 0.0f;
|
||||
|
||||
// 更新颜色
|
||||
p.color += p.colorDelta * dt;
|
||||
|
||||
// 保持活跃
|
||||
if (newActiveCount != i) {
|
||||
particles_[newActiveCount] = p;
|
||||
}
|
||||
newActiveCount++;
|
||||
}
|
||||
|
||||
activeCount_ = newActiveCount;
|
||||
}
|
||||
|
||||
void ParticleEmitter::render(RenderBackend &renderer) {
|
||||
if (activeCount_ == 0)
|
||||
return;
|
||||
|
||||
// 设置混合模式
|
||||
renderer.setBlendMode(config_.blendMode);
|
||||
|
||||
// 渲染所有活跃粒子
|
||||
if (config_.texture) {
|
||||
// 使用纹理批量渲染
|
||||
renderer.beginSpriteBatch();
|
||||
|
||||
for (size_t i = 0; i < activeCount_; ++i) {
|
||||
const auto &p = particles_[i];
|
||||
if (!p.active)
|
||||
continue;
|
||||
|
||||
// 计算目标矩形
|
||||
// 锚点由 RenderBackend 在绘制时处理,这里只传递位置和尺寸
|
||||
Rect destRect(p.position.x, p.position.y, p.size, p.size);
|
||||
|
||||
renderer.drawSprite(
|
||||
*config_.texture, destRect,
|
||||
Rect(0, 0, config_.texture->getWidth(), config_.texture->getHeight()),
|
||||
p.color, p.rotation, Vec2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
renderer.endSpriteBatch();
|
||||
} else {
|
||||
// 没有纹理,使用圆形填充渲染
|
||||
for (size_t i = 0; i < activeCount_; ++i) {
|
||||
const auto &p = particles_[i];
|
||||
if (!p.active)
|
||||
continue;
|
||||
|
||||
// 渲染圆形粒子
|
||||
renderer.fillCircle(p.position, p.size * 0.5f, p.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleEmitter::emitParticle() {
|
||||
if (activeCount_ >= particles_.size())
|
||||
return;
|
||||
|
||||
Particle &p = particles_[activeCount_];
|
||||
p.active = true;
|
||||
|
||||
// 位置
|
||||
p.position = position_ + randomPointInShape();
|
||||
|
||||
// 速度
|
||||
p.velocity = randomVelocity();
|
||||
|
||||
// 加速度
|
||||
p.acceleration = config_.acceleration;
|
||||
|
||||
// 旋转
|
||||
p.rotation = randomFloat(config_.minRotation, config_.maxRotation);
|
||||
p.angularVelocity =
|
||||
randomFloat(config_.minAngularVelocity, config_.maxAngularVelocity);
|
||||
|
||||
// 大小
|
||||
float startSize = randomFloat(config_.minStartSize, config_.maxStartSize);
|
||||
float endSize = randomFloat(config_.minEndSize, config_.maxEndSize);
|
||||
p.size = startSize;
|
||||
|
||||
// 生命周期
|
||||
p.maxLife = randomFloat(config_.minLife, config_.maxLife);
|
||||
p.life = p.maxLife;
|
||||
|
||||
// 计算每帧变化量
|
||||
if (p.maxLife > 0.0f) {
|
||||
p.sizeDelta = (endSize - startSize) / p.maxLife;
|
||||
p.colorDelta = (config_.endColor - config_.startColor) / p.maxLife;
|
||||
}
|
||||
|
||||
// 颜色
|
||||
p.color = config_.startColor;
|
||||
|
||||
activeCount_++;
|
||||
}
|
||||
|
||||
float ParticleEmitter::randomFloat(float min, float max) {
|
||||
return rng_.nextFloat(min, max);
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomPointInShape() {
|
||||
// 使用查找表替代 switch
|
||||
size_t shapeIndex = static_cast<size_t>(config_.shape);
|
||||
if (shapeIndex < SHAPE_GENERATOR_COUNT) {
|
||||
return (this->*SHAPE_GENERATORS[shapeIndex])();
|
||||
}
|
||||
return Vec2::Zero();
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomPointShape() {
|
||||
return Vec2::Zero();
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomCircleShape() {
|
||||
float angle = randomFloat(0.0f, 2.0f * 3.14159265359f);
|
||||
float radius = randomFloat(0.0f, config_.shapeRadius);
|
||||
return Vec2(std::cos(angle) * radius, std::sin(angle) * radius);
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomRectangleShape() {
|
||||
return Vec2(
|
||||
randomFloat(-config_.shapeSize.x * 0.5f, config_.shapeSize.x * 0.5f),
|
||||
randomFloat(-config_.shapeSize.y * 0.5f, config_.shapeSize.y * 0.5f));
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomConeShape() {
|
||||
float angle =
|
||||
randomFloat(-config_.coneAngle * 0.5f, config_.coneAngle * 0.5f);
|
||||
float radius = randomFloat(0.0f, config_.shapeRadius);
|
||||
float rad = angle * 3.14159265359f / 180.0f;
|
||||
return Vec2(std::cos(rad) * radius, std::sin(rad) * radius);
|
||||
}
|
||||
|
||||
Vec2 ParticleEmitter::randomVelocity() {
|
||||
return Vec2(randomFloat(config_.minVelocity.x, config_.maxVelocity.x),
|
||||
randomFloat(config_.minVelocity.y, config_.maxVelocity.y));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ParticleSystem实现
|
||||
// ============================================================================
|
||||
|
||||
ParticleSystem::ParticleSystem() {}
|
||||
|
||||
Ptr<ParticleSystem> ParticleSystem::create() {
|
||||
return std::make_shared<ParticleSystem>();
|
||||
}
|
||||
|
||||
Ptr<ParticleEmitter> ParticleSystem::addEmitter(const EmitterConfig &config) {
|
||||
auto emitter = std::make_shared<ParticleEmitter>();
|
||||
emitter->setConfig(config);
|
||||
emitter->init(1000); // 默认最大1000个粒子
|
||||
emitters_.push_back(emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
void ParticleSystem::removeEmitter(Ptr<ParticleEmitter> emitter) {
|
||||
auto it = std::find(emitters_.begin(), emitters_.end(), emitter);
|
||||
if (it != emitters_.end()) {
|
||||
(*it)->shutdown();
|
||||
emitters_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::removeAllEmitters() {
|
||||
for (auto &emitter : emitters_) {
|
||||
emitter->shutdown();
|
||||
}
|
||||
emitters_.clear();
|
||||
}
|
||||
|
||||
void ParticleSystem::startAll() {
|
||||
for (auto &emitter : emitters_) {
|
||||
emitter->start();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::stopAll() {
|
||||
for (auto &emitter : emitters_) {
|
||||
emitter->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::resetAll() {
|
||||
for (auto &emitter : emitters_) {
|
||||
emitter->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::onUpdate(float dt) {
|
||||
// 获取粒子系统的世界位置
|
||||
auto worldPos = convertToWorldSpace(Vec2::Zero());
|
||||
|
||||
for (auto &emitter : emitters_) {
|
||||
// 更新发射器位置为粒子系统的世界位置
|
||||
emitter->setPosition(worldPos);
|
||||
// 更新发射器
|
||||
emitter->update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::onDraw(RenderBackend &renderer) {
|
||||
for (auto &emitter : emitters_) {
|
||||
emitter->render(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 预设实现
|
||||
// ============================================================================
|
||||
|
||||
EmitterConfig ParticlePreset::Fire() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 200.0f;
|
||||
config.minLife = 0.5f;
|
||||
config.maxLife = 1.5f;
|
||||
config.minStartSize = 20.0f;
|
||||
config.maxStartSize = 40.0f;
|
||||
config.minEndSize = 5.0f;
|
||||
config.maxEndSize = 10.0f;
|
||||
config.minVelocity = Vec2(-30.0f, -150.0f); // 向上(负y)
|
||||
config.maxVelocity = Vec2(30.0f, -50.0f);
|
||||
config.acceleration = Vec2(0.0f, 0.0f);
|
||||
config.startColor = Color(1.0f, 0.8f, 0.2f, 1.0f); // 黄色
|
||||
config.endColor = Color(1.0f, 0.2f, 0.0f, 0.0f); // 红色透明
|
||||
config.blendMode = BlendMode::Additive;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Smoke() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 50.0f;
|
||||
config.minLife = 2.0f;
|
||||
config.maxLife = 4.0f;
|
||||
config.minStartSize = 30.0f;
|
||||
config.maxStartSize = 60.0f;
|
||||
config.minEndSize = 80.0f;
|
||||
config.maxEndSize = 120.0f;
|
||||
config.minVelocity = Vec2(-20.0f, -60.0f); // 向上(负y)
|
||||
config.maxVelocity = Vec2(20.0f, -30.0f);
|
||||
config.acceleration = Vec2(0.0f, -10.0f); // 向上加速度
|
||||
config.startColor = Color(0.5f, 0.5f, 0.5f, 0.5f); // 灰色半透明
|
||||
config.endColor = Color(0.3f, 0.3f, 0.3f, 0.0f); // 深灰透明
|
||||
config.blendMode = BlendMode::Alpha;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Explosion() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 1000.0f;
|
||||
config.emissionDuration = 0.1f; // 瞬间爆发
|
||||
config.minLife = 0.5f;
|
||||
config.maxLife = 1.5f;
|
||||
config.minStartSize = 10.0f;
|
||||
config.maxStartSize = 30.0f;
|
||||
config.minEndSize = 0.0f;
|
||||
config.maxEndSize = 5.0f;
|
||||
config.minVelocity = Vec2(-300.0f, -300.0f);
|
||||
config.maxVelocity = Vec2(300.0f, 300.0f);
|
||||
config.acceleration = Vec2(0.0f, -50.0f);
|
||||
config.startColor = Color(1.0f, 1.0f, 0.5f, 1.0f); // 亮黄
|
||||
config.endColor = Color(1.0f, 0.3f, 0.0f, 0.0f); // 橙红透明
|
||||
config.blendMode = BlendMode::Additive;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Sparkle() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 20.0f;
|
||||
config.minLife = 0.2f;
|
||||
config.maxLife = 0.8f;
|
||||
config.minStartSize = 2.0f;
|
||||
config.maxStartSize = 5.0f;
|
||||
config.minEndSize = 0.0f;
|
||||
config.maxEndSize = 2.0f;
|
||||
config.minVelocity = Vec2(-10.0f, -10.0f);
|
||||
config.maxVelocity = Vec2(10.0f, 10.0f);
|
||||
config.acceleration = Vec2(0.0f, 0.0f);
|
||||
config.startColor = Color(1.0f, 1.0f, 1.0f, 1.0f); // 白色
|
||||
config.endColor = Color(1.0f, 1.0f, 1.0f, 0.0f); // 透明
|
||||
config.blendMode = BlendMode::Additive;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Rain() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 500.0f;
|
||||
config.minLife = 1.0f;
|
||||
config.maxLife = 2.0f;
|
||||
config.minStartSize = 2.0f;
|
||||
config.maxStartSize = 4.0f;
|
||||
config.minEndSize = 2.0f;
|
||||
config.maxEndSize = 4.0f;
|
||||
config.minVelocity = Vec2(-100.0f, -400.0f);
|
||||
config.maxVelocity = Vec2(100.0f, -600.0f);
|
||||
config.acceleration = Vec2(0.0f, -100.0f);
|
||||
config.startColor = Color(0.7f, 0.8f, 1.0f, 0.6f); // 淡蓝
|
||||
config.endColor = Color(0.7f, 0.8f, 1.0f, 0.3f); // 淡蓝半透明
|
||||
config.shape = EmitterConfig::Shape::Rectangle;
|
||||
config.shapeSize = Vec2(800.0f, 100.0f); // 在顶部区域发射
|
||||
config.blendMode = BlendMode::Alpha;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Snow() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 100.0f;
|
||||
config.minLife = 3.0f;
|
||||
config.maxLife = 6.0f;
|
||||
config.minStartSize = 5.0f;
|
||||
config.maxStartSize = 10.0f;
|
||||
config.minEndSize = 5.0f;
|
||||
config.maxEndSize = 10.0f;
|
||||
config.minVelocity = Vec2(-30.0f, -30.0f);
|
||||
config.maxVelocity = Vec2(30.0f, -80.0f);
|
||||
config.acceleration = Vec2(0.0f, 0.0f);
|
||||
config.startColor = Color(1.0f, 1.0f, 1.0f, 0.8f); // 白色
|
||||
config.endColor = Color(1.0f, 1.0f, 1.0f, 0.8f); // 白色
|
||||
config.shape = EmitterConfig::Shape::Rectangle;
|
||||
config.shapeSize = Vec2(800.0f, 100.0f);
|
||||
config.blendMode = BlendMode::Alpha;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Magic() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 100.0f;
|
||||
config.minLife = 1.0f;
|
||||
config.maxLife = 2.0f;
|
||||
config.minStartSize = 5.0f;
|
||||
config.maxStartSize = 15.0f;
|
||||
config.minEndSize = 0.0f;
|
||||
config.maxEndSize = 5.0f;
|
||||
config.minVelocity = Vec2(-50.0f, -50.0f); // 主要向上
|
||||
config.maxVelocity = Vec2(50.0f, -50.0f);
|
||||
config.acceleration = Vec2(0.0f, -20.0f); // 向上加速度
|
||||
config.startColor = Color(0.5f, 0.2f, 1.0f, 1.0f); // 紫色
|
||||
config.endColor = Color(0.2f, 0.8f, 1.0f, 0.0f); // 青色透明
|
||||
config.blendMode = BlendMode::Additive;
|
||||
return config;
|
||||
}
|
||||
|
||||
EmitterConfig ParticlePreset::Bubbles() {
|
||||
EmitterConfig config;
|
||||
config.emissionRate = 30.0f;
|
||||
config.minLife = 2.0f;
|
||||
config.maxLife = 4.0f;
|
||||
config.minStartSize = 5.0f;
|
||||
config.maxStartSize = 15.0f;
|
||||
config.minEndSize = 5.0f;
|
||||
config.maxEndSize = 15.0f;
|
||||
config.minVelocity = Vec2(-20.0f, 20.0f);
|
||||
config.maxVelocity = Vec2(20.0f, 60.0f);
|
||||
config.acceleration = Vec2(0.0f, 30.0f);
|
||||
config.startColor = Color(0.8f, 0.9f, 1.0f, 0.4f); // 淡蓝透明
|
||||
config.endColor = Color(0.8f, 0.9f, 1.0f, 0.1f); // 更透明
|
||||
config.blendMode = BlendMode::Alpha;
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,389 +0,0 @@
|
|||
#include <extra2d/effects/post_process.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <extra2d/graphics/render_target.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 静态成员初始化
|
||||
// ============================================================================
|
||||
GLuint PostProcessEffect::quadVao_ = 0;
|
||||
GLuint PostProcessEffect::quadVbo_ = 0;
|
||||
bool PostProcessEffect::quadInitialized_ = false;
|
||||
|
||||
// ============================================================================
|
||||
// PostProcessEffect实现
|
||||
// ============================================================================
|
||||
|
||||
PostProcessEffect::PostProcessEffect(const std::string &name) : name_(name) {}
|
||||
|
||||
bool PostProcessEffect::init() {
|
||||
initQuad();
|
||||
valid_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PostProcessEffect::shutdown() {
|
||||
shader_.reset();
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void PostProcessEffect::apply(const Texture &source, RenderTarget &target,
|
||||
RenderBackend &renderer) {
|
||||
if (!enabled_ || !valid_)
|
||||
return;
|
||||
|
||||
target.bind();
|
||||
|
||||
if (shader_) {
|
||||
shader_->bind();
|
||||
shader_->setInt("u_texture", 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D,
|
||||
static_cast<GLuint>(
|
||||
reinterpret_cast<uintptr_t>(source.getNativeHandle())));
|
||||
|
||||
onShaderBind(*shader_);
|
||||
}
|
||||
|
||||
renderFullscreenQuad();
|
||||
|
||||
if (shader_) {
|
||||
shader_->unbind();
|
||||
}
|
||||
|
||||
target.unbind();
|
||||
}
|
||||
|
||||
bool PostProcessEffect::loadShader(const std::string &vertSource,
|
||||
const std::string &fragSource) {
|
||||
shader_ = std::make_shared<GLShader>();
|
||||
if (!shader_->compileFromSource(vertSource.c_str(), fragSource.c_str())) {
|
||||
E2D_ERROR("后处理效果 '{}' 加载Shader失败", name_);
|
||||
shader_.reset();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostProcessEffect::loadShaderFromFile(const std::string &vertPath,
|
||||
const std::string &fragPath) {
|
||||
shader_ = std::make_shared<GLShader>();
|
||||
if (!shader_->compileFromFile(vertPath, fragPath)) {
|
||||
E2D_ERROR("后处理效果 '{}' 从文件加载Shader失败", name_);
|
||||
shader_.reset();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PostProcessEffect::initQuad() {
|
||||
if (quadInitialized_)
|
||||
return;
|
||||
|
||||
// 全屏四边形顶点数据(位置和纹理坐标)
|
||||
float quadVertices[] = {// 位置 // 纹理坐标
|
||||
-1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f,
|
||||
1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f,
|
||||
1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
glGenVertexArrays(1, &quadVao_);
|
||||
glGenBuffers(1, &quadVbo_);
|
||||
|
||||
glBindVertexArray(quadVao_);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, quadVbo_);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices,
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// 位置属性
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
|
||||
|
||||
// 纹理坐标属性
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
|
||||
(void *)(2 * sizeof(float)));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
quadInitialized_ = true;
|
||||
}
|
||||
|
||||
void PostProcessEffect::destroyQuad() {
|
||||
if (quadVao_ != 0) {
|
||||
glDeleteVertexArrays(1, &quadVao_);
|
||||
quadVao_ = 0;
|
||||
}
|
||||
if (quadVbo_ != 0) {
|
||||
glDeleteBuffers(1, &quadVbo_);
|
||||
quadVbo_ = 0;
|
||||
}
|
||||
quadInitialized_ = false;
|
||||
}
|
||||
|
||||
void PostProcessEffect::renderFullscreenQuad() {
|
||||
if (!quadInitialized_) {
|
||||
initQuad();
|
||||
}
|
||||
|
||||
glBindVertexArray(quadVao_);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PostProcessStack实现
|
||||
// ============================================================================
|
||||
|
||||
PostProcessStack::PostProcessStack() = default;
|
||||
|
||||
PostProcessStack::~PostProcessStack() { shutdown(); }
|
||||
|
||||
bool PostProcessStack::init(int width, int height) {
|
||||
E2D_INFO("初始化后处理栈...");
|
||||
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
// 创建两个渲染目标用于乒乓渲染
|
||||
RenderTargetConfig config;
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
config.hasDepthBuffer = false;
|
||||
config.autoResize = false;
|
||||
|
||||
renderTargetA_ = std::make_shared<RenderTarget>();
|
||||
renderTargetB_ = std::make_shared<RenderTarget>();
|
||||
|
||||
if (!renderTargetA_->init(config)) {
|
||||
E2D_ERROR("创建后处理渲染目标A失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!renderTargetB_->init(config)) {
|
||||
E2D_ERROR("创建后处理渲染目标B失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
E2D_INFO("后处理栈初始化成功");
|
||||
return true;
|
||||
}
|
||||
|
||||
void PostProcessStack::shutdown() {
|
||||
E2D_INFO("关闭后处理栈...");
|
||||
|
||||
clearEffects();
|
||||
|
||||
if (renderTargetA_) {
|
||||
renderTargetA_->shutdown();
|
||||
renderTargetA_.reset();
|
||||
}
|
||||
|
||||
if (renderTargetB_) {
|
||||
renderTargetB_->shutdown();
|
||||
renderTargetB_.reset();
|
||||
}
|
||||
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void PostProcessStack::addEffect(Ptr<PostProcessEffect> effect) {
|
||||
if (effect && effect->init()) {
|
||||
effects_.push_back(effect);
|
||||
E2D_INFO("添加后处理效果: {}", effect->getName());
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessStack::insertEffect(size_t index,
|
||||
Ptr<PostProcessEffect> effect) {
|
||||
if (effect && effect->init() && index <= effects_.size()) {
|
||||
effects_.insert(effects_.begin() + index, effect);
|
||||
E2D_INFO("插入后处理效果 '{}' 到位置 {}", effect->getName(), index);
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessStack::removeEffect(const std::string &name) {
|
||||
for (auto it = effects_.begin(); it != effects_.end(); ++it) {
|
||||
if ((*it)->getName() == name) {
|
||||
(*it)->shutdown();
|
||||
effects_.erase(it);
|
||||
E2D_INFO("移除后处理效果: {}", name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessStack::removeEffect(size_t index) {
|
||||
if (index < effects_.size()) {
|
||||
effects_[index]->shutdown();
|
||||
effects_.erase(effects_.begin() + index);
|
||||
}
|
||||
}
|
||||
|
||||
Ptr<PostProcessEffect> PostProcessStack::getEffect(const std::string &name) {
|
||||
for (auto &effect : effects_) {
|
||||
if (effect->getName() == name) {
|
||||
return effect;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ptr<PostProcessEffect> PostProcessStack::getEffect(size_t index) {
|
||||
if (index < effects_.size()) {
|
||||
return effects_[index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PostProcessStack::clearEffects() {
|
||||
for (auto &effect : effects_) {
|
||||
effect->shutdown();
|
||||
}
|
||||
effects_.clear();
|
||||
}
|
||||
|
||||
void PostProcessStack::beginCapture() {
|
||||
if (!valid_)
|
||||
return;
|
||||
|
||||
renderTargetA_->bind();
|
||||
renderTargetA_->clear(Colors::Black);
|
||||
capturing_ = true;
|
||||
}
|
||||
|
||||
void PostProcessStack::endCapture(RenderBackend &renderer) {
|
||||
if (!valid_ || !capturing_)
|
||||
return;
|
||||
|
||||
renderTargetA_->unbind();
|
||||
|
||||
// 应用所有后处理效果
|
||||
if (effects_.empty()) {
|
||||
// 没有效果,直接渲染到屏幕
|
||||
// 这里需要渲染renderTargetA_的纹理到屏幕
|
||||
capturing_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 乒乓渲染
|
||||
RenderTarget *readTarget = renderTargetA_.get();
|
||||
RenderTarget *writeTarget = renderTargetB_.get();
|
||||
|
||||
for (size_t i = 0; i < effects_.size(); ++i) {
|
||||
auto &effect = effects_[i];
|
||||
|
||||
if (effect->isEnabled()) {
|
||||
effect->apply(*readTarget->getColorTexture(), *writeTarget, renderer);
|
||||
}
|
||||
|
||||
// 交换读写目标
|
||||
std::swap(readTarget, writeTarget);
|
||||
}
|
||||
|
||||
// 最终结果在readTarget中(因为最后一次交换)
|
||||
// 这里应该将结果渲染到屏幕
|
||||
|
||||
capturing_ = false;
|
||||
}
|
||||
|
||||
void PostProcessStack::process(const Texture &source, RenderTarget &target,
|
||||
RenderBackend &renderer) {
|
||||
if (!valid_)
|
||||
return;
|
||||
|
||||
RenderTarget *readTarget = nullptr;
|
||||
RenderTarget *writeTarget = nullptr;
|
||||
|
||||
// 确定读写目标
|
||||
if (target.getFBO() == renderTargetA_->getFBO()) {
|
||||
readTarget = renderTargetB_.get();
|
||||
writeTarget = renderTargetA_.get();
|
||||
} else {
|
||||
readTarget = renderTargetA_.get();
|
||||
writeTarget = renderTargetB_.get();
|
||||
}
|
||||
|
||||
// 首先将源纹理复制到读目标
|
||||
readTarget->bind();
|
||||
// 这里需要渲染源纹理到readTarget
|
||||
readTarget->unbind();
|
||||
|
||||
// 应用效果
|
||||
for (auto &effect : effects_) {
|
||||
if (effect->isEnabled()) {
|
||||
effect->apply(*readTarget->getColorTexture(), *writeTarget, renderer);
|
||||
}
|
||||
std::swap(readTarget, writeTarget);
|
||||
}
|
||||
|
||||
// 将最终结果复制到目标
|
||||
readTarget->blitTo(target, true, false);
|
||||
}
|
||||
|
||||
void PostProcessStack::resize(int width, int height) {
|
||||
if (width_ == width && height_ == height)
|
||||
return;
|
||||
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
if (renderTargetA_) {
|
||||
renderTargetA_->resize(width, height);
|
||||
}
|
||||
|
||||
if (renderTargetB_) {
|
||||
renderTargetB_->resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PostProcessManager实现
|
||||
// ============================================================================
|
||||
|
||||
PostProcessManager &PostProcessManager::getInstance() {
|
||||
static PostProcessManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void PostProcessManager::init(int width, int height) {
|
||||
if (initialized_)
|
||||
return;
|
||||
|
||||
E2D_INFO("初始化后处理管理器...");
|
||||
mainStack_.init(width, height);
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
void PostProcessManager::shutdown() {
|
||||
if (!initialized_)
|
||||
return;
|
||||
|
||||
E2D_INFO("关闭后处理管理器...");
|
||||
mainStack_.shutdown();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
void PostProcessManager::resize(int width, int height) {
|
||||
if (initialized_) {
|
||||
mainStack_.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessManager::beginFrame() {
|
||||
if (initialized_) {
|
||||
mainStack_.beginCapture();
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessManager::endFrame(RenderBackend &renderer) {
|
||||
if (initialized_) {
|
||||
mainStack_.endCapture(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <extra2d/action/action.h>
|
||||
#include <extra2d/action/action_manager.h>
|
||||
#include <extra2d/graphics/render_command.h>
|
||||
#include <extra2d/scene/node.h>
|
||||
#include <extra2d/scene/scene.h>
|
||||
|
|
@ -13,8 +11,6 @@ Node::Node() = default;
|
|||
|
||||
Node::~Node() {
|
||||
removeAllChildren();
|
||||
stopAllActions();
|
||||
ActionManager::getInstance()->removeAllActionsFromTarget(this);
|
||||
}
|
||||
|
||||
void Node::addChild(Ptr<Node> child) {
|
||||
|
|
@ -403,46 +399,6 @@ void Node::updateSpatialIndex() {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 动作系统 - 新接口
|
||||
// ============================================================================
|
||||
|
||||
Action* Node::runAction(Action* action) {
|
||||
if (!action) {
|
||||
return nullptr;
|
||||
}
|
||||
ActionManager::getInstance()->addAction(action, this);
|
||||
return action;
|
||||
}
|
||||
|
||||
void Node::stopAllActions() {
|
||||
ActionManager::getInstance()->removeAllActionsFromTarget(this);
|
||||
}
|
||||
|
||||
void Node::stopAction(Action* action) {
|
||||
ActionManager::getInstance()->removeAction(action);
|
||||
}
|
||||
|
||||
void Node::stopActionByTag(int tag) {
|
||||
ActionManager::getInstance()->removeActionByTag(tag, this);
|
||||
}
|
||||
|
||||
void Node::stopActionsByFlags(unsigned int flags) {
|
||||
ActionManager::getInstance()->removeActionsByFlags(flags, this);
|
||||
}
|
||||
|
||||
Action* Node::getActionByTag(int tag) {
|
||||
return ActionManager::getInstance()->getActionByTag(tag, this);
|
||||
}
|
||||
|
||||
size_t Node::getActionCount() const {
|
||||
return ActionManager::getInstance()->getActionCount(const_cast<Node*>(this));
|
||||
}
|
||||
|
||||
bool Node::isRunningActions() const {
|
||||
return getActionCount() > 0;
|
||||
}
|
||||
|
||||
void Node::update(float dt) { onUpdate(dt); }
|
||||
|
||||
void Node::render(RenderBackend &renderer) {
|
||||
|
|
|
|||
|
|
@ -1,279 +0,0 @@
|
|||
#include <cmath>
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
using namespace extra2d;
|
||||
|
||||
// ============================================================================
|
||||
// 碰撞测试节点 - 有实际边界框
|
||||
// ============================================================================
|
||||
class CollisionBox : public Node {
|
||||
public:
|
||||
CollisionBox(float width, float height, const Color &color)
|
||||
: width_(width), height_(height), color_(color), isColliding_(false) {
|
||||
// 启用空间索引,这是碰撞检测的关键
|
||||
setSpatialIndexed(true);
|
||||
}
|
||||
|
||||
void setColliding(bool colliding) { isColliding_ = colliding; }
|
||||
|
||||
Rect getBoundingBox() const override {
|
||||
// 返回实际的矩形边界
|
||||
Vec2 pos = getPosition();
|
||||
return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_);
|
||||
}
|
||||
|
||||
void onRender(RenderBackend &renderer) override {
|
||||
Vec2 pos = getPosition();
|
||||
|
||||
// 绘制填充矩形
|
||||
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_;
|
||||
renderer.fillRect(
|
||||
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
|
||||
fillColor);
|
||||
|
||||
// 绘制边框
|
||||
Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f)
|
||||
: Color(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
float borderWidth = isColliding_ ? 3.0f : 2.0f;
|
||||
renderer.drawRect(
|
||||
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
|
||||
borderColor, borderWidth);
|
||||
}
|
||||
|
||||
private:
|
||||
float width_, height_;
|
||||
Color color_;
|
||||
bool isColliding_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 碰撞检测场景
|
||||
// ============================================================================
|
||||
class CollisionDemoScene : public Scene {
|
||||
public:
|
||||
void onEnter() override {
|
||||
E2D_LOG_INFO("CollisionDemoScene::onEnter - 碰撞检测演示");
|
||||
|
||||
// 设置背景色
|
||||
setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f));
|
||||
|
||||
// 获取屏幕中心
|
||||
auto &app = Application::instance();
|
||||
float centerX = app.getConfig().width / 2.0f;
|
||||
float centerY = app.getConfig().height / 2.0f;
|
||||
|
||||
// 创建静态碰撞框
|
||||
createStaticBoxes(centerX, centerY);
|
||||
|
||||
// 创建移动的中心方块
|
||||
centerBox_ =
|
||||
makePtr<CollisionBox>(80.0f, 80.0f, Color(0.2f, 0.6f, 1.0f, 0.8f));
|
||||
centerBox_->setPosition(Vec2(centerX, centerY));
|
||||
addChild(centerBox_);
|
||||
|
||||
// 加载字体并创建UI
|
||||
loadFonts();
|
||||
|
||||
E2D_LOG_INFO("创建了 {} 个碰撞框", boxes_.size() + 1);
|
||||
}
|
||||
|
||||
void onUpdate(float dt) override {
|
||||
Scene::onUpdate(dt);
|
||||
|
||||
// 旋转中心方块
|
||||
rotationAngle_ += rotationSpeed_ * dt;
|
||||
if (rotationAngle_ >= 360.0f)
|
||||
rotationAngle_ -= 360.0f;
|
||||
|
||||
// 让中心方块沿圆形路径移动
|
||||
float radius = 150.0f;
|
||||
float rad = rotationAngle_ * 3.14159f / 180.0f;
|
||||
|
||||
auto &app = Application::instance();
|
||||
Vec2 center =
|
||||
Vec2(app.getConfig().width / 2.0f, app.getConfig().height / 2.0f);
|
||||
|
||||
centerBox_->setPosition(Vec2(center.x + std::cos(rad) * radius,
|
||||
center.y + std::sin(rad) * radius));
|
||||
centerBox_->setRotation(rotationAngle_);
|
||||
|
||||
// 执行碰撞检测
|
||||
performCollisionDetection();
|
||||
|
||||
// 更新UI文本
|
||||
updateUI();
|
||||
|
||||
// 检查退出按键
|
||||
auto &input = Application::instance().input();
|
||||
if (input.isButtonPressed(GamepadButton::Start)) {
|
||||
E2D_LOG_INFO("退出应用");
|
||||
Application::instance().quit();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 加载字体资源并创建UI文本
|
||||
*/
|
||||
void loadFonts() {
|
||||
auto &resources = Application::instance().resources();
|
||||
titleFont_ = resources.loadFont("assets/font.ttf", 60, true);
|
||||
infoFont_ = resources.loadFont("assets/font.ttf", 28, true);
|
||||
|
||||
// 创建标题文本
|
||||
titleText_ = Text::create("碰撞检测演示", titleFont_);
|
||||
titleText_->setPosition(50.0f, 30.0f);
|
||||
titleText_->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
addChild(titleText_);
|
||||
|
||||
// 创建说明文本
|
||||
descText_ = Text::create("蓝色方块旋转并检测碰撞", infoFont_);
|
||||
descText_->setPosition(50.0f, 80.0f);
|
||||
descText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
|
||||
addChild(descText_);
|
||||
|
||||
collideHintText_ = Text::create("红色 = 检测到碰撞", infoFont_);
|
||||
collideHintText_->setPosition(50.0f, 105.0f);
|
||||
collideHintText_->setTextColor(Color(1.0f, 0.5f, 0.5f, 1.0f));
|
||||
addChild(collideHintText_);
|
||||
|
||||
// 创建动态统计文本
|
||||
collisionText_ = Text::create("", infoFont_);
|
||||
collisionText_->setPosition(50.0f, 150.0f);
|
||||
collisionText_->setTextColor(Color(1.0f, 1.0f, 0.5f, 1.0f));
|
||||
addChild(collisionText_);
|
||||
|
||||
fpsText_ = Text::create("", infoFont_);
|
||||
fpsText_->setPosition(50.0f, 175.0f);
|
||||
fpsText_->setTextColor(Color(0.8f, 1.0f, 0.8f, 1.0f));
|
||||
addChild(fpsText_);
|
||||
|
||||
// 创建退出提示文本
|
||||
float screenHeight = static_cast<float>(Application::instance().getConfig().height);
|
||||
exitHintText_ = Text::create("按 + 键退出", infoFont_);
|
||||
exitHintText_->setPosition(50.0f, screenHeight - 50.0f);
|
||||
exitHintText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
|
||||
addChild(exitHintText_);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新UI文本
|
||||
*/
|
||||
void updateUI() {
|
||||
auto &app = Application::instance();
|
||||
|
||||
// 使用 setFormat 更新动态文本
|
||||
collisionText_->setFormat("碰撞数: %zu", collisionCount_);
|
||||
fpsText_->setFormat("FPS: %u", app.fps());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建静态碰撞框
|
||||
*/
|
||||
void createStaticBoxes(float centerX, float centerY) {
|
||||
// 创建围绕中心的静态碰撞框
|
||||
std::vector<std::pair<Vec2, Color>> positions = {
|
||||
{Vec2(centerX - 200, centerY - 150), Color(0.3f, 1.0f, 0.3f, 0.7f)},
|
||||
{Vec2(centerX + 200, centerY - 150), Color(1.0f, 0.3f, 0.3f, 0.7f)},
|
||||
{Vec2(centerX - 200, centerY + 150), Color(0.3f, 0.3f, 1.0f, 0.7f)},
|
||||
{Vec2(centerX + 200, centerY + 150), Color(1.0f, 1.0f, 0.3f, 0.7f)},
|
||||
{Vec2(centerX, centerY - 220), Color(1.0f, 0.3f, 1.0f, 0.7f)},
|
||||
{Vec2(centerX, centerY + 220), Color(0.3f, 1.0f, 1.0f, 0.7f)},
|
||||
};
|
||||
|
||||
for (const auto &[pos, color] : positions) {
|
||||
auto box = makePtr<CollisionBox>(70.0f, 70.0f, color);
|
||||
box->setPosition(pos);
|
||||
addChild(box);
|
||||
boxes_.push_back(box);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 执行碰撞检测
|
||||
*/
|
||||
void performCollisionDetection() {
|
||||
// 清除之前的碰撞状态
|
||||
centerBox_->setColliding(false);
|
||||
for (auto &box : boxes_) {
|
||||
box->setColliding(false);
|
||||
}
|
||||
|
||||
// 使用空间索引进行碰撞检测
|
||||
auto collisions = queryCollisions();
|
||||
|
||||
collisionCount_ = collisions.size();
|
||||
|
||||
// 标记碰撞的节点
|
||||
for (const auto &[nodeA, nodeB] : collisions) {
|
||||
if (auto boxA = dynamic_cast<CollisionBox *>(nodeA)) {
|
||||
boxA->setColliding(true);
|
||||
}
|
||||
if (auto boxB = dynamic_cast<CollisionBox *>(nodeB)) {
|
||||
boxB->setColliding(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ptr<CollisionBox> centerBox_;
|
||||
std::vector<Ptr<CollisionBox>> boxes_;
|
||||
float rotationAngle_ = 0.0f;
|
||||
float rotationSpeed_ = 60.0f; // 旋转速度(度/秒)
|
||||
size_t collisionCount_ = 0;
|
||||
|
||||
// 字体资源
|
||||
Ptr<FontAtlas> titleFont_;
|
||||
Ptr<FontAtlas> infoFont_;
|
||||
|
||||
// UI 文本组件
|
||||
Ptr<Text> titleText_;
|
||||
Ptr<Text> descText_;
|
||||
Ptr<Text> collideHintText_;
|
||||
Ptr<Text> collisionText_;
|
||||
Ptr<Text> fpsText_;
|
||||
Ptr<Text> exitHintText_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 程序入口
|
||||
// ============================================================================
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// 初始化日志系统
|
||||
Logger::init();
|
||||
Logger::setLevel(LogLevel::Debug);
|
||||
|
||||
E2D_LOG_INFO("========================");
|
||||
E2D_LOG_INFO("Easy2D 碰撞检测演示");
|
||||
E2D_LOG_INFO("========================");
|
||||
|
||||
// 获取应用实例
|
||||
auto &app = Application::instance();
|
||||
|
||||
// 配置应用
|
||||
AppConfig config;
|
||||
config.title = "Easy2D - 碰撞检测演示";
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.vsync = true;
|
||||
config.fpsLimit = 60;
|
||||
|
||||
// 初始化应用
|
||||
if (!app.init(config)) {
|
||||
E2D_LOG_ERROR("应用初始化失败!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 进入场景
|
||||
app.enterScene(makePtr<CollisionDemoScene>());
|
||||
|
||||
E2D_LOG_INFO("开始主循环...");
|
||||
|
||||
// 运行应用
|
||||
app.run();
|
||||
|
||||
E2D_LOG_INFO("应用结束");
|
||||
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,76 +0,0 @@
|
|||
-- ==============================================
|
||||
-- Collision Demo 示例 - Xmake 构建脚本
|
||||
-- 支持平台: MinGW (Windows), Nintendo Switch
|
||||
-- ==============================================
|
||||
|
||||
-- 获取当前脚本所在目录(示例根目录)
|
||||
local example_dir = os.scriptdir()
|
||||
|
||||
-- 可执行文件目标
|
||||
target("collision_demo")
|
||||
set_kind("binary")
|
||||
add_files("main.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
if is_plat("switch") then
|
||||
set_targetdir("../../build/examples/collision_demo")
|
||||
|
||||
-- 构建后生成 NRO 文件
|
||||
after_build(function (target)
|
||||
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||
local elf_file = target:targetfile()
|
||||
local output_dir = path.directory(elf_file)
|
||||
local nacp_file = path.join(output_dir, "collision_demo.nacp")
|
||||
local nro_file = path.join(output_dir, "collision_demo.nro")
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
if os.isfile(nacptool) and os.isfile(elf2nro) then
|
||||
os.vrunv(nacptool, {"--create", "Collision Demo", "Extra2D Team", "1.0.0", nacp_file})
|
||||
local romfs = path.join(example_dir, "romfs")
|
||||
if os.isdir(romfs) then
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
|
||||
else
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||
end
|
||||
print("Generated NRO: " .. nro_file)
|
||||
end
|
||||
end)
|
||||
|
||||
-- 打包时将 NRO 文件复制到 package 目录
|
||||
after_package(function (target)
|
||||
local nro_file = path.join(target:targetdir(), "collision_demo.nro")
|
||||
local package_dir = target:packagedir()
|
||||
if os.isfile(nro_file) and package_dir then
|
||||
os.cp(nro_file, package_dir)
|
||||
print("Copied NRO to package: " .. package_dir)
|
||||
end
|
||||
end)
|
||||
|
||||
elseif is_plat("mingw") then
|
||||
set_targetdir("../../build/examples/collision_demo")
|
||||
add_ldflags("-mwindows", {force = true})
|
||||
|
||||
-- 复制资源到输出目录
|
||||
after_build(function (target)
|
||||
local romfs = path.join(example_dir, "romfs")
|
||||
if os.isdir(romfs) then
|
||||
local target_dir = path.directory(target:targetfile())
|
||||
local assets_dir = path.join(target_dir, "assets")
|
||||
|
||||
-- 创建 assets 目录
|
||||
if not os.isdir(assets_dir) then
|
||||
os.mkdir(assets_dir)
|
||||
end
|
||||
|
||||
-- 复制所有资源文件(包括子目录)
|
||||
os.cp(path.join(romfs, "assets/**"), assets_dir)
|
||||
print("Copied assets from " .. romfs .. " to " .. assets_dir)
|
||||
else
|
||||
print("Warning: romfs directory not found at " .. romfs)
|
||||
end
|
||||
end)
|
||||
end
|
||||
target_end()
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
// ============================================================================
|
||||
// BaseScene.cpp - Flappy Bird 基础场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "BaseScene.h"
|
||||
#include <extra2d/scene/transition_scene.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
BaseScene::BaseScene() {
|
||||
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
|
||||
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void BaseScene::onEnter() {
|
||||
extra2d::Scene::onEnter();
|
||||
// 计算并更新视口
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
void BaseScene::updateViewport() {
|
||||
auto &app = extra2d::Application::instance();
|
||||
float windowWidth = static_cast<float>(app.window().getWidth());
|
||||
float windowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
// 计算游戏内容在窗口中的居中位置
|
||||
// 保持游戏原始宽高比,进行"黑边"适配
|
||||
float scaleX = windowWidth / GAME_WIDTH;
|
||||
float scaleY = windowHeight / GAME_HEIGHT;
|
||||
// 使用较小的缩放比例,确保游戏内容完整显示在窗口中
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
|
||||
scaledGameWidth_ = GAME_WIDTH * scale;
|
||||
scaledGameHeight_ = GAME_HEIGHT * scale;
|
||||
// 计算居中偏移,使游戏内容在窗口中水平和垂直居中
|
||||
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
|
||||
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
|
||||
|
||||
// 设置视口大小为游戏逻辑分辨率
|
||||
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
// 创建并设置相机
|
||||
auto camera = extra2d::makePtr<extra2d::Camera>();
|
||||
// 设置正交投影,覆盖整个游戏逻辑区域
|
||||
// 注意:对于2D游戏,Y轴向下增长,所以bottom > top
|
||||
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
|
||||
setCamera(camera);
|
||||
}
|
||||
|
||||
void BaseScene::onRender(extra2d::RenderBackend &renderer) {
|
||||
// 检查窗口大小是否改变,如果改变则更新视口
|
||||
auto &app = extra2d::Application::instance();
|
||||
float currentWindowWidth = static_cast<float>(app.window().getWidth());
|
||||
float currentWindowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
// 如果窗口大小改变,重新计算视口
|
||||
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
|
||||
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
|
||||
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
|
||||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
|
||||
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
|
||||
"({} x {}), updating viewport",
|
||||
expectedWidth, expectedHeight, currentWindowWidth,
|
||||
currentWindowHeight);
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 设置视口为居中区域
|
||||
renderer.setViewport(
|
||||
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
|
||||
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
|
||||
|
||||
// 调用父类的 onRender 进行实际渲染
|
||||
extra2d::Scene::onRender(renderer);
|
||||
}
|
||||
|
||||
void BaseScene::renderContent(extra2d::RenderBackend &renderer) {
|
||||
// 如果视口参数未初始化(onEnter 还没被调用),先初始化
|
||||
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 检查窗口大小是否改变
|
||||
auto &app = extra2d::Application::instance();
|
||||
float currentWindowWidth = static_cast<float>(app.window().getWidth());
|
||||
float currentWindowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
|
||||
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
|
||||
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
|
||||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
|
||||
bool isChildOfTransition = false;
|
||||
if (auto parent = getParent()) {
|
||||
if (dynamic_cast<extra2d::TransitionScene *>(parent.get())) {
|
||||
isChildOfTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isChildOfTransition) {
|
||||
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
|
||||
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
|
||||
auto camera = getActiveCamera();
|
||||
if (camera) {
|
||||
// 设置投影矩阵覆盖整个游戏逻辑区域
|
||||
renderer.setViewProjection(camera->getViewProjectionMatrix());
|
||||
}
|
||||
// 渲染场景内容(投影矩阵已设置,直接渲染)
|
||||
batchUpdateTransforms();
|
||||
renderer.beginSpriteBatch();
|
||||
render(renderer);
|
||||
renderer.endSpriteBatch();
|
||||
} else {
|
||||
// 正常渲染时,调用父类的 renderContent 处理视口和投影
|
||||
renderer.setViewport(static_cast<int>(viewportOffsetX_),
|
||||
static_cast<int>(viewportOffsetY_),
|
||||
static_cast<int>(scaledGameWidth_),
|
||||
static_cast<int>(scaledGameHeight_));
|
||||
extra2d::Scene::renderContent(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// ============================================================================
|
||||
// BaseScene.h - Flappy Bird 基础场景类
|
||||
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
// 游戏逻辑分辨率(原始 Flappy Bird 尺寸)
|
||||
static constexpr float GAME_WIDTH = 288.0f;
|
||||
static constexpr float GAME_HEIGHT = 512.0f;
|
||||
|
||||
/**
|
||||
* @brief Flappy Bird 基础场景类
|
||||
* 所有游戏场景都应继承此类,以获得统一的居中视口适配功能
|
||||
*/
|
||||
class BaseScene : public extra2d::Scene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
BaseScene();
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 渲染时调用,设置居中视口
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void onRender(extra2d::RenderBackend &renderer) override;
|
||||
|
||||
/**
|
||||
* @brief 渲染场景内容,确保视口正确设置
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void renderContent(extra2d::RenderBackend &renderer) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief 更新视口计算,使游戏内容在窗口中居中显示
|
||||
*/
|
||||
void updateViewport();
|
||||
|
||||
// 视口适配参数(用于在窗口中居中显示游戏内容)
|
||||
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
|
||||
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
|
||||
float viewportOffsetX_ = 0.0f; // 视口水平偏移
|
||||
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
// ============================================================================
|
||||
// GameOverLayer.cpp - 游戏结束层实现
|
||||
// ============================================================================
|
||||
|
||||
#include "GameOverLayer.h"
|
||||
#include "BaseScene.h"
|
||||
#include "GameScene.h"
|
||||
#include "Number.h"
|
||||
#include "ResLoader.h"
|
||||
#include "StartScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
GameOverLayer::GameOverLayer(int score) : score_(score) {
|
||||
// 注意:不要在构造函数中创建子节点
|
||||
// 因为此时 weak_from_this() 还不能使用
|
||||
}
|
||||
|
||||
void GameOverLayer::onEnter() {
|
||||
Node::onEnter();
|
||||
|
||||
// 在 onEnter 中初始化,此时 weak_from_this() 可用
|
||||
// 使用游戏逻辑分辨率
|
||||
float screenWidth = GAME_WIDTH;
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 整体居中(x 坐标相对于屏幕中心)
|
||||
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight));
|
||||
|
||||
// 显示 "Game Over" 文字(y=120,从顶部开始)
|
||||
auto gameOverFrame = ResLoader::getKeyFrame("text_game_over");
|
||||
if (gameOverFrame) {
|
||||
auto gameOver = extra2d::Sprite::create(gameOverFrame->getTexture(),
|
||||
gameOverFrame->getRect());
|
||||
gameOver->setAnchor(extra2d::Vec2(0.5f, 0.0f));
|
||||
gameOver->setPosition(extra2d::Vec2(0.0f, 120.0f)); // x=0 表示相对于中心点
|
||||
addChild(gameOver);
|
||||
}
|
||||
|
||||
// 初始化得分面板
|
||||
initPanel(score_, screenHeight);
|
||||
|
||||
// 初始化按钮
|
||||
initButtons();
|
||||
|
||||
// 创建向上移动的动画(从屏幕底部移动到正常位置)
|
||||
auto moveAction = extra2d::MoveBy::create(1.0f, extra2d::Vec2(0.0f, -screenHeight));
|
||||
moveAction->setCompletionCallback([this]() {
|
||||
animationDone_ = true;
|
||||
if (restartBtn_)
|
||||
restartBtn_->setEnabled(true);
|
||||
if (menuBtn_)
|
||||
menuBtn_->setEnabled(true);
|
||||
if (shareBtn_)
|
||||
shareBtn_->setEnabled(true);
|
||||
});
|
||||
runAction(moveAction);
|
||||
}
|
||||
|
||||
void GameOverLayer::initPanel(int score, float screenHeight) {
|
||||
// 显示得分板(在屏幕中间)
|
||||
auto panelFrame = ResLoader::getKeyFrame("score_panel");
|
||||
if (!panelFrame)
|
||||
return;
|
||||
|
||||
auto panel =
|
||||
extra2d::Sprite::create(panelFrame->getTexture(), panelFrame->getRect());
|
||||
panel->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
panel->setPosition(
|
||||
extra2d::Vec2(0.0f, screenHeight / 2.0f)); // x=0 表示相对于中心点
|
||||
addChild(panel);
|
||||
|
||||
// 获取最高分(从存储中读取)
|
||||
static int bestScore = 0;
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
}
|
||||
|
||||
// 显示 "New" 标记(如果破了记录)
|
||||
if (score >= bestScore && score > 0) {
|
||||
auto newFrame = ResLoader::getKeyFrame("new");
|
||||
if (newFrame) {
|
||||
auto newSprite =
|
||||
extra2d::Sprite::create(newFrame->getTexture(), newFrame->getRect());
|
||||
newSprite->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
// 调整位置使其在面板内部,靠近 BEST 分数
|
||||
newSprite->setPosition(
|
||||
extra2d::Vec2(30.0f, 25.0f)); // 相对于面板的坐标,在 BEST 右侧
|
||||
panel->addChild(newSprite);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示奖牌
|
||||
auto medalFrame = getMedal(score);
|
||||
if (medalFrame) {
|
||||
auto medal = extra2d::Sprite::create(medalFrame->getTexture(),
|
||||
medalFrame->getRect());
|
||||
medal->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
medal->setPosition(extra2d::Vec2(54.0f, 68.0f)); // 相对于面板的坐标
|
||||
panel->addChild(medal);
|
||||
}
|
||||
|
||||
// 显示本局得分
|
||||
auto scoreNumber = extra2d::makePtr<Number>();
|
||||
scoreNumber->setLittleNumber(score);
|
||||
scoreNumber->setPosition(
|
||||
extra2d::Vec2(80.0f, -15.0f)); // 相对于面板的坐标,右侧对齐
|
||||
panel->addChild(scoreNumber);
|
||||
|
||||
// 显示最高分
|
||||
auto bestNumber = extra2d::makePtr<Number>();
|
||||
bestNumber->setLittleNumber(bestScore);
|
||||
bestNumber->setPosition(
|
||||
extra2d::Vec2(80.0f, 25.0f)); // 相对于面板的坐标,右侧对齐
|
||||
panel->addChild(bestNumber);
|
||||
}
|
||||
|
||||
void GameOverLayer::initButtons() {
|
||||
auto restartFrame = ResLoader::getKeyFrame("button_restart");
|
||||
if (restartFrame) {
|
||||
restartBtn_ = extra2d::Button::create();
|
||||
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
|
||||
restartFrame->getRect());
|
||||
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
|
||||
restartBtn_->setEnabled(false);
|
||||
restartBtn_->setOnClick([]() {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
});
|
||||
addChild(restartBtn_);
|
||||
}
|
||||
|
||||
auto menuFrame = ResLoader::getKeyFrame("button_menu");
|
||||
if (menuFrame) {
|
||||
menuBtn_ = extra2d::Button::create();
|
||||
menuBtn_->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect());
|
||||
menuBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
menuBtn_->setPosition(extra2d::Vec2(0.0f, 420.0f));
|
||||
menuBtn_->setEnabled(false);
|
||||
menuBtn_->setOnClick([]() {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
});
|
||||
addChild(menuBtn_);
|
||||
}
|
||||
|
||||
auto shareFrame = ResLoader::getKeyFrame("button_share");
|
||||
if (shareFrame) {
|
||||
shareBtn_ = extra2d::Button::create();
|
||||
shareBtn_->setBackgroundImage(shareFrame->getTexture(),
|
||||
shareFrame->getRect());
|
||||
shareBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
shareBtn_->setPosition(extra2d::Vec2(0.0f, 460.0f));
|
||||
shareBtn_->setEnabled(false);
|
||||
shareBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); });
|
||||
addChild(shareBtn_);
|
||||
}
|
||||
}
|
||||
|
||||
void GameOverLayer::onUpdate(float dt) {
|
||||
Node::onUpdate(dt);
|
||||
|
||||
if (!animationDone_)
|
||||
return;
|
||||
|
||||
auto &input = extra2d::Application::instance().input();
|
||||
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
}
|
||||
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
extra2d::Ptr<extra2d::SpriteFrame> GameOverLayer::getMedal(int score) {
|
||||
if (score < 10) {
|
||||
return nullptr; // 无奖牌
|
||||
} else if (score < 20) {
|
||||
return ResLoader::getKeyFrame("medals_0"); // 铜牌
|
||||
} else if (score < 30) {
|
||||
return ResLoader::getKeyFrame("medals_1"); // 银牌
|
||||
} else if (score < 50) {
|
||||
return ResLoader::getKeyFrame("medals_2"); // 金牌
|
||||
} else {
|
||||
return ResLoader::getKeyFrame("medals_3"); // 钻石奖牌
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
// ============================================================================
|
||||
// GameOverLayer.h - 游戏结束层
|
||||
// 描述: 显示游戏结束界面、得分和奖牌
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 游戏结束层类
|
||||
* 显示游戏结束后的得分面板和按钮
|
||||
*/
|
||||
class GameOverLayer : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* @param score 本局得分
|
||||
*/
|
||||
GameOverLayer(int score);
|
||||
|
||||
/**
|
||||
* @brief 进入场景时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧更新时调用
|
||||
* @param dt 时间间隔
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 初始化得分面板
|
||||
* @param score 本局得分
|
||||
* @param screenHeight 屏幕高度
|
||||
*/
|
||||
void initPanel(int score, float screenHeight);
|
||||
|
||||
/**
|
||||
* @brief 初始化按钮
|
||||
*/
|
||||
void initButtons();
|
||||
|
||||
/**
|
||||
* @brief 根据得分获取奖牌
|
||||
* @param score 得分
|
||||
* @return 奖牌精灵帧
|
||||
*/
|
||||
extra2d::Ptr<extra2d::SpriteFrame> getMedal(int score);
|
||||
|
||||
int score_ = 0; // 本局得分
|
||||
bool animationDone_ = false; // 动画是否完成
|
||||
extra2d::Ptr<extra2d::Button> restartBtn_; // 重新开始按钮
|
||||
extra2d::Ptr<extra2d::Button> menuBtn_; // 菜单按钮
|
||||
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
// ============================================================================
|
||||
// GameScene.cpp - 游戏主场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "GameScene.h"
|
||||
#include "GameOverLayer.h"
|
||||
#include "ResLoader.h"
|
||||
#include "input.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
GameScene::GameScene() {
|
||||
// 基类 BaseScene 已经处理了视口设置和背景颜色
|
||||
}
|
||||
|
||||
void GameScene::onEnter() {
|
||||
BaseScene::onEnter();
|
||||
|
||||
// 游戏坐标系:使用游戏逻辑分辨率
|
||||
float screenWidth = GAME_WIDTH;
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 添加背景(使用左上角锚点,与原游戏一致)
|
||||
auto bgFrame = ResLoader::getKeyFrame("bg_day");
|
||||
if (bgFrame) {
|
||||
auto background =
|
||||
extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect());
|
||||
background->setAnchor(extra2d::Vec2(0.0f, 0.0f));
|
||||
background->setPosition(extra2d::Vec2(0.0f, 0.0f));
|
||||
addChild(background);
|
||||
}
|
||||
|
||||
// 添加水管(初始时隐藏,游戏开始后才显示)
|
||||
auto pipes = extra2d::makePtr<Pipes>();
|
||||
pipes_ = pipes.get();
|
||||
pipes->setVisible(false);
|
||||
addChild(pipes);
|
||||
|
||||
// 添加小鸟(在屏幕中间偏左位置)
|
||||
auto bird = extra2d::makePtr<Bird>();
|
||||
bird->setPosition(
|
||||
extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f));
|
||||
bird_ = bird.get();
|
||||
addChild(bird);
|
||||
|
||||
// 添加地面
|
||||
auto ground = extra2d::makePtr<Ground>();
|
||||
ground_ = ground.get();
|
||||
addChild(ground);
|
||||
|
||||
// 添加得分(屏幕顶部中央)
|
||||
auto scoreNumber = extra2d::makePtr<Number>();
|
||||
scoreNumber->setPosition(extra2d::Vec2(screenWidth / 2.0f, 50.0f));
|
||||
scoreNumber->setNumber(0);
|
||||
scoreNumber_ = scoreNumber.get();
|
||||
addChild(scoreNumber);
|
||||
|
||||
// 添加 ready 图片(屏幕中央偏上)
|
||||
auto readyFrame = ResLoader::getKeyFrame("text_ready");
|
||||
if (readyFrame) {
|
||||
readySprite_ = extra2d::Sprite::create(readyFrame->getTexture(),
|
||||
readyFrame->getRect());
|
||||
readySprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
readySprite_->setPosition(
|
||||
extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f - 70.0f));
|
||||
addChild(readySprite_);
|
||||
}
|
||||
|
||||
// 添加教程图片(屏幕中央偏下)
|
||||
auto tutorialFrame = ResLoader::getKeyFrame("tutorial");
|
||||
if (tutorialFrame) {
|
||||
tutorialSprite_ = extra2d::Sprite::create(tutorialFrame->getTexture(),
|
||||
tutorialFrame->getRect());
|
||||
tutorialSprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
|
||||
tutorialSprite_->setPosition(
|
||||
extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f + 30.0f));
|
||||
addChild(tutorialSprite_);
|
||||
}
|
||||
|
||||
// 播放转场音效
|
||||
ResLoader::playMusic(MusicType::Swoosh);
|
||||
|
||||
// 初始化状态
|
||||
started_ = false;
|
||||
score_ = 0;
|
||||
}
|
||||
|
||||
void GameScene::onUpdate(float dt) {
|
||||
if (!gameOver_) {
|
||||
if (!bird_)
|
||||
return;
|
||||
|
||||
auto &input = extra2d::Application::instance().input();
|
||||
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
|
||||
input.isMousePressed(extra2d::MouseButton::Left)) {
|
||||
if (!started_) {
|
||||
started_ = true;
|
||||
startGame();
|
||||
}
|
||||
bird_->jump();
|
||||
}
|
||||
|
||||
if (started_) {
|
||||
bird_->fall(dt);
|
||||
|
||||
if (pipes_) {
|
||||
Pipe *firstPipe = pipes_->getPipe(0);
|
||||
if (firstPipe && !firstPipe->scored) {
|
||||
float birdX = bird_->getPosition().x;
|
||||
float pipeX = firstPipe->getPosition().x;
|
||||
if (pipeX <= birdX) {
|
||||
score_++;
|
||||
scoreNumber_->setNumber(score_);
|
||||
firstPipe->scored = true;
|
||||
ResLoader::playMusic(MusicType::Point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bird_->isLiving() && checkCollision()) {
|
||||
onHit();
|
||||
}
|
||||
|
||||
if (bird_->isLiving() && GAME_HEIGHT - bird_->getPosition().y <= 123.0f) {
|
||||
bird_->setPosition(
|
||||
extra2d::Vec2(bird_->getPosition().x, GAME_HEIGHT - 123.0f));
|
||||
bird_->setStatus(Bird::Status::Still);
|
||||
onHit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseScene::onUpdate(dt);
|
||||
}
|
||||
|
||||
void GameScene::startGame() {
|
||||
// 隐藏 ready 和 tutorial 图片
|
||||
if (readySprite_) {
|
||||
readySprite_->setVisible(false);
|
||||
}
|
||||
if (tutorialSprite_) {
|
||||
tutorialSprite_->setVisible(false);
|
||||
}
|
||||
|
||||
// 显示并开始移动水管
|
||||
if (pipes_) {
|
||||
pipes_->setVisible(true);
|
||||
pipes_->start();
|
||||
}
|
||||
|
||||
// 设置小鸟状态
|
||||
if (bird_) {
|
||||
bird_->setStatus(Bird::Status::StartToFly);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameScene::checkCollision() {
|
||||
if (!bird_ || !pipes_)
|
||||
return false;
|
||||
|
||||
extra2d::Rect birdBox = bird_->getBoundingBox();
|
||||
|
||||
// 检查与每个水管的碰撞
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Pipe *pipe = pipes_->getPipe(i);
|
||||
if (!pipe)
|
||||
continue;
|
||||
|
||||
// 检查与上水管的碰撞
|
||||
extra2d::Rect topBox = pipe->getTopPipeBox();
|
||||
if (birdBox.intersects(topBox)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查与下水管的碰撞
|
||||
extra2d::Rect bottomBox = pipe->getBottomPipeBox();
|
||||
if (birdBox.intersects(bottomBox)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void GameScene::onHit() {
|
||||
if (!bird_->isLiving())
|
||||
return;
|
||||
|
||||
// 小鸟死亡
|
||||
bird_->die();
|
||||
|
||||
// 停止地面滚动
|
||||
if (ground_) {
|
||||
ground_->stop();
|
||||
}
|
||||
|
||||
// 停止水管移动
|
||||
if (pipes_) {
|
||||
pipes_->stop();
|
||||
}
|
||||
|
||||
// 停止小鸟动画
|
||||
if (bird_) {
|
||||
bird_->setStatus(Bird::Status::Still);
|
||||
}
|
||||
|
||||
// 隐藏得分
|
||||
if (scoreNumber_) {
|
||||
scoreNumber_->setVisible(false);
|
||||
}
|
||||
|
||||
gameOver();
|
||||
}
|
||||
|
||||
void GameScene::gameOver() {
|
||||
if (gameOver_)
|
||||
return;
|
||||
|
||||
started_ = false;
|
||||
gameOver_ = true;
|
||||
|
||||
auto gameOverLayer = extra2d::makePtr<GameOverLayer>(score_);
|
||||
addChild(gameOverLayer);
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
// ============================================================================
|
||||
// GameScene.h - 游戏主场景
|
||||
// 描述: 游戏的核心场景,包含小鸟、水管、地面和得分系统
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseScene.h"
|
||||
#include "Bird.h"
|
||||
#include "Pipes.h"
|
||||
#include "Ground.h"
|
||||
#include "Number.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 游戏主场景类
|
||||
* 游戏的核心场景,处理游戏逻辑、碰撞检测和得分
|
||||
*/
|
||||
class GameScene : public BaseScene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
GameScene();
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧更新时调用
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 开始游戏
|
||||
*/
|
||||
void startGame();
|
||||
|
||||
/**
|
||||
* @brief 处理碰撞事件
|
||||
*/
|
||||
void onHit();
|
||||
|
||||
/**
|
||||
* @brief 游戏结束
|
||||
*/
|
||||
void gameOver();
|
||||
|
||||
/**
|
||||
* @brief 检查小鸟与水管的碰撞
|
||||
* @return 是否发生碰撞
|
||||
*/
|
||||
bool checkCollision();
|
||||
|
||||
Bird* bird_ = nullptr; // 小鸟
|
||||
Pipes* pipes_ = nullptr; // 水管管理器
|
||||
Ground* ground_ = nullptr; // 地面
|
||||
Number* scoreNumber_ = nullptr; // 得分显示
|
||||
extra2d::Ptr<extra2d::Sprite> readySprite_; // "Get Ready" 提示
|
||||
extra2d::Ptr<extra2d::Sprite> tutorialSprite_; // 操作教程提示
|
||||
|
||||
bool started_ = false; // 游戏是否已开始
|
||||
bool gameOver_ = false; // 游戏是否已结束
|
||||
int score_ = 0; // 当前得分
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// ============================================================================
|
||||
// Number.cpp - 数字显示类实现
|
||||
// ============================================================================
|
||||
|
||||
#include "Number.h"
|
||||
#include "ResLoader.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
Number::Number() : number_(0) {
|
||||
}
|
||||
|
||||
void Number::setNumber(int number) {
|
||||
number_ = number;
|
||||
createNumberSprites(number, "number_big_");
|
||||
}
|
||||
|
||||
void Number::setLittleNumber(int number) {
|
||||
number_ = number;
|
||||
createNumberSprites(number, "number_medium_");
|
||||
}
|
||||
|
||||
void Number::createNumberSprites(int number, const std::string& prefix) {
|
||||
// 清除之前的数字精灵
|
||||
removeAllChildren();
|
||||
|
||||
// 获取数字 0 的高度作为参考
|
||||
auto zeroFrame = ResLoader::getKeyFrame(prefix + "0");
|
||||
float digitHeight = zeroFrame ? zeroFrame->getRect().size.height : 36.0f;
|
||||
|
||||
// 收集所有数字位
|
||||
std::vector<int> digits;
|
||||
if (number == 0) {
|
||||
digits.push_back(0);
|
||||
} else {
|
||||
while (number > 0) {
|
||||
digits.push_back(number % 10);
|
||||
number /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总宽度
|
||||
float totalWidth = 0.0f;
|
||||
std::vector<float> digitWidths;
|
||||
for (int digit : digits) {
|
||||
auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digit));
|
||||
float width = frame ? frame->getRect().size.width : 24.0f;
|
||||
digitWidths.push_back(width);
|
||||
totalWidth += width;
|
||||
}
|
||||
|
||||
// 创建数字精灵并居中排列
|
||||
float currentX = -totalWidth / 2.0f;
|
||||
for (size_t i = 0; i < digits.size(); ++i) {
|
||||
auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digits[i]));
|
||||
if (frame) {
|
||||
auto digitSprite = extra2d::Sprite::create(frame->getTexture(), frame->getRect());
|
||||
digitSprite->setAnchor(extra2d::Vec2(0.0f, 0.0f));
|
||||
digitSprite->setPosition(extra2d::Vec2(currentX, -digitHeight / 2.0f));
|
||||
addChild(digitSprite);
|
||||
}
|
||||
currentX += digitWidths[i];
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
// ============================================================================
|
||||
// Number.h - 数字显示类
|
||||
// 描述: 将整数数字转换为精灵图片显示
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 数字显示类
|
||||
* 用于显示得分,将整数转换为对应的数字图片
|
||||
*/
|
||||
class Number : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
Number();
|
||||
|
||||
/**
|
||||
* @brief 设置显示的数字(大号)
|
||||
* @param number 要显示的数字
|
||||
*/
|
||||
void setNumber(int number);
|
||||
|
||||
/**
|
||||
* @brief 设置显示的数字(小号)
|
||||
* @param number 要显示的数字
|
||||
*/
|
||||
void setLittleNumber(int number);
|
||||
|
||||
/**
|
||||
* @brief 获取当前数字
|
||||
* @return 当前数字
|
||||
*/
|
||||
int getNumber() const { return number_; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 创建数字精灵
|
||||
* @param number 数字值
|
||||
* @param prefix 数字图片前缀("number_big_" 或 "number_medium_")
|
||||
*/
|
||||
void createNumberSprites(int number, const std::string& prefix);
|
||||
|
||||
int number_ = 0; // 当前数字
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
// ============================================================================
|
||||
// ResLoader.cpp - 资源加载器实现
|
||||
// ============================================================================
|
||||
|
||||
#include "ResLoader.h"
|
||||
#include <json/json.hpp>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
|
||||
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
|
||||
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
|
||||
|
||||
void ResLoader::init() {
|
||||
auto &resources = extra2d::Application::instance().resources();
|
||||
|
||||
// 加载图集纹理
|
||||
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
|
||||
if (!atlasTexture_) {
|
||||
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用资源管理器加载 JSON 文件
|
||||
std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json");
|
||||
if (jsonContent.empty()) {
|
||||
E2D_LOG_ERROR("无法加载 atlas.json 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 JSON 图集数据
|
||||
try {
|
||||
nlohmann::json jsonData = nlohmann::json::parse(jsonContent);
|
||||
|
||||
for (const auto &sprite : jsonData["sprites"]) {
|
||||
std::string name = sprite["name"];
|
||||
float x = sprite["x"];
|
||||
float y = sprite["y"];
|
||||
float width = sprite["width"];
|
||||
float height = sprite["height"];
|
||||
|
||||
ImageInfo info = {width, height, x, y};
|
||||
imageMap_[name] = info;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
|
||||
} catch (const std::exception &e) {
|
||||
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载音效
|
||||
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
|
||||
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
|
||||
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
|
||||
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
|
||||
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
|
||||
|
||||
E2D_LOG_INFO("资源加载完成");
|
||||
}
|
||||
|
||||
extra2d::Ptr<extra2d::SpriteFrame>
|
||||
ResLoader::getKeyFrame(const std::string &name) {
|
||||
auto it = imageMap_.find(name);
|
||||
if (it == imageMap_.end()) {
|
||||
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ImageInfo &info = it->second;
|
||||
E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", name, info.width,
|
||||
info.height, info.x, info.y);
|
||||
|
||||
// 检查纹理尺寸
|
||||
if (atlasTexture_) {
|
||||
E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->getWidth(),
|
||||
atlasTexture_->getHeight());
|
||||
}
|
||||
|
||||
return extra2d::makePtr<extra2d::SpriteFrame>(
|
||||
atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
|
||||
}
|
||||
|
||||
void ResLoader::playMusic(MusicType type) {
|
||||
auto it = soundMap_.find(type);
|
||||
if (it == soundMap_.end()) {
|
||||
E2D_LOG_WARN("ResLoader::playMusic: sound type not found");
|
||||
return;
|
||||
}
|
||||
if (!it->second) {
|
||||
E2D_LOG_WARN("ResLoader::playMusic: sound pointer is null");
|
||||
return;
|
||||
}
|
||||
if (!it->second->play()) {
|
||||
E2D_LOG_WARN("ResLoader::playMusic: failed to play sound");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// ============================================================================
|
||||
// ResLoader.h - 资源加载器
|
||||
// 描述: 管理游戏资源的加载和访问
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 音频类型枚举
|
||||
*/
|
||||
enum class MusicType {
|
||||
Click, // 按键声音
|
||||
Hit, // 小鸟死亡声音
|
||||
Fly, // 小鸟飞翔声音
|
||||
Point, // 得分声音
|
||||
Swoosh // 转场声音
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 资源加载器类
|
||||
* 管理纹理图集、精灵帧和音频资源的加载
|
||||
*/
|
||||
class ResLoader {
|
||||
public:
|
||||
/**
|
||||
* @brief 初始化资源加载器
|
||||
*/
|
||||
static void init();
|
||||
|
||||
/**
|
||||
* @brief 获取精灵帧
|
||||
* @param name 帧名称
|
||||
* @return 精灵帧指针
|
||||
*/
|
||||
static extra2d::Ptr<extra2d::SpriteFrame> getKeyFrame(const std::string& name);
|
||||
|
||||
/**
|
||||
* @brief 播放音效
|
||||
* @param type 音效类型
|
||||
*/
|
||||
static void playMusic(MusicType type);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 图片信息结构
|
||||
* 对应 atlas.txt 格式: 元素名 width height x y
|
||||
*/
|
||||
struct ImageInfo {
|
||||
float width, height, x, y;
|
||||
};
|
||||
|
||||
static extra2d::Ptr<extra2d::Texture> atlasTexture_; // 图集纹理
|
||||
static std::map<std::string, ImageInfo> imageMap_; // 图片信息映射
|
||||
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_; // 音效映射
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
// ============================================================================
|
||||
// SplashScene.cpp - 启动场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "SplashScene.h"
|
||||
#include "ResLoader.h"
|
||||
#include "StartScene.h"
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
SplashScene::SplashScene() {
|
||||
// 基类 BaseScene 已经处理了视口设置和背景颜色
|
||||
}
|
||||
|
||||
void SplashScene::onEnter() {
|
||||
BaseScene::onEnter();
|
||||
|
||||
// 尝试加载 splash 图片
|
||||
auto splashFrame = ResLoader::getKeyFrame("splash");
|
||||
if (splashFrame) {
|
||||
auto splash = extra2d::Sprite::create(splashFrame->getTexture(),
|
||||
splashFrame->getRect());
|
||||
splash->setAnchor(0.5f, 0.5f);
|
||||
// splash 图片是全屏的(288x512),将其中心放在游戏区域中心
|
||||
splash->setPosition(GAME_WIDTH / 2.0f, GAME_HEIGHT / 2.0f);
|
||||
addChild(splash);
|
||||
}
|
||||
// 播放转场音效
|
||||
ResLoader::playMusic(MusicType::Swoosh);
|
||||
}
|
||||
|
||||
void SplashScene::onUpdate(float dt) {
|
||||
BaseScene::onUpdate(dt);
|
||||
|
||||
// 计时
|
||||
timer_ += dt;
|
||||
if (timer_ >= delay_) {
|
||||
gotoStartScene();
|
||||
}
|
||||
}
|
||||
|
||||
void SplashScene::gotoStartScene() {
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
|
||||
extra2d::TransitionType::Fade, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// ============================================================================
|
||||
// SplashScene.h - 启动场景
|
||||
// 描述: 显示游戏 Logo,2秒后自动跳转到开始场景
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 启动场景类
|
||||
* 显示游戏 Logo,短暂延迟后进入主菜单
|
||||
*/
|
||||
class SplashScene : public BaseScene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
SplashScene();
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧更新时调用
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 跳转到开始场景
|
||||
*/
|
||||
void gotoStartScene();
|
||||
|
||||
float timer_ = 0.0f; // 计时器
|
||||
const float delay_ = 2.0f; // 延迟时间(秒)
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
// ============================================================================
|
||||
// StartScene.cpp - 开始菜单场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "StartScene.h"
|
||||
#include "Bird.h"
|
||||
#include "GameScene.h"
|
||||
#include "Ground.h"
|
||||
#include "ResLoader.h"
|
||||
#include "extra2d/event/input_codes.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
StartScene::StartScene() {
|
||||
// 基类 BaseScene 已经处理了视口设置和背景颜色
|
||||
}
|
||||
|
||||
void StartScene::onEnter() {
|
||||
BaseScene::onEnter();
|
||||
|
||||
// 使用游戏逻辑分辨率
|
||||
float screenWidth = GAME_WIDTH;
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 添加背景(使用左上角锚点)
|
||||
auto bgFrame = ResLoader::getKeyFrame("bg_day");
|
||||
if (bgFrame) {
|
||||
auto background =
|
||||
extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect());
|
||||
background->setAnchor(0.0f, 0.0f);
|
||||
background->setPosition(0.0f, 0.0f);
|
||||
addChild(background);
|
||||
E2D_LOG_INFO("背景已添加: size={} x {}", bgFrame->getRect().size.width,
|
||||
bgFrame->getRect().size.height);
|
||||
} else {
|
||||
E2D_LOG_ERROR("无法加载背景图片");
|
||||
}
|
||||
|
||||
// 添加地面
|
||||
auto ground = extra2d::makePtr<Ground>();
|
||||
addChild(ground);
|
||||
|
||||
// 添加标题图片(在上方)
|
||||
auto titleFrame = ResLoader::getKeyFrame("title");
|
||||
if (titleFrame) {
|
||||
auto title = extra2d::Sprite::create(titleFrame->getTexture(),
|
||||
titleFrame->getRect());
|
||||
title->setAnchor(0.5f, 0.5f);
|
||||
// 标题在屏幕上方
|
||||
title->setPosition(screenWidth / 2.0f, 150.0f);
|
||||
addChild(title);
|
||||
E2D_LOG_INFO("标题已添加: size={} x {}", titleFrame->getRect().size.width,
|
||||
titleFrame->getRect().size.height);
|
||||
} else {
|
||||
E2D_LOG_ERROR("无法加载标题图片");
|
||||
}
|
||||
|
||||
// 添加小鸟(在标题下方)
|
||||
auto bird = extra2d::makePtr<Bird>();
|
||||
bird->setAnchor(0.5f, 0.5f);
|
||||
bird->setPosition(screenWidth / 2.0f, screenHeight / 2.0f);
|
||||
bird->setStatus(Bird::Status::Idle);
|
||||
addChild(bird);
|
||||
|
||||
// 添加开始按钮 - 在小鸟下方
|
||||
auto playFrame = ResLoader::getKeyFrame("button_play");
|
||||
if (playFrame) {
|
||||
float btnWidth = playFrame->getRect().size.width;
|
||||
float btnHeight = playFrame->getRect().size.height;
|
||||
|
||||
playBtn_ = extra2d::Button::create();
|
||||
playBtn_->setBackgroundImage(playFrame->getTexture(), playFrame->getRect());
|
||||
// 使用世界坐标,中心锚点
|
||||
playBtn_->setAnchor(0.5f, 0.5f);
|
||||
// PLAY 按钮在小鸟下方
|
||||
playBtn_->setPosition(screenWidth / 2.0f,
|
||||
screenHeight - playBtn_->getSize().height - 100.0f);
|
||||
playBtn_->setOnClick([this]() {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
startGame();
|
||||
});
|
||||
addChild(playBtn_);
|
||||
}
|
||||
|
||||
// 添加分享按钮 - 在 PLAY 按钮下方,靠近地面
|
||||
auto shareFrame = ResLoader::getKeyFrame("button_share");
|
||||
if (shareFrame) {
|
||||
float btnWidth = shareFrame->getRect().size.width;
|
||||
float btnHeight = shareFrame->getRect().size.height;
|
||||
|
||||
shareBtn_ = extra2d::Button::create();
|
||||
shareBtn_->setBackgroundImage(shareFrame->getTexture(),
|
||||
shareFrame->getRect());
|
||||
// 使用世界坐标,中心锚点
|
||||
shareBtn_->setAnchor(0.5f, 0.5f);
|
||||
// SHARE 按钮在 PLAY 按钮下方,靠近地面
|
||||
shareBtn_->setPosition(screenWidth / 2.0f,
|
||||
screenHeight - shareBtn_->getSize().height - 80.0f);
|
||||
shareBtn_->setOnClick([this]() {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
// 分享功能暂不实现
|
||||
});
|
||||
addChild(shareBtn_);
|
||||
}
|
||||
|
||||
// 添加 copyright 图片(在底部)
|
||||
auto copyrightFrame = ResLoader::getKeyFrame("brand_copyright");
|
||||
if (copyrightFrame) {
|
||||
auto copyright = extra2d::Sprite::create(copyrightFrame->getTexture(),
|
||||
copyrightFrame->getRect());
|
||||
copyright->setAnchor(0.5f, 0.5f);
|
||||
// Copyright 在屏幕底部
|
||||
copyright->setPosition(screenWidth / 2.0f, screenHeight - 20.0f);
|
||||
addChild(copyright);
|
||||
}
|
||||
|
||||
// 播放转场音效
|
||||
ResLoader::playMusic(MusicType::Swoosh);
|
||||
}
|
||||
|
||||
void StartScene::onUpdate(float dt) {
|
||||
BaseScene::onUpdate(dt);
|
||||
|
||||
// 检测 A 键或空格开始游戏
|
||||
auto &input = extra2d::Application::instance().input();
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
startGame();
|
||||
}
|
||||
|
||||
// 检测 BACK 键退出游戏
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::Start)) {
|
||||
ResLoader::playMusic(MusicType::Click);
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
void StartScene::startGame() {
|
||||
auto &app = extra2d::Application::instance();
|
||||
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// ============================================================================
|
||||
// StartScene.h - 开始菜单场景
|
||||
// 描述: 显示游戏标题、开始按钮和版权信息
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 开始场景类
|
||||
* 游戏主菜单,包含开始游戏按钮和版权信息
|
||||
*/
|
||||
class StartScene : public BaseScene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
StartScene();
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧更新时调用
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 创建菜单按钮
|
||||
*/
|
||||
void createMenuButtons();
|
||||
|
||||
/**
|
||||
* @brief 开始游戏
|
||||
*/
|
||||
void startGame();
|
||||
|
||||
extra2d::Ptr<extra2d::Button> playBtn_; // 开始按钮
|
||||
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
|
||||
extra2d::Ptr<extra2d::Sprite> title_; // 标题精灵
|
||||
float titleFinalY_ = 0.0f; // 标题最终Y位置
|
||||
float titleAnimTime_ = 0.0f; // 标题动画时间
|
||||
static constexpr float TITLE_ANIM_DURATION = 0.5f; // 标题动画持续时间
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
// ============================================================================
|
||||
// Bird.cpp - 小鸟类实现
|
||||
// ============================================================================
|
||||
|
||||
#include "Bird.h"
|
||||
#include "ResLoader.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
Bird::Bird() {
|
||||
// 注意:不要在构造函数中调用 initAnimations()
|
||||
// 因为此时 weak_from_this() 还不能使用
|
||||
setStatus(Status::Idle);
|
||||
}
|
||||
|
||||
void Bird::onEnter() {
|
||||
Node::onEnter();
|
||||
// 在 onEnter 中初始化动画,此时 weak_from_this() 可用
|
||||
if (!animSprite_) {
|
||||
initAnimations();
|
||||
}
|
||||
}
|
||||
|
||||
Bird::~Bird() = default;
|
||||
|
||||
void Bird::initAnimations() {
|
||||
// 随机选择小鸟颜色(0-2)
|
||||
int colorMode = extra2d::randomInt(0, 2);
|
||||
std::string prefix = "bird" + std::to_string(colorMode) + "_";
|
||||
|
||||
// 创建动画片段
|
||||
auto clip = extra2d::AnimationClip::create("bird_fly");
|
||||
|
||||
// 添加动画帧序列: 0 -> 1 -> 2 -> 1
|
||||
// 注意:每个颜色只有 0, 1, 2 三个帧,没有 3
|
||||
int frameSequence[] = {0, 1, 2, 1};
|
||||
for (int frameIndex : frameSequence) {
|
||||
auto frameSprite = ResLoader::getKeyFrame(prefix + std::to_string(frameIndex));
|
||||
if (frameSprite) {
|
||||
extra2d::AnimationFrame frame;
|
||||
frame.spriteFrame = frameSprite;
|
||||
frame.delay = 100.0f; // 100毫秒 = 0.1秒
|
||||
clip->addFrame(std::move(frame));
|
||||
} else {
|
||||
E2D_LOG_WARN("无法加载动画帧: {}{}", prefix, frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建动画精灵
|
||||
if (clip->getFrameCount() > 0) {
|
||||
clip->setLooping(true);
|
||||
animSprite_ = extra2d::AnimatedSprite::create(clip);
|
||||
// 精灵图动画不应应用帧变换(避免覆盖节点位置)
|
||||
animSprite_->setApplyFrameTransform(false);
|
||||
animSprite_->play();
|
||||
addChild(animSprite_);
|
||||
E2D_LOG_INFO("小鸟动画创建成功: 颜色={}, 帧数={}, running={}, animSprite父节点={}",
|
||||
colorMode, clip->getFrameCount(), isRunning(),
|
||||
animSprite_->getParent() ? "有" : "无");
|
||||
} else {
|
||||
E2D_LOG_ERROR("小鸟动画创建失败: 没有找到任何动画帧");
|
||||
}
|
||||
}
|
||||
|
||||
void Bird::onUpdate(float dt) {
|
||||
extra2d::Node::onUpdate(dt);
|
||||
|
||||
// 处理闲置动画(上下浮动)
|
||||
if (status_ == Status::Idle) {
|
||||
idleTimer_ += dt;
|
||||
idleOffset_ = std::sin(idleTimer_ * 5.0f) * 4.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void Bird::onRender(extra2d::RenderBackend& renderer) {
|
||||
// 动画精灵会自动渲染,这里只需要处理旋转和偏移
|
||||
if (animSprite_) {
|
||||
animSprite_->setRotation(rotation_);
|
||||
|
||||
// 应用闲置偏移
|
||||
if (status_ == Status::Idle) {
|
||||
animSprite_->setPosition(extra2d::Vec2(0.0f, idleOffset_));
|
||||
} else {
|
||||
animSprite_->setPosition(extra2d::Vec2(0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
// 调用父类的 onRender 来渲染子节点
|
||||
Node::onRender(renderer);
|
||||
}
|
||||
|
||||
void Bird::fall(float dt) {
|
||||
if (!living_) return;
|
||||
|
||||
// 更新垂直位置
|
||||
extra2d::Vec2 pos = getPosition();
|
||||
pos.y += speed_ * dt;
|
||||
setPosition(pos);
|
||||
|
||||
// 应用重力
|
||||
speed_ += gravity * dt;
|
||||
|
||||
// 限制顶部边界
|
||||
if (pos.y < 0) {
|
||||
pos.y = 0;
|
||||
setPosition(pos);
|
||||
speed_ = 0;
|
||||
}
|
||||
|
||||
// 根据速度计算旋转角度
|
||||
// 上升时抬头(-15度),下降时低头(最大90度)
|
||||
if (speed_ < 0) {
|
||||
rotation_ = -15.0f;
|
||||
} else {
|
||||
rotation_ = std::min(90.0f, speed_ * 0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
void Bird::jump() {
|
||||
if (!living_) return;
|
||||
|
||||
// 给小鸟向上的速度
|
||||
speed_ = -jumpSpeed;
|
||||
|
||||
// 设置状态为飞行
|
||||
setStatus(Status::Fly);
|
||||
|
||||
// 播放音效
|
||||
ResLoader::playMusic(MusicType::Fly);
|
||||
}
|
||||
|
||||
void Bird::die() {
|
||||
living_ = false;
|
||||
|
||||
// 播放死亡音效
|
||||
ResLoader::playMusic(MusicType::Hit);
|
||||
}
|
||||
|
||||
void Bird::setStatus(Status status) {
|
||||
status_ = status;
|
||||
|
||||
switch (status) {
|
||||
case Status::Still:
|
||||
// 停止所有动画
|
||||
if (animSprite_) {
|
||||
animSprite_->pause();
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::Idle:
|
||||
// 开始闲置动画
|
||||
if (animSprite_) {
|
||||
animSprite_->setPlaybackSpeed(1.0f); // 正常速度
|
||||
animSprite_->play();
|
||||
}
|
||||
idleTimer_ = 0.0f;
|
||||
break;
|
||||
|
||||
case Status::StartToFly:
|
||||
// 停止闲置动画,加速翅膀扇动
|
||||
idleOffset_ = 0.0f;
|
||||
if (animSprite_) {
|
||||
animSprite_->setPlaybackSpeed(2.0f); // 2倍速度 = 0.05秒每帧
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::Fly:
|
||||
// 飞行状态
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
extra2d::Rect Bird::getBoundingBox() const {
|
||||
extra2d::Vec2 pos = getPosition();
|
||||
// 小鸟碰撞框大小约为 24x24
|
||||
float halfSize = 12.0f;
|
||||
return extra2d::Rect(
|
||||
pos.x - halfSize,
|
||||
pos.y - halfSize,
|
||||
halfSize * 2.0f,
|
||||
halfSize * 2.0f
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// ============================================================================
|
||||
// Bird.h - 小鸟类
|
||||
// 描述: 玩家控制的小鸟角色,包含飞行动画和物理效果
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 小鸟类
|
||||
* 游戏主角,包含飞行动画、重力模拟和状态管理
|
||||
*/
|
||||
class Bird : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 小鸟状态枚举
|
||||
*/
|
||||
enum class Status {
|
||||
Still, // 静止不动
|
||||
Idle, // 上下浮动(菜单展示)
|
||||
StartToFly, // 开始飞行
|
||||
Fly // 飞行中
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
Bird();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~Bird();
|
||||
|
||||
/**
|
||||
* @brief 每帧更新
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
/**
|
||||
* @brief 渲染小鸟
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void onRender(extra2d::RenderBackend& renderer) override;
|
||||
|
||||
/**
|
||||
* @brief 进入场景时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 模拟下落
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void fall(float dt);
|
||||
|
||||
/**
|
||||
* @brief 跳跃
|
||||
*/
|
||||
void jump();
|
||||
|
||||
/**
|
||||
* @brief 死亡
|
||||
*/
|
||||
void die();
|
||||
|
||||
/**
|
||||
* @brief 设置小鸟状态
|
||||
* @param status 新状态
|
||||
*/
|
||||
void setStatus(Status status);
|
||||
|
||||
/**
|
||||
* @brief 获取当前状态
|
||||
* @return 当前状态
|
||||
*/
|
||||
Status getStatus() const { return status_; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否存活
|
||||
* @return 是否存活
|
||||
*/
|
||||
bool isLiving() const { return living_; }
|
||||
|
||||
/**
|
||||
* @brief 获取边界框(用于碰撞检测)
|
||||
* @return 边界框
|
||||
*/
|
||||
extra2d::Rect getBoundingBox() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 初始化动画
|
||||
*/
|
||||
void initAnimations();
|
||||
|
||||
bool living_ = true; // 是否存活
|
||||
float speed_ = 0.0f; // 垂直速度
|
||||
float rotation_ = 0.0f; // 旋转角度
|
||||
Status status_ = Status::Idle; // 当前状态
|
||||
|
||||
// 动画相关
|
||||
extra2d::Ptr<extra2d::AnimatedSprite> animSprite_; // 动画精灵
|
||||
float idleTimer_ = 0.0f; // 闲置动画计时器
|
||||
float idleOffset_ = 0.0f; // 闲置偏移量
|
||||
|
||||
// 物理常量
|
||||
static constexpr float gravity = 1440.0f; // 重力加速度
|
||||
static constexpr float jumpSpeed = 432.0f; // 跳跃初速度
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// ============================================================================
|
||||
// Ground.cpp - 地面类实现
|
||||
// ============================================================================
|
||||
|
||||
#include "Ground.h"
|
||||
#include "ResLoader.h"
|
||||
#include "BaseScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
Ground::Ground() {
|
||||
moving_ = true;
|
||||
|
||||
// 使用游戏逻辑高度,而不是窗口高度
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 获取地面纹理帧
|
||||
auto landFrame = ResLoader::getKeyFrame("land");
|
||||
if (!landFrame) return;
|
||||
|
||||
// 获取地面纹理和矩形
|
||||
auto texture = landFrame->getTexture();
|
||||
auto rect = landFrame->getRect();
|
||||
float groundWidth = rect.size.width;
|
||||
float groundHeight = rect.size.height;
|
||||
|
||||
// 创建第一块地面
|
||||
ground1_ = extra2d::Sprite::create(texture, rect);
|
||||
ground1_->setAnchor(extra2d::Vec2(0.0f, 1.0f)); // 锚点设在左下角
|
||||
ground1_->setPosition(extra2d::Vec2(0.0f, screenHeight));
|
||||
addChild(ground1_);
|
||||
|
||||
// 创建第二块地面,紧挨在第一块右边
|
||||
ground2_ = extra2d::Sprite::create(texture, rect);
|
||||
ground2_->setAnchor(extra2d::Vec2(0.0f, 1.0f));
|
||||
ground2_->setPosition(extra2d::Vec2(groundWidth - 1.0f, screenHeight));
|
||||
addChild(ground2_);
|
||||
}
|
||||
|
||||
void Ground::onUpdate(float dt) {
|
||||
extra2d::Node::onUpdate(dt);
|
||||
|
||||
if (!moving_) return;
|
||||
if (!ground1_ || !ground2_) return;
|
||||
|
||||
// 获取地面宽度(从纹理矩形获取)
|
||||
float groundWidth = ground1_->getTextureRect().size.width;
|
||||
|
||||
// 移动两块地面
|
||||
extra2d::Vec2 pos1 = ground1_->getPosition();
|
||||
extra2d::Vec2 pos2 = ground2_->getPosition();
|
||||
|
||||
pos1.x -= speed * dt;
|
||||
pos2.x -= speed * dt;
|
||||
|
||||
// 当地面完全移出屏幕左侧时,重置到右侧
|
||||
if (pos1.x <= -groundWidth) {
|
||||
pos1.x = pos2.x + groundWidth - 1.0f;
|
||||
}
|
||||
if (pos2.x <= -groundWidth) {
|
||||
pos2.x = pos1.x + groundWidth - 1.0f;
|
||||
}
|
||||
|
||||
ground1_->setPosition(pos1);
|
||||
ground2_->setPosition(pos2);
|
||||
}
|
||||
|
||||
void Ground::stop() {
|
||||
moving_ = false;
|
||||
}
|
||||
|
||||
float Ground::getHeight() const {
|
||||
auto landFrame = ResLoader::getKeyFrame("land");
|
||||
return landFrame ? landFrame->getRect().size.height : 112.0f;
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// ============================================================================
|
||||
// Ground.h - 地面类
|
||||
// 描述: 游戏底部不断向左滚动的地面
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 地面类
|
||||
* 游戏底部的滚动地面,由两块地面拼接而成
|
||||
*/
|
||||
class Ground : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
Ground();
|
||||
|
||||
/**
|
||||
* @brief 每帧更新
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
/**
|
||||
* @brief 停止地面滚动
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief 获取地面高度
|
||||
* @return 地面高度
|
||||
*/
|
||||
float getHeight() const;
|
||||
|
||||
private:
|
||||
extra2d::Ptr<extra2d::Sprite> ground1_; // 第一块地面
|
||||
extra2d::Ptr<extra2d::Sprite> ground2_; // 第二块地面
|
||||
|
||||
static constexpr float speed = 120.0f; // 滚动速度(像素/秒)
|
||||
bool moving_ = true; // 是否正在滚动
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// ============================================================================
|
||||
// FlappyBird - Extra2D 示例程序
|
||||
// 作者: Extra2D Team
|
||||
// 描述: 经典的 Flappy Bird 游戏实现
|
||||
// ============================================================================
|
||||
|
||||
#include "ResLoader.h"
|
||||
#include "SplashScene.h"
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
using namespace extra2d;
|
||||
|
||||
/**
|
||||
* @brief 程序入口
|
||||
*/
|
||||
int main(int argc, char **argv) {
|
||||
// 初始化日志系统
|
||||
Logger::init();
|
||||
Logger::setLevel(LogLevel::Debug);
|
||||
|
||||
E2D_LOG_INFO("========================");
|
||||
E2D_LOG_INFO("Extra2D FlappyBird");
|
||||
E2D_LOG_INFO("========================");
|
||||
|
||||
// 获取应用实例
|
||||
auto &app = Application::instance();
|
||||
|
||||
// 配置应用
|
||||
AppConfig config;
|
||||
config.title = "Extra2D - FlappyBird";
|
||||
config.width = 1280; // 窗口宽度 (720P 分辨率)
|
||||
config.height = 720; // 窗口高度 (720P 分辨率)
|
||||
config.vsync = true;
|
||||
config.fpsLimit = 60;
|
||||
|
||||
// 初始化应用
|
||||
if (!app.init(config)) {
|
||||
E2D_LOG_ERROR("应用初始化失败!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 初始化资源加载器
|
||||
flappybird::ResLoader::init();
|
||||
|
||||
// 进入启动场景
|
||||
app.enterScene(makePtr<flappybird::SplashScene>());
|
||||
|
||||
E2D_LOG_INFO("开始主循环...");
|
||||
app.run();
|
||||
|
||||
E2D_LOG_INFO("应用结束");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
// ============================================================================
|
||||
// Pipe.cpp - 水管类实现
|
||||
// ============================================================================
|
||||
|
||||
#include "Pipe.h"
|
||||
#include "ResLoader.h"
|
||||
#include "BaseScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
Pipe::Pipe() {
|
||||
scored = false;
|
||||
// 注意:不要在构造函数中创建子节点
|
||||
// 因为此时 weak_from_this() 还不能使用
|
||||
}
|
||||
|
||||
void Pipe::onEnter() {
|
||||
Node::onEnter();
|
||||
|
||||
// 在 onEnter 中创建子节点,此时 weak_from_this() 可用
|
||||
if (!topPipe_ && !bottomPipe_) {
|
||||
// 使用游戏逻辑高度
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 获取地面高度
|
||||
auto landFrame = ResLoader::getKeyFrame("land");
|
||||
float landHeight = landFrame ? landFrame->getRect().size.height : 112.0f;
|
||||
|
||||
// 随机生成水管高度
|
||||
// 范围:与屏幕顶部最小距离不小于 100 像素
|
||||
// 与屏幕底部最小距离不小于地面上方 100 像素
|
||||
float minHeight = 100.0f;
|
||||
float maxHeight = screenHeight - landHeight - 100.0f - gapHeight_;
|
||||
float height = static_cast<float>(extra2d::randomInt(static_cast<int>(minHeight), static_cast<int>(maxHeight)));
|
||||
|
||||
// 创建上水管
|
||||
auto topFrame = ResLoader::getKeyFrame("pipe_above");
|
||||
if (topFrame) {
|
||||
topPipe_ = extra2d::Sprite::create(topFrame->getTexture(), topFrame->getRect());
|
||||
topPipe_->setAnchor(extra2d::Vec2(0.5f, 1.0f)); // 锚点设在底部中心
|
||||
topPipe_->setPosition(extra2d::Vec2(0.0f, height - gapHeight_ / 2.0f));
|
||||
addChild(topPipe_);
|
||||
}
|
||||
|
||||
// 创建下水管
|
||||
auto bottomFrame = ResLoader::getKeyFrame("pipe_below");
|
||||
if (bottomFrame) {
|
||||
bottomPipe_ = extra2d::Sprite::create(bottomFrame->getTexture(), bottomFrame->getRect());
|
||||
bottomPipe_->setAnchor(extra2d::Vec2(0.5f, 0.0f)); // 锚点设在顶部中心
|
||||
bottomPipe_->setPosition(extra2d::Vec2(0.0f, height + gapHeight_ / 2.0f));
|
||||
addChild(bottomPipe_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pipe::~Pipe() = default;
|
||||
|
||||
extra2d::Rect Pipe::getBoundingBox() const {
|
||||
// 返回整个水管的边界框(包含上下两根)
|
||||
extra2d::Vec2 pos = getPosition();
|
||||
|
||||
// 水管宽度约为 52
|
||||
float pipeWidth = 52.0f;
|
||||
float halfWidth = pipeWidth / 2.0f;
|
||||
|
||||
// 使用游戏逻辑高度
|
||||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
return extra2d::Rect(
|
||||
pos.x - halfWidth,
|
||||
0.0f,
|
||||
pipeWidth,
|
||||
screenHeight
|
||||
);
|
||||
}
|
||||
|
||||
extra2d::Rect Pipe::getTopPipeBox() const {
|
||||
if (!topPipe_) return extra2d::Rect();
|
||||
|
||||
extra2d::Vec2 pos = getPosition();
|
||||
extra2d::Vec2 topPos = topPipe_->getPosition();
|
||||
|
||||
// 上水管尺寸
|
||||
float pipeWidth = 52.0f;
|
||||
float pipeHeight = 320.0f;
|
||||
|
||||
return extra2d::Rect(
|
||||
pos.x - pipeWidth / 2.0f,
|
||||
pos.y + topPos.y - pipeHeight,
|
||||
pipeWidth,
|
||||
pipeHeight
|
||||
);
|
||||
}
|
||||
|
||||
extra2d::Rect Pipe::getBottomPipeBox() const {
|
||||
if (!bottomPipe_) return extra2d::Rect();
|
||||
|
||||
extra2d::Vec2 pos = getPosition();
|
||||
extra2d::Vec2 bottomPos = bottomPipe_->getPosition();
|
||||
|
||||
// 下水管尺寸
|
||||
float pipeWidth = 52.0f;
|
||||
float pipeHeight = 320.0f;
|
||||
|
||||
return extra2d::Rect(
|
||||
pos.x - pipeWidth / 2.0f,
|
||||
pos.y + bottomPos.y,
|
||||
pipeWidth,
|
||||
pipeHeight
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// ============================================================================
|
||||
// Pipe.h - 水管类
|
||||
// 描述: 游戏中的障碍物,由上下两根水管组成
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 水管类
|
||||
* 由上下两根水管组成的障碍物
|
||||
*/
|
||||
class Pipe : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
Pipe();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~Pipe();
|
||||
|
||||
/**
|
||||
* @brief 进入场景时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 获取边界框(用于碰撞检测)
|
||||
* @return 边界框
|
||||
*/
|
||||
extra2d::Rect getBoundingBox() const override;
|
||||
|
||||
/**
|
||||
* @brief 获取上水管边界框
|
||||
* @return 边界框
|
||||
*/
|
||||
extra2d::Rect getTopPipeBox() const;
|
||||
|
||||
/**
|
||||
* @brief 获取下水管边界框
|
||||
* @return 边界框
|
||||
*/
|
||||
extra2d::Rect getBottomPipeBox() const;
|
||||
|
||||
bool scored = false; // 是否已计分
|
||||
|
||||
private:
|
||||
extra2d::Ptr<extra2d::Sprite> topPipe_; // 上水管
|
||||
extra2d::Ptr<extra2d::Sprite> bottomPipe_; // 下水管
|
||||
float gapHeight_ = 120.0f; // 间隙高度
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// ============================================================================
|
||||
// Pipes.cpp - 水管管理器实现
|
||||
// ============================================================================
|
||||
|
||||
#include "Pipes.h"
|
||||
#include "BaseScene.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
Pipes::Pipes() {
|
||||
pipeCount_ = 0;
|
||||
moving_ = false;
|
||||
|
||||
// 初始化水管数组
|
||||
for (int i = 0; i < maxPipes; ++i) {
|
||||
pipes_[i] = nullptr;
|
||||
}
|
||||
|
||||
// 注意:不要在构造函数中添加水管
|
||||
// 因为此时 weak_from_this() 还不能使用
|
||||
}
|
||||
|
||||
void Pipes::onEnter() {
|
||||
Node::onEnter();
|
||||
// 在 onEnter 中初始化水管,此时 weak_from_this() 可用
|
||||
if (pipeCount_ == 0) {
|
||||
addPipe();
|
||||
addPipe();
|
||||
addPipe();
|
||||
}
|
||||
}
|
||||
|
||||
Pipes::~Pipes() = default;
|
||||
|
||||
void Pipes::onUpdate(float dt) {
|
||||
extra2d::Node::onUpdate(dt);
|
||||
|
||||
if (!moving_) return;
|
||||
|
||||
// 移动所有水管
|
||||
for (int i = 0; i < pipeCount_; ++i) {
|
||||
if (pipes_[i]) {
|
||||
extra2d::Vec2 pos = pipes_[i]->getPosition();
|
||||
pos.x -= pipeSpeed * dt;
|
||||
pipes_[i]->setPosition(pos);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查最前面的水管是否移出屏幕
|
||||
if (pipes_[0] && pipes_[0]->getPosition().x <= -30.0f) {
|
||||
// 移除第一个水管(通过名称查找并移除)
|
||||
// 由于 removeChild 需要 Ptr<Node>,我们使用 removeChildByName 或直接操作
|
||||
// 这里我们直接移除第一个子节点(假设它是水管)
|
||||
auto children = getChildren();
|
||||
if (!children.empty()) {
|
||||
removeChild(children[0]);
|
||||
}
|
||||
|
||||
// 将后面的水管前移
|
||||
for (int i = 0; i < pipeCount_ - 1; ++i) {
|
||||
pipes_[i] = pipes_[i + 1];
|
||||
}
|
||||
pipes_[pipeCount_ - 1] = nullptr;
|
||||
pipeCount_--;
|
||||
|
||||
// 添加新水管
|
||||
addPipe();
|
||||
}
|
||||
}
|
||||
|
||||
void Pipes::addPipe() {
|
||||
if (pipeCount_ >= maxPipes) return;
|
||||
|
||||
// 创建新水管
|
||||
auto pipe = extra2d::makePtr<Pipe>();
|
||||
|
||||
// 设置水管位置
|
||||
if (pipeCount_ == 0) {
|
||||
// 第一个水管在屏幕外 130 像素处
|
||||
pipe->setPosition(extra2d::Vec2(
|
||||
GAME_WIDTH + 130.0f,
|
||||
0.0f
|
||||
));
|
||||
} else {
|
||||
// 其他水管在前一个水管后方
|
||||
float prevX = pipes_[pipeCount_ - 1]->getPosition().x;
|
||||
pipe->setPosition(extra2d::Vec2(prevX + pipeSpacing, 0.0f));
|
||||
}
|
||||
|
||||
// 保存水管指针
|
||||
pipes_[pipeCount_] = pipe.get();
|
||||
pipeCount_++;
|
||||
|
||||
// 添加到场景
|
||||
addChild(pipe);
|
||||
}
|
||||
|
||||
void Pipes::start() {
|
||||
moving_ = true;
|
||||
}
|
||||
|
||||
void Pipes::stop() {
|
||||
moving_ = false;
|
||||
}
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// ============================================================================
|
||||
// Pipes.h - 水管管理器
|
||||
// 描述: 管理多个水管的生成、移动和回收
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
#include "Pipe.h"
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
/**
|
||||
* @brief 水管管理器类
|
||||
* 管理游戏中的所有水管,负责生成、移动和回收
|
||||
*/
|
||||
class Pipes : public extra2d::Node {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
Pipes();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~Pipes();
|
||||
|
||||
/**
|
||||
* @brief 每帧更新
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
/**
|
||||
* @brief 进入场景时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 开始移动水管
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief 停止移动水管
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief 获取当前水管数组
|
||||
* @return 水管指针数组
|
||||
*/
|
||||
Pipe* getPipe(int index) { return (index >= 0 && index < 3) ? pipes_[index] : nullptr; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 添加一根新水管
|
||||
*/
|
||||
void addPipe();
|
||||
|
||||
static constexpr int maxPipes = 3; // 最大水管数量
|
||||
static constexpr float pipeSpeed = 120.0f; // 水管移动速度(像素/秒)
|
||||
static constexpr float pipeSpacing = 200.0f; // 水管间距
|
||||
|
||||
Pipe* pipes_[maxPipes]; // 水管数组
|
||||
int pipeCount_ = 0; // 当前水管数量
|
||||
bool moving_ = false; // 是否正在移动
|
||||
};
|
||||
|
||||
} // namespace flappybird
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
"sprites": [
|
||||
{"name": "splash", "width": 288, "height": 512, "x": 292, "y": 515},
|
||||
{"name": "bg_day", "width": 288, "height": 512, "x": 0, "y": 0},
|
||||
{"name": "bg_night", "width": 288, "height": 512, "x": 292, "y": 0},
|
||||
{"name": "bird0_0", "width": 34, "height": 24, "x": 5, "y": 982},
|
||||
{"name": "bird0_1", "width": 34, "height": 24, "x": 61, "y": 982},
|
||||
{"name": "bird0_2", "width": 34, "height": 24, "x": 117, "y": 982},
|
||||
{"name": "bird1_0", "width": 34, "height": 24, "x": 173, "y": 982},
|
||||
{"name": "bird1_1", "width": 34, "height": 24, "x": 229, "y": 658},
|
||||
{"name": "bird1_2", "width": 34, "height": 24, "x": 229, "y": 710},
|
||||
{"name": "bird2_0", "width": 34, "height": 24, "x": 229, "y": 762},
|
||||
{"name": "bird2_1", "width": 34, "height": 24, "x": 229, "y": 814},
|
||||
{"name": "bird2_2", "width": 34, "height": 24, "x": 229, "y": 866},
|
||||
{"name": "black", "width": 32, "height": 32, "x": 584, "y": 412},
|
||||
{"name": "blink_00", "width": 10, "height": 10, "x": 276, "y": 682},
|
||||
{"name": "blink_01", "width": 10, "height": 10, "x": 276, "y": 734},
|
||||
{"name": "blink_02", "width": 10, "height": 10, "x": 276, "y": 786},
|
||||
{"name": "brand_copyright", "width": 126, "height": 14, "x": 884, "y": 182},
|
||||
{"name": "button_ok", "width": 80, "height": 28, "x": 924, "y": 84},
|
||||
{"name": "button_pause", "width": 26, "height": 28, "x": 242, "y": 612},
|
||||
{"name": "button_rate", "width": 74, "height": 48, "x": 924, "y": 0},
|
||||
{"name": "button_resume", "width": 26, "height": 28, "x": 668, "y": 284},
|
||||
{"name": "button_score", "width": 116, "height": 70, "x": 822, "y": 234},
|
||||
{"name": "button_restart", "width": 116, "height": 70, "x": 702, "y": 316},
|
||||
{"name": "button_share_big", "width": 116, "height": 70, "x": 822, "y": 316},
|
||||
{"name": "button_play", "width": 116, "height": 70, "x": 702, "y": 234},
|
||||
{"name": "button_menu", "width": 80, "height": 28, "x": 924, "y": 52},
|
||||
{"name": "button_share", "width": 80, "height": 28, "x": 584, "y": 284},
|
||||
{"name": "land", "width": 336, "height": 112, "x": 584, "y": 0},
|
||||
{"name": "medals_0", "width": 44, "height": 44, "x": 224, "y": 954},
|
||||
{"name": "medals_1", "width": 44, "height": 44, "x": 224, "y": 906},
|
||||
{"name": "medals_2", "width": 44, "height": 44, "x": 242, "y": 564},
|
||||
{"name": "medals_3", "width": 44, "height": 44, "x": 242, "y": 516},
|
||||
{"name": "new", "width": 32, "height": 14, "x": 224, "y": 1002},
|
||||
{"name": "number_big_0", "width": 24, "height": 36, "x": 992, "y": 120},
|
||||
{"name": "number_big_1", "width": 16, "height": 36, "x": 272, "y": 910},
|
||||
{"name": "number_big_2", "width": 24, "height": 36, "x": 584, "y": 320},
|
||||
{"name": "number_big_3", "width": 24, "height": 36, "x": 612, "y": 320},
|
||||
{"name": "number_big_4", "width": 24, "height": 36, "x": 640, "y": 320},
|
||||
{"name": "number_big_5", "width": 24, "height": 36, "x": 668, "y": 320},
|
||||
{"name": "number_big_6", "width": 24, "height": 36, "x": 584, "y": 368},
|
||||
{"name": "number_big_7", "width": 24, "height": 36, "x": 612, "y": 368},
|
||||
{"name": "number_big_8", "width": 24, "height": 36, "x": 640, "y": 368},
|
||||
{"name": "number_big_9", "width": 24, "height": 36, "x": 668, "y": 368},
|
||||
{"name": "number_medium_0", "width": 16, "height": 20, "x": 272, "y": 612},
|
||||
{"name": "number_medium_1", "width": 16, "height": 20, "x": 272, "y": 954},
|
||||
{"name": "number_medium_2", "width": 16, "height": 20, "x": 272, "y": 978},
|
||||
{"name": "number_medium_3", "width": 16, "height": 20, "x": 260, "y": 1002},
|
||||
{"name": "number_medium_4", "width": 16, "height": 20, "x": 1002, "y": 0},
|
||||
{"name": "number_medium_5", "width": 16, "height": 20, "x": 1002, "y": 24},
|
||||
{"name": "number_medium_6", "width": 16, "height": 20, "x": 1008, "y": 52},
|
||||
{"name": "number_medium_7", "width": 16, "height": 20, "x": 1008, "y": 84},
|
||||
{"name": "number_medium_8", "width": 16, "height": 20, "x": 584, "y": 484},
|
||||
{"name": "number_medium_9", "width": 16, "height": 20, "x": 620, "y": 412},
|
||||
{"name": "number_small_0", "width": 12, "height": 14, "x": 276, "y": 646},
|
||||
{"name": "number_small_1", "width": 12, "height": 14, "x": 276, "y": 664},
|
||||
{"name": "number_small_2", "width": 12, "height": 14, "x": 276, "y": 698},
|
||||
{"name": "number_small_3", "width": 12, "height": 14, "x": 276, "y": 716},
|
||||
{"name": "number_small_4", "width": 12, "height": 14, "x": 276, "y": 750},
|
||||
{"name": "number_small_5", "width": 12, "height": 14, "x": 276, "y": 768},
|
||||
{"name": "number_small_6", "width": 12, "height": 14, "x": 276, "y": 802},
|
||||
{"name": "number_small_7", "width": 12, "height": 14, "x": 276, "y": 820},
|
||||
{"name": "number_small_8", "width": 12, "height": 14, "x": 276, "y": 854},
|
||||
{"name": "number_small_9", "width": 12, "height": 14, "x": 276, "y": 872},
|
||||
{"name": "number_context_10", "width": 12, "height": 14, "x": 992, "y": 164},
|
||||
{"name": "pipe_above_2", "width": 52, "height": 320, "x": 0, "y": 646},
|
||||
{"name": "pipe_below_2", "width": 52, "height": 320, "x": 56, "y": 646},
|
||||
{"name": "pipe_above", "width": 52, "height": 320, "x": 112, "y": 646},
|
||||
{"name": "pipe_below", "width": 52, "height": 320, "x": 168, "y": 646},
|
||||
{"name": "score_panel", "width": 238, "height": 126, "x": 0, "y": 516},
|
||||
{"name": "text_game_over", "width": 204, "height": 54, "x": 784, "y": 116},
|
||||
{"name": "text_ready", "width": 196, "height": 62, "x": 584, "y": 116},
|
||||
{"name": "title", "width": 178, "height": 48, "x": 702, "y": 182},
|
||||
{"name": "tutorial", "width": 114, "height": 98, "x": 584, "y": 182},
|
||||
{"name": "white", "width": 32, "height": 32, "x": 584, "y": 448}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,77 +0,0 @@
|
|||
-- ==============================================
|
||||
-- FlappyBird 示例 - Xmake 构建脚本
|
||||
-- 支持平台: MinGW (Windows), Nintendo Switch
|
||||
-- ==============================================
|
||||
|
||||
-- 获取当前脚本所在目录(示例根目录)
|
||||
local example_dir = os.scriptdir()
|
||||
|
||||
-- 可执行文件目标
|
||||
target("flappy_bird")
|
||||
set_kind("binary")
|
||||
add_files("*.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../Extra2D/include/json")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
if is_plat("switch") then
|
||||
set_targetdir("../../build/examples/flappy_bird")
|
||||
|
||||
-- 构建后生成 NRO 文件
|
||||
after_build(function (target)
|
||||
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||
local elf_file = target:targetfile()
|
||||
local output_dir = path.directory(elf_file)
|
||||
local nacp_file = path.join(output_dir, "flappy_bird.nacp")
|
||||
local nro_file = path.join(output_dir, "flappy_bird.nro")
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
if os.isfile(nacptool) and os.isfile(elf2nro) then
|
||||
os.vrunv(nacptool, {"--create", "FlappyBird", "Extra2D Team", "1.0.0", nacp_file})
|
||||
local romfs = path.join(example_dir, "romfs")
|
||||
if os.isdir(romfs) then
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
|
||||
else
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||
end
|
||||
print("Generated NRO: " .. nro_file)
|
||||
end
|
||||
end)
|
||||
|
||||
-- 打包时将 NRO 文件复制到 package 目录
|
||||
after_package(function (target)
|
||||
local nro_file = path.join(target:targetdir(), "flappy_bird.nro")
|
||||
local package_dir = target:packagedir()
|
||||
if os.isfile(nro_file) and package_dir then
|
||||
os.cp(nro_file, package_dir)
|
||||
print("Copied NRO to package: " .. package_dir)
|
||||
end
|
||||
end)
|
||||
|
||||
elseif is_plat("mingw") then
|
||||
set_targetdir("../../build/examples/flappy_bird")
|
||||
-- add_ldflags("-mwindows", {force = true})
|
||||
|
||||
-- 复制资源到输出目录
|
||||
after_build(function (target)
|
||||
local romfs = path.join(example_dir, "romfs")
|
||||
if os.isdir(romfs) then
|
||||
local target_dir = path.directory(target:targetfile())
|
||||
local assets_dir = path.join(target_dir, "assets")
|
||||
|
||||
-- 创建 assets 目录
|
||||
if not os.isdir(assets_dir) then
|
||||
os.mkdir(assets_dir)
|
||||
end
|
||||
|
||||
-- 复制所有资源文件(包括子目录)
|
||||
os.cp(path.join(romfs, "assets/**"), assets_dir)
|
||||
print("Copied assets from " .. romfs .. " to " .. assets_dir)
|
||||
else
|
||||
print("Warning: romfs directory not found at " .. romfs)
|
||||
end
|
||||
end)
|
||||
end
|
||||
target_end()
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
// ============================================================================
|
||||
// BaseScene.cpp - Push Box 基础场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "BaseScene.h"
|
||||
#include <extra2d/scene/transition_scene.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
BaseScene::BaseScene() {
|
||||
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
|
||||
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void BaseScene::onEnter() {
|
||||
extra2d::Scene::onEnter();
|
||||
// 计算并更新视口
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新视口计算,使游戏内容在窗口中居中显示
|
||||
*/
|
||||
void BaseScene::updateViewport() {
|
||||
auto &app = extra2d::Application::instance();
|
||||
float windowWidth = static_cast<float>(app.window().getWidth());
|
||||
float windowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
// 计算游戏内容在窗口中的居中位置
|
||||
// 保持游戏原始宽高比,进行"黑边"适配
|
||||
float scaleX = windowWidth / GAME_WIDTH;
|
||||
float scaleY = windowHeight / GAME_HEIGHT;
|
||||
// 使用较小的缩放比例,确保游戏内容完整显示在窗口中
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
|
||||
scaledGameWidth_ = GAME_WIDTH * scale;
|
||||
scaledGameHeight_ = GAME_HEIGHT * scale;
|
||||
// 计算居中偏移,使游戏内容在窗口中水平和垂直居中
|
||||
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
|
||||
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
|
||||
|
||||
// 设置视口大小为游戏逻辑分辨率
|
||||
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
// 创建并设置相机
|
||||
auto camera = extra2d::makePtr<extra2d::Camera>();
|
||||
// 设置正交投影,覆盖整个游戏逻辑区域
|
||||
// 注意:对于2D游戏,Y轴向下增长,所以bottom > top
|
||||
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
|
||||
setCamera(camera);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 渲染时调用,设置居中视口
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void BaseScene::onRender(extra2d::RenderBackend &renderer) {
|
||||
// 检查窗口大小是否改变,如果改变则更新视口
|
||||
auto &app = extra2d::Application::instance();
|
||||
float currentWindowWidth = static_cast<float>(app.window().getWidth());
|
||||
float currentWindowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
// 如果窗口大小改变,重新计算视口
|
||||
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
|
||||
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
|
||||
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
|
||||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
|
||||
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
|
||||
"({} x {}), updating viewport",
|
||||
expectedWidth, expectedHeight, currentWindowWidth,
|
||||
currentWindowHeight);
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 设置视口为居中区域
|
||||
renderer.setViewport(
|
||||
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
|
||||
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
|
||||
|
||||
// 调用父类的 onRender 进行实际渲染
|
||||
extra2d::Scene::onRender(renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 渲染场景内容,确保视口正确设置
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void BaseScene::renderContent(extra2d::RenderBackend &renderer) {
|
||||
// 如果视口参数未初始化(onEnter 还没被调用),先初始化
|
||||
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 检查窗口大小是否改变
|
||||
auto &app = extra2d::Application::instance();
|
||||
float currentWindowWidth = static_cast<float>(app.window().getWidth());
|
||||
float currentWindowHeight = static_cast<float>(app.window().getHeight());
|
||||
|
||||
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
|
||||
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
|
||||
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
|
||||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
|
||||
bool isChildOfTransition = false;
|
||||
if (auto parent = getParent()) {
|
||||
if (dynamic_cast<extra2d::TransitionScene *>(parent.get())) {
|
||||
isChildOfTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isChildOfTransition) {
|
||||
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
|
||||
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
|
||||
auto camera = getActiveCamera();
|
||||
if (camera) {
|
||||
// 设置投影矩阵覆盖整个游戏逻辑区域
|
||||
renderer.setViewProjection(camera->getViewProjectionMatrix());
|
||||
}
|
||||
// 渲染场景内容(投影矩阵已设置,直接渲染)
|
||||
batchUpdateTransforms();
|
||||
renderer.beginSpriteBatch();
|
||||
render(renderer);
|
||||
renderer.endSpriteBatch();
|
||||
} else {
|
||||
// 正常渲染时,调用父类的 renderContent 处理视口和投影
|
||||
renderer.setViewport(static_cast<int>(viewportOffsetX_),
|
||||
static_cast<int>(viewportOffsetY_),
|
||||
static_cast<int>(scaledGameWidth_),
|
||||
static_cast<int>(scaledGameHeight_));
|
||||
extra2d::Scene::renderContent(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// ============================================================================
|
||||
// BaseScene.h - Push Box 基础场景类
|
||||
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
// 游戏逻辑分辨率
|
||||
static constexpr float GAME_WIDTH = 640.0f;
|
||||
static constexpr float GAME_HEIGHT = 480.0f;
|
||||
|
||||
/**
|
||||
* @brief Push Box 基础场景类
|
||||
* 所有游戏场景都应继承此类,以获得统一的居中视口适配功能
|
||||
*/
|
||||
class BaseScene : public extra2d::Scene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
BaseScene();
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 渲染时调用,设置居中视口
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void onRender(extra2d::RenderBackend &renderer) override;
|
||||
|
||||
/**
|
||||
* @brief 渲染场景内容,确保视口正确设置
|
||||
* @param renderer 渲染后端
|
||||
*/
|
||||
void renderContent(extra2d::RenderBackend &renderer) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief 更新视口计算,使游戏内容在窗口中居中显示
|
||||
*/
|
||||
void updateViewport();
|
||||
|
||||
// 视口适配参数(用于在窗口中居中显示游戏内容)
|
||||
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
|
||||
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
|
||||
float viewportOffsetX_ = 0.0f; // 视口水平偏移
|
||||
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,487 +0,0 @@
|
|||
// ============================================================================
|
||||
// PlayScene.cpp - Push Box 游戏场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "PlayScene.h"
|
||||
|
||||
#include "StartScene.h"
|
||||
#include "SuccessScene.h"
|
||||
#include "audio_manager.h"
|
||||
#include "storage.h"
|
||||
#include <extra2d/extra2d.h>
|
||||
#include <extra2d/utils/object_pool.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
/**
|
||||
* @brief 加载字体
|
||||
* @param size 字体大小
|
||||
*/
|
||||
static extra2d::Ptr<extra2d::FontAtlas> loadFont(int size) {
|
||||
auto &resources = extra2d::Application::instance().resources();
|
||||
auto font = resources.loadFont("assets/font.ttf", size);
|
||||
return font;
|
||||
}
|
||||
|
||||
PlayScene::PlayScene(int level) : BaseScene() {
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &resources = app.resources();
|
||||
|
||||
E2D_LOG_INFO("PlayScene: Loading textures...");
|
||||
|
||||
texWall_ = resources.loadTexture("assets/images/wall.gif");
|
||||
texPoint_ = resources.loadTexture("assets/images/point.gif");
|
||||
texFloor_ = resources.loadTexture("assets/images/floor.gif");
|
||||
texBox_ = resources.loadTexture("assets/images/box.gif");
|
||||
texBoxInPoint_ = resources.loadTexture("assets/images/boxinpoint.gif");
|
||||
|
||||
texMan_[1] = resources.loadTexture("assets/images/player/manup.gif");
|
||||
texMan_[2] = resources.loadTexture("assets/images/player/mandown.gif");
|
||||
texMan_[3] = resources.loadTexture("assets/images/player/manleft.gif");
|
||||
texMan_[4] = resources.loadTexture("assets/images/player/manright.gif");
|
||||
|
||||
texManPush_[1] = resources.loadTexture("assets/images/player/manhandup.gif");
|
||||
texManPush_[2] =
|
||||
resources.loadTexture("assets/images/player/manhanddown.gif");
|
||||
texManPush_[3] =
|
||||
resources.loadTexture("assets/images/player/manhandleft.gif");
|
||||
texManPush_[4] =
|
||||
resources.loadTexture("assets/images/player/manhandright.gif");
|
||||
|
||||
font28_ = loadFont(28);
|
||||
font20_ = loadFont(20);
|
||||
|
||||
// 使用游戏逻辑分辨率
|
||||
float screenW = GAME_WIDTH;
|
||||
float screenH = GAME_HEIGHT;
|
||||
|
||||
// 计算游戏区域居中偏移
|
||||
float offsetX = (screenW - GAME_WIDTH) / 2.0f;
|
||||
float offsetY = (screenH - GAME_HEIGHT) / 2.0f;
|
||||
|
||||
// 音效开关按钮(使用 Button 的切换模式)
|
||||
auto soundOn = resources.loadTexture("assets/images/soundon.png");
|
||||
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
|
||||
if (soundOn && soundOff) {
|
||||
soundBtn_ = extra2d::Button::create();
|
||||
soundBtn_->setToggleMode(true);
|
||||
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
soundBtn_->setAnchor(0.0f, 0.0f);
|
||||
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
|
||||
soundBtn_->setOnStateChange([](bool isOn) {
|
||||
g_SoundOpen = isOn;
|
||||
AudioManager::instance().setEnabled(isOn);
|
||||
});
|
||||
addChild(soundBtn_);
|
||||
}
|
||||
|
||||
levelText_ = extra2d::Text::create("", font28_);
|
||||
levelText_->setPosition(offsetX + 520.0f, offsetY + 30.0f);
|
||||
levelText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(levelText_);
|
||||
|
||||
stepText_ = extra2d::Text::create("", font20_);
|
||||
stepText_->setPosition(offsetX + 520.0f, offsetY + 100.0f);
|
||||
stepText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(stepText_);
|
||||
|
||||
bestText_ = extra2d::Text::create("", font20_);
|
||||
bestText_->setPosition(offsetX + 520.0f, offsetY + 140.0f);
|
||||
bestText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(bestText_);
|
||||
|
||||
// 创建菜单文本(使用颜色变化指示选中)
|
||||
restartText_ = extra2d::Text::create("Y键重开", font20_);
|
||||
restartText_->setPosition(offsetX + 520.0f, offsetY + 290.0f);
|
||||
addChild(restartText_);
|
||||
|
||||
soundToggleText_ = extra2d::Text::create("X键切换音效", font20_);
|
||||
soundToggleText_->setPosition(offsetX + 520.0f, offsetY + 330.0f);
|
||||
addChild(soundToggleText_);
|
||||
|
||||
// 撤销提示(对象池使用示例)
|
||||
undoText_ = extra2d::Text::create("Z键撤销", font20_);
|
||||
undoText_->setPosition(offsetX + 520.0f, offsetY + 370.0f);
|
||||
addChild(undoText_);
|
||||
|
||||
mapLayer_ = extra2d::makePtr<extra2d::Node>();
|
||||
mapLayer_->setAnchor(0.0f, 0.0f);
|
||||
mapLayer_->setPosition(0.0f, 0.0f);
|
||||
addChild(mapLayer_);
|
||||
|
||||
setLevel(level);
|
||||
}
|
||||
|
||||
void PlayScene::onEnter() {
|
||||
BaseScene::onEnter();
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
updateMenuColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新菜单颜色
|
||||
*/
|
||||
void PlayScene::updateMenuColors() {
|
||||
// 选中的项用红色,未选中的用白色
|
||||
if (restartText_) {
|
||||
restartText_->setTextColor(menuIndex_ == 0 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::White);
|
||||
}
|
||||
if (soundToggleText_) {
|
||||
soundToggleText_->setTextColor(menuIndex_ == 1 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::White);
|
||||
}
|
||||
if (undoText_) {
|
||||
undoText_->setTextColor(menuIndex_ == 2 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::White);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayScene::onUpdate(float dt) {
|
||||
BaseScene::onUpdate(dt);
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &input = app.input();
|
||||
|
||||
// B 键返回主菜单
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
|
||||
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Y 键重开
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::Y)) {
|
||||
setLevel(g_CurrentLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
// X键直接切换音效(备用,按钮也可点击切换)
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
|
||||
g_SoundOpen = !g_SoundOpen;
|
||||
AudioManager::instance().setEnabled(g_SoundOpen);
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Z 键撤销(对象池使用示例)
|
||||
if (input.isKeyPressed(extra2d::Key::Z)) {
|
||||
undoMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// A 键执行选中的菜单项
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
|
||||
executeMenuItem();
|
||||
return;
|
||||
}
|
||||
|
||||
// 方向键移动
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
|
||||
move(0, -1, 1);
|
||||
flush();
|
||||
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
|
||||
move(0, 1, 2);
|
||||
flush();
|
||||
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadLeft)) {
|
||||
move(-1, 0, 3);
|
||||
flush();
|
||||
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadRight)) {
|
||||
move(1, 0, 4);
|
||||
flush();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否通关
|
||||
for (int i = 0; i < map_.width; i++) {
|
||||
for (int j = 0; j < map_.height; j++) {
|
||||
Piece p = map_.value[j][i];
|
||||
if (p.type == TYPE::Box && p.isPoint == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameOver();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 执行选中的菜单项
|
||||
*/
|
||||
void PlayScene::executeMenuItem() {
|
||||
switch (menuIndex_) {
|
||||
case 0: // 重开
|
||||
setLevel(g_CurrentLevel);
|
||||
break;
|
||||
case 1: // 切换音效
|
||||
g_SoundOpen = !g_SoundOpen;
|
||||
AudioManager::instance().setEnabled(g_SoundOpen);
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
break;
|
||||
case 2: // 撤销
|
||||
undoMove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 刷新地图显示
|
||||
*/
|
||||
void PlayScene::flush() {
|
||||
mapLayer_->removeAllChildren();
|
||||
|
||||
int tileW = texFloor_ ? texFloor_->getWidth() : 32;
|
||||
int tileH = texFloor_ ? texFloor_->getHeight() : 32;
|
||||
|
||||
// 使用游戏逻辑分辨率
|
||||
float gameWidth = GAME_WIDTH;
|
||||
float gameHeight = GAME_HEIGHT;
|
||||
float baseOffsetX = 0.0f;
|
||||
float baseOffsetY = 0.0f;
|
||||
|
||||
// 在 12x12 网格中居中地图
|
||||
float mapOffsetX = static_cast<float>((12.0f - map_.width) / 2.0f) * tileW;
|
||||
float mapOffsetY = static_cast<float>((12.0f - map_.height) / 2.0f) * tileH;
|
||||
|
||||
float offsetX = baseOffsetX + mapOffsetX;
|
||||
float offsetY = baseOffsetY + mapOffsetY;
|
||||
|
||||
for (int i = 0; i < map_.width; i++) {
|
||||
for (int j = 0; j < map_.height; j++) {
|
||||
Piece piece = map_.value[j][i];
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> tex;
|
||||
|
||||
if (piece.type == TYPE::Wall) {
|
||||
tex = texWall_;
|
||||
} else if (piece.type == TYPE::Ground && piece.isPoint) {
|
||||
tex = texPoint_;
|
||||
} else if (piece.type == TYPE::Ground) {
|
||||
tex = texFloor_;
|
||||
} else if (piece.type == TYPE::Box && piece.isPoint) {
|
||||
tex = texBoxInPoint_;
|
||||
} else if (piece.type == TYPE::Box) {
|
||||
tex = texBox_;
|
||||
} else if (piece.type == TYPE::Man && g_Pushing) {
|
||||
tex = texManPush_[g_Direct];
|
||||
} else if (piece.type == TYPE::Man) {
|
||||
tex = texMan_[g_Direct];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto sprite = extra2d::Sprite::create(tex);
|
||||
sprite->setAnchor(0.0f, 0.0f);
|
||||
sprite->setPosition(offsetX + static_cast<float>(i * tileW),
|
||||
offsetY + static_cast<float>(j * tileH));
|
||||
mapLayer_->addChild(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置关卡
|
||||
* @param level 关卡编号
|
||||
*/
|
||||
void PlayScene::setLevel(int level) {
|
||||
g_CurrentLevel = level;
|
||||
saveCurrentLevel(g_CurrentLevel);
|
||||
|
||||
// 清空移动历史(智能指针自动回收到对象池)
|
||||
while (!moveHistory_.empty()) {
|
||||
moveHistory_.pop();
|
||||
}
|
||||
|
||||
if (levelText_) {
|
||||
levelText_->setText("第" + std::to_string(level) + "关");
|
||||
}
|
||||
|
||||
setStep(0);
|
||||
|
||||
int bestStep = loadBestStep(level, 0);
|
||||
if (bestText_) {
|
||||
if (bestStep != 0) {
|
||||
bestText_->setText("最佳" + std::to_string(bestStep) + "步");
|
||||
} else {
|
||||
bestText_->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
// 深拷贝地图数据
|
||||
Map &sourceMap = g_Maps[level - 1];
|
||||
map_.width = sourceMap.width;
|
||||
map_.height = sourceMap.height;
|
||||
map_.roleX = sourceMap.roleX;
|
||||
map_.roleY = sourceMap.roleY;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
for (int j = 0; j < 12; j++) {
|
||||
map_.value[i][j] = sourceMap.value[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
g_Direct = 2;
|
||||
g_Pushing = false;
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置步数
|
||||
* @param step 步数
|
||||
*/
|
||||
void PlayScene::setStep(int step) {
|
||||
step_ = step;
|
||||
if (stepText_) {
|
||||
stepText_->setText("当前" + std::to_string(step) + "步");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移动玩家
|
||||
* @param dx X方向偏移
|
||||
* @param dy Y方向偏移
|
||||
* @param direct 方向(1=上,2=下,3=左,4=右)
|
||||
*/
|
||||
void PlayScene::move(int dx, int dy, int direct) {
|
||||
int targetX = dx + map_.roleX;
|
||||
int targetY = dy + map_.roleY;
|
||||
g_Direct = direct;
|
||||
|
||||
if (targetX < 0 || targetX >= map_.width || targetY < 0 ||
|
||||
targetY >= map_.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (map_.value[targetY][targetX].type == TYPE::Wall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用对象池创建移动记录(自动管理内存)
|
||||
auto record = E2D_MAKE_POOLED(MoveRecord, map_.roleX, map_.roleY, targetX, targetY, false);
|
||||
|
||||
if (map_.value[targetY][targetX].type == TYPE::Ground) {
|
||||
g_Pushing = false;
|
||||
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
|
||||
map_.value[targetY][targetX].type = TYPE::Man;
|
||||
AudioManager::instance().playManMove();
|
||||
} else if (map_.value[targetY][targetX].type == TYPE::Box) {
|
||||
g_Pushing = true;
|
||||
|
||||
int boxX = 0;
|
||||
int boxY = 0;
|
||||
switch (g_Direct) {
|
||||
case 1:
|
||||
boxX = targetX;
|
||||
boxY = targetY - 1;
|
||||
break;
|
||||
case 2:
|
||||
boxX = targetX;
|
||||
boxY = targetY + 1;
|
||||
break;
|
||||
case 3:
|
||||
boxX = targetX - 1;
|
||||
boxY = targetY;
|
||||
break;
|
||||
case 4:
|
||||
boxX = targetX + 1;
|
||||
boxY = targetY;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (boxX < 0 || boxX >= map_.width || boxY < 0 || boxY >= map_.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (map_.value[boxY][boxX].type == TYPE::Wall ||
|
||||
map_.value[boxY][boxX].type == TYPE::Box) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录箱子移动
|
||||
record->pushedBox = true;
|
||||
record->boxFromX = targetX;
|
||||
record->boxFromY = targetY;
|
||||
record->boxToX = boxX;
|
||||
record->boxToY = boxY;
|
||||
|
||||
map_.value[boxY][boxX].type = TYPE::Box;
|
||||
map_.value[targetY][targetX].type = TYPE::Man;
|
||||
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
|
||||
|
||||
AudioManager::instance().playBoxMove();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存移动记录到历史栈
|
||||
moveHistory_.push(record);
|
||||
|
||||
map_.roleX = targetX;
|
||||
map_.roleY = targetY;
|
||||
setStep(step_ + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 游戏通关
|
||||
*/
|
||||
void PlayScene::gameOver() {
|
||||
int bestStep = loadBestStep(g_CurrentLevel, 0);
|
||||
if (bestStep == 0 || step_ < bestStep) {
|
||||
saveBestStep(g_CurrentLevel, step_);
|
||||
}
|
||||
|
||||
if (g_CurrentLevel == MAX_LEVEL) {
|
||||
extra2d::Application::instance().scenes().pushScene(
|
||||
extra2d::makePtr<SuccessScene>(), extra2d::TransitionType::Fade, 0.5f);
|
||||
return;
|
||||
}
|
||||
|
||||
setLevel(g_CurrentLevel + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 撤销上一步移动(对象池使用示例)
|
||||
* 智能指针离开作用域时自动回收到对象池
|
||||
*/
|
||||
void PlayScene::undoMove() {
|
||||
if (moveHistory_.empty()) {
|
||||
E2D_LOG_INFO("No moves to undo");
|
||||
return;
|
||||
}
|
||||
|
||||
auto record = moveHistory_.top();
|
||||
moveHistory_.pop();
|
||||
|
||||
// 恢复玩家位置
|
||||
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
|
||||
map_.value[record->fromY][record->fromX].type = TYPE::Man;
|
||||
map_.roleX = record->fromX;
|
||||
map_.roleY = record->fromY;
|
||||
|
||||
// 如果推了箱子,恢复箱子位置
|
||||
if (record->pushedBox) {
|
||||
map_.value[record->boxToY][record->boxToX].type = TYPE::Ground;
|
||||
map_.value[record->boxFromY][record->boxFromX].type = TYPE::Box;
|
||||
}
|
||||
|
||||
// record 智能指针离开作用域后自动回收到对象池
|
||||
setStep(step_ - 1);
|
||||
flush();
|
||||
|
||||
E2D_LOG_INFO("Undo move, step: {}", step_);
|
||||
}
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
// ============================================================================
|
||||
// PlayScene.h - Push Box 游戏场景
|
||||
// ============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseScene.h"
|
||||
#include "data.h"
|
||||
#include <extra2d/extra2d.h>
|
||||
#include <extra2d/utils/object_pool.h>
|
||||
#include <stack>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
/**
|
||||
* @brief Push Box 游戏场景
|
||||
*/
|
||||
class PlayScene : public BaseScene {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* @param level 关卡编号
|
||||
*/
|
||||
explicit PlayScene(int level);
|
||||
|
||||
/**
|
||||
* @brief 场景进入时调用
|
||||
*/
|
||||
void onEnter() override;
|
||||
|
||||
/**
|
||||
* @brief 每帧更新
|
||||
* @param dt 帧间隔时间
|
||||
*/
|
||||
void onUpdate(float dt) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 更新菜单颜色
|
||||
*/
|
||||
void updateMenuColors();
|
||||
|
||||
/**
|
||||
* @brief 执行选中的菜单项
|
||||
*/
|
||||
void executeMenuItem();
|
||||
|
||||
/**
|
||||
* @brief 刷新地图显示
|
||||
*/
|
||||
void flush();
|
||||
|
||||
/**
|
||||
* @brief 设置关卡
|
||||
* @param level 关卡编号
|
||||
*/
|
||||
void setLevel(int level);
|
||||
|
||||
/**
|
||||
* @brief 设置步数
|
||||
* @param step 步数
|
||||
*/
|
||||
void setStep(int step);
|
||||
|
||||
/**
|
||||
* @brief 移动玩家
|
||||
* @param dx X方向偏移
|
||||
* @param dy Y方向偏移
|
||||
* @param direct 方向(1=上,2=下,3=左,4=右)
|
||||
*/
|
||||
void move(int dx, int dy, int direct);
|
||||
|
||||
/**
|
||||
* @brief 游戏通关
|
||||
*/
|
||||
void gameOver();
|
||||
|
||||
/**
|
||||
* @brief 撤销上一步移动(对象池使用示例)
|
||||
*/
|
||||
void undoMove();
|
||||
|
||||
int step_ = 0;
|
||||
int menuIndex_ = 0;
|
||||
Map map_{};
|
||||
|
||||
extra2d::Ptr<extra2d::FontAtlas> font28_;
|
||||
extra2d::Ptr<extra2d::FontAtlas> font20_;
|
||||
|
||||
extra2d::Ptr<extra2d::Text> levelText_;
|
||||
extra2d::Ptr<extra2d::Text> stepText_;
|
||||
extra2d::Ptr<extra2d::Text> bestText_;
|
||||
extra2d::Ptr<extra2d::Text> restartText_;
|
||||
extra2d::Ptr<extra2d::Text> soundToggleText_;
|
||||
extra2d::Ptr<extra2d::Text> undoText_;
|
||||
extra2d::Ptr<extra2d::Node> mapLayer_;
|
||||
|
||||
extra2d::Ptr<extra2d::Button> soundBtn_;
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> texWall_;
|
||||
extra2d::Ptr<extra2d::Texture> texPoint_;
|
||||
extra2d::Ptr<extra2d::Texture> texFloor_;
|
||||
extra2d::Ptr<extra2d::Texture> texBox_;
|
||||
extra2d::Ptr<extra2d::Texture> texBoxInPoint_;
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> texMan_[5];
|
||||
extra2d::Ptr<extra2d::Texture> texManPush_[5];
|
||||
|
||||
// 对象池使用示例:使用智能指针管理 MoveRecord
|
||||
std::stack<extra2d::Ptr<MoveRecord>> moveHistory_;
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
// ============================================================================
|
||||
// StartScene.cpp - Push Box 开始场景实现
|
||||
// ============================================================================
|
||||
|
||||
#include "StartScene.h"
|
||||
|
||||
#include "PlayScene.h"
|
||||
#include "audio_manager.h"
|
||||
#include "data.h"
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
StartScene::StartScene() : BaseScene() {
|
||||
// BaseScene 已处理视口设置
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载菜单字体
|
||||
*/
|
||||
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
|
||||
auto &resources = extra2d::Application::instance().resources();
|
||||
auto font = resources.loadFont("assets/font.ttf", 28, true);
|
||||
return font;
|
||||
}
|
||||
|
||||
void StartScene::onEnter() {
|
||||
BaseScene::onEnter();
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &resources = app.resources();
|
||||
|
||||
if (getChildren().empty()) {
|
||||
// 使用游戏逻辑分辨率
|
||||
float screenW = GAME_WIDTH;
|
||||
float screenH = GAME_HEIGHT;
|
||||
|
||||
auto bgTex = resources.loadTexture("assets/images/start.jpg");
|
||||
if (bgTex) {
|
||||
auto background = extra2d::Sprite::create(bgTex);
|
||||
float bgWidth = static_cast<float>(bgTex->getWidth());
|
||||
float bgHeight = static_cast<float>(bgTex->getHeight());
|
||||
float offsetX = (screenW - bgWidth) / 2.0f;
|
||||
float offsetY = (screenH - bgHeight) / 2.0f;
|
||||
|
||||
background->setAnchor(0.0f, 0.0f);
|
||||
background->setPosition(offsetX, offsetY);
|
||||
addChild(background);
|
||||
|
||||
float centerX = screenW / 2.0f;
|
||||
|
||||
font_ = loadMenuFont();
|
||||
if (!font_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建菜单按钮(使用 Button 实现文本居中)
|
||||
startBtn_ = extra2d::Button::create();
|
||||
startBtn_->setFont(font_);
|
||||
startBtn_->setText("新游戏");
|
||||
startBtn_->setTextColor(extra2d::Colors::Black);
|
||||
startBtn_->setBackgroundColor(extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent);
|
||||
startBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
|
||||
startBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
|
||||
startBtn_->setCustomSize(200.0f, 40.0f);
|
||||
startBtn_->setAnchor(0.5f, 0.5f);
|
||||
startBtn_->setPosition(centerX, offsetY + 260.0f);
|
||||
addChild(startBtn_);
|
||||
|
||||
resumeBtn_ = extra2d::Button::create();
|
||||
resumeBtn_->setFont(font_);
|
||||
resumeBtn_->setText("继续关卡");
|
||||
resumeBtn_->setTextColor(extra2d::Colors::Black);
|
||||
resumeBtn_->setBackgroundColor(extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent);
|
||||
resumeBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
|
||||
resumeBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
|
||||
resumeBtn_->setCustomSize(200.0f, 40.0f);
|
||||
resumeBtn_->setAnchor(0.5f, 0.5f);
|
||||
resumeBtn_->setPosition(centerX, offsetY + 300.0f);
|
||||
addChild(resumeBtn_);
|
||||
|
||||
exitBtn_ = extra2d::Button::create();
|
||||
exitBtn_->setFont(font_);
|
||||
exitBtn_->setText("退出");
|
||||
exitBtn_->setTextColor(extra2d::Colors::Black);
|
||||
exitBtn_->setBackgroundColor(extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent,
|
||||
extra2d::Colors::Transparent);
|
||||
exitBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
|
||||
exitBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
|
||||
exitBtn_->setCustomSize(200.0f, 40.0f);
|
||||
exitBtn_->setAnchor(0.5f, 0.5f);
|
||||
exitBtn_->setPosition(centerX, offsetY + 340.0f);
|
||||
addChild(exitBtn_);
|
||||
|
||||
// 音效开关按钮(使用 Button 的切换模式)
|
||||
auto soundOn = resources.loadTexture("assets/images/soundon.png");
|
||||
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
|
||||
if (soundOn && soundOff) {
|
||||
soundBtn_ = extra2d::Button::create();
|
||||
soundBtn_->setToggleMode(true);
|
||||
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
soundBtn_->setAnchor(0.0f, 0.0f);
|
||||
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
|
||||
soundBtn_->setOnStateChange([](bool isOn) {
|
||||
g_SoundOpen = isOn;
|
||||
AudioManager::instance().setEnabled(isOn);
|
||||
});
|
||||
addChild(soundBtn_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 始终有3个菜单项
|
||||
menuCount_ = 3;
|
||||
updateMenuColors();
|
||||
}
|
||||
|
||||
void StartScene::onUpdate(float dt) {
|
||||
BaseScene::onUpdate(dt);
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &input = app.input();
|
||||
|
||||
// 方向键上下切换选择
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
|
||||
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
|
||||
updateMenuColors();
|
||||
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
|
||||
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
|
||||
updateMenuColors();
|
||||
}
|
||||
|
||||
// A键确认
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
|
||||
executeMenuItem();
|
||||
}
|
||||
|
||||
// X键切换音效(备用,按钮也可点击切换)
|
||||
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
|
||||
g_SoundOpen = !g_SoundOpen;
|
||||
AudioManager::instance().setEnabled(g_SoundOpen);
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新菜单颜色
|
||||
*/
|
||||
void StartScene::updateMenuColors() {
|
||||
// 根据选中状态更新按钮文本颜色
|
||||
// 选中的项用红色,未选中的用黑色,禁用的项用深灰色
|
||||
|
||||
if (startBtn_) {
|
||||
startBtn_->setTextColor(selectedIndex_ == 0 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::Black);
|
||||
}
|
||||
|
||||
if (resumeBtn_) {
|
||||
// "继续关卡"始终显示,但当 g_CurrentLevel == 1 时禁用(深灰色)
|
||||
if (g_CurrentLevel > 1) {
|
||||
// 可用状态:选中为红色,未选中为黑色
|
||||
resumeBtn_->setTextColor(selectedIndex_ == 1 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::Black);
|
||||
} else {
|
||||
// 禁用状态:深灰色 (RGB: 80, 80, 80)
|
||||
resumeBtn_->setTextColor(extra2d::Color(80, 80, 80, 255));
|
||||
}
|
||||
}
|
||||
|
||||
if (exitBtn_) {
|
||||
exitBtn_->setTextColor(selectedIndex_ == 2 ? extra2d::Colors::Red
|
||||
: extra2d::Colors::Black);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 执行选中的菜单项
|
||||
*/
|
||||
void StartScene::executeMenuItem() {
|
||||
// 始终有3个选项,但"继续关卡"(索引1)在 g_CurrentLevel == 1 时禁用
|
||||
switch (selectedIndex_) {
|
||||
case 0:
|
||||
startNewGame();
|
||||
break;
|
||||
case 1:
|
||||
// 只有当 g_CurrentLevel > 1 时才能选择"继续关卡"
|
||||
if (g_CurrentLevel > 1) {
|
||||
continueGame();
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
exitGame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开始新游戏
|
||||
*/
|
||||
void StartScene::startNewGame() {
|
||||
extra2d::Application::instance().scenes().replaceScene(
|
||||
extra2d::makePtr<PlayScene>(1), extra2d::TransitionType::Fade, 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 继续游戏
|
||||
*/
|
||||
void StartScene::continueGame() {
|
||||
extra2d::Application::instance().scenes().replaceScene(
|
||||
extra2d::makePtr<PlayScene>(g_CurrentLevel),
|
||||
extra2d::TransitionType::Fade, 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 退出游戏
|
||||
*/
|
||||
void StartScene::exitGame() { extra2d::Application::instance().quit(); }
|
||||
|
||||
} // namespace pushbox
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue