refactor(animation): 移除旧动画系统并添加Tween动画支持
重构动画系统,删除旧动画组件(AnimationClip, AnimatedSprite等),添加新的Tween动画系统。主要变更包括: 1. 删除旧的动画相关文件(animation_clip, animation_frame等) 2. 实现新的Tween动画系统,支持多种缓动函数 3. 更新flappy_bird示例使用新的Tween系统 4. 调整include路径和xmake配置 5. 添加输入系统对控制器和鼠标滚轮的支持
This commit is contained in:
parent
5039b1d9fc
commit
a6c1f66fff
|
|
@ -10,7 +10,7 @@ local example_dir = os.scriptdir()
|
|||
target("collision_demo")
|
||||
set_kind("binary")
|
||||
add_files("main.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "Number.h"
|
||||
#include "ResLoader.h"
|
||||
#include "StartScene.h"
|
||||
#include <animation/tween.h>
|
||||
|
||||
namespace flappybird {
|
||||
|
||||
|
|
@ -25,7 +26,8 @@ void GameOverLayer::onEnter() {
|
|||
float screenHeight = GAME_HEIGHT;
|
||||
|
||||
// 整体居中(x 坐标相对于屏幕中心)
|
||||
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight));
|
||||
// 初始位置在屏幕底部,然后向上滑出
|
||||
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight + 200.0f));
|
||||
|
||||
// 显示 "Game Over" 文字(y=120,从顶部开始)
|
||||
auto gameOverFrame = ResLoader::getKeyFrame("text_game_over");
|
||||
|
|
@ -43,14 +45,23 @@ void GameOverLayer::onEnter() {
|
|||
// 初始化按钮
|
||||
initButtons();
|
||||
|
||||
// 动画完成,启用按钮
|
||||
animationDone_ = true;
|
||||
if (restartBtn_)
|
||||
restartBtn_->setEnabled(true);
|
||||
if (menuBtn_)
|
||||
menuBtn_->setEnabled(true);
|
||||
if (shareBtn_)
|
||||
shareBtn_->setEnabled(true);
|
||||
// 向上滑出动画
|
||||
extra2d::TweenOptions opts;
|
||||
opts.easing = extra2d::TweenEasing::BackOut;
|
||||
tween()
|
||||
.to(0.5f,
|
||||
extra2d::tween::pos(screenWidth / 2.0f, screenHeight / 2.0f - 300.0f),
|
||||
opts)
|
||||
.call([this]() {
|
||||
animationDone_ = true;
|
||||
if (restartBtn_)
|
||||
restartBtn_->setEnabled(true);
|
||||
if (menuBtn_)
|
||||
menuBtn_->setEnabled(true);
|
||||
if (shareBtn_)
|
||||
shareBtn_->setEnabled(true);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
void GameOverLayer::initPanel(int score, float screenHeight) {
|
||||
|
|
|
|||
|
|
@ -8,63 +8,78 @@
|
|||
namespace flappybird {
|
||||
|
||||
Bird::Bird() {
|
||||
// 注意:不要在构造函数中调用 initAnimations()
|
||||
// 因为此时 weak_from_this() 还不能使用
|
||||
setStatus(Status::Idle);
|
||||
}
|
||||
|
||||
Bird::~Bird() = default;
|
||||
|
||||
void Bird::onEnter() {
|
||||
Node::onEnter();
|
||||
// 在 onEnter 中初始化动画,此时 weak_from_this() 可用
|
||||
if (!animSprite_) {
|
||||
if (!sprite_) {
|
||||
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
|
||||
// 加载动画帧序列: 0 -> 1 -> 2 -> 1
|
||||
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));
|
||||
frames_.push_back(frameSprite);
|
||||
} 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() ? "有" : "无");
|
||||
// 创建精灵
|
||||
if (!frames_.empty()) {
|
||||
sprite_ = extra2d::Sprite::create();
|
||||
setCurrentFrame(0);
|
||||
addChild(sprite_);
|
||||
E2D_LOG_INFO("小鸟动画创建成功: 颜色={}, 帧数={}", colorMode, frames_.size());
|
||||
} else {
|
||||
E2D_LOG_ERROR("小鸟动画创建失败: 没有找到任何动画帧");
|
||||
}
|
||||
}
|
||||
|
||||
void Bird::setCurrentFrame(int frameIndex) {
|
||||
if (frames_.empty() || !sprite_) return;
|
||||
|
||||
frameIndex = frameIndex % static_cast<int>(frames_.size());
|
||||
currentFrame_ = frameIndex;
|
||||
|
||||
auto& frame = frames_[frameIndex];
|
||||
sprite_->setTexture(frame->getTexture());
|
||||
sprite_->setTextureRect(frame->getRect());
|
||||
}
|
||||
|
||||
void Bird::updateFrameAnimation(float dt) {
|
||||
if (frames_.empty() || status_ == Status::Still) return;
|
||||
|
||||
frameTimer_ += dt;
|
||||
|
||||
float interval = frameInterval_;
|
||||
if (status_ == Status::StartToFly) {
|
||||
interval = 0.05f; // 2倍速度
|
||||
}
|
||||
|
||||
while (frameTimer_ >= interval) {
|
||||
frameTimer_ -= interval;
|
||||
setCurrentFrame((currentFrame_ + 1) % static_cast<int>(frames_.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void Bird::onUpdate(float dt) {
|
||||
extra2d::Node::onUpdate(dt);
|
||||
|
||||
// 更新帧动画
|
||||
updateFrameAnimation(dt);
|
||||
|
||||
// 处理闲置动画(上下浮动)
|
||||
if (status_ == Status::Idle) {
|
||||
idleTimer_ += dt;
|
||||
|
|
@ -73,15 +88,15 @@ void Bird::onUpdate(float dt) {
|
|||
}
|
||||
|
||||
void Bird::onRender(extra2d::RenderBackend& renderer) {
|
||||
// 动画精灵会自动渲染,这里只需要处理旋转和偏移
|
||||
if (animSprite_) {
|
||||
animSprite_->setRotation(rotation_);
|
||||
// 精灵会自动渲染,这里只需要处理旋转和偏移
|
||||
if (sprite_) {
|
||||
sprite_->setRotation(rotation_);
|
||||
|
||||
// 应用闲置偏移
|
||||
if (status_ == Status::Idle) {
|
||||
animSprite_->setPosition(extra2d::Vec2(0.0f, idleOffset_));
|
||||
sprite_->setPosition(extra2d::Vec2(0.0f, idleOffset_));
|
||||
} else {
|
||||
animSprite_->setPosition(extra2d::Vec2(0.0f, 0.0f));
|
||||
sprite_->setPosition(extra2d::Vec2(0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,26 +157,18 @@ void Bird::setStatus(Status status) {
|
|||
switch (status) {
|
||||
case Status::Still:
|
||||
// 停止所有动画
|
||||
if (animSprite_) {
|
||||
animSprite_->pause();
|
||||
}
|
||||
frameTimer_ = 0.0f;
|
||||
break;
|
||||
|
||||
case Status::Idle:
|
||||
// 开始闲置动画
|
||||
if (animSprite_) {
|
||||
animSprite_->setPlaybackSpeed(1.0f); // 正常速度
|
||||
animSprite_->play();
|
||||
}
|
||||
frameInterval_ = 0.1f; // 正常速度
|
||||
idleTimer_ = 0.0f;
|
||||
break;
|
||||
|
||||
case Status::StartToFly:
|
||||
// 停止闲置动画,加速翅膀扇动
|
||||
idleOffset_ = 0.0f;
|
||||
if (animSprite_) {
|
||||
animSprite_->setPlaybackSpeed(2.0f); // 2倍速度 = 0.05秒每帧
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::Fly:
|
||||
|
|
|
|||
|
|
@ -98,15 +98,29 @@ private:
|
|||
*/
|
||||
void initAnimations();
|
||||
|
||||
/**
|
||||
* @brief 更新帧动画
|
||||
*/
|
||||
void updateFrameAnimation(float dt);
|
||||
|
||||
/**
|
||||
* @brief 设置当前帧
|
||||
*/
|
||||
void setCurrentFrame(int frameIndex);
|
||||
|
||||
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; // 闲置偏移量
|
||||
extra2d::Ptr<extra2d::Sprite> sprite_; // 精灵
|
||||
std::vector<extra2d::Ptr<extra2d::SpriteFrame>> frames_; // 动画帧
|
||||
int currentFrame_ = 0; // 当前帧索引
|
||||
float frameTimer_ = 0.0f; // 帧计时器
|
||||
float frameInterval_ = 0.1f; // 帧间隔(秒)
|
||||
float idleTimer_ = 0.0f; // 闲置动画计时器
|
||||
float idleOffset_ = 0.0f; // 闲置偏移量
|
||||
|
||||
// 物理常量
|
||||
static constexpr float gravity = 1440.0f; // 重力加速度
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local example_dir = os.scriptdir()
|
|||
target("flappy_bird")
|
||||
set_kind("binary")
|
||||
add_files("*.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local example_dir = os.scriptdir()
|
|||
target("hello_world")
|
||||
set_kind("binary")
|
||||
add_files("main.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local example_dir = os.scriptdir()
|
|||
target("push_box")
|
||||
set_kind("binary")
|
||||
add_files("*.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local example_dir = os.scriptdir()
|
|||
target("spatial_index_demo")
|
||||
set_kind("binary")
|
||||
add_files("main.cpp")
|
||||
add_includedirs("../../Extra2D/include")
|
||||
add_includedirs("../../include")
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 使用与主项目相同的平台配置
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <core/math_types.h>
|
||||
#include <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 <animation/ani_parser.h>
|
||||
#include <animation/animation_cache.h>
|
||||
#include <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 <animation/animation_cache.h>
|
||||
#include <animation/animation_clip.h>
|
||||
#include <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 <animation/animation_cache.h>
|
||||
#include <animation/animation_controller.h>
|
||||
#include <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 <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 <animation/animation_frame.h>
|
||||
#include <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 <animation/animation_clip.h>
|
||||
#include <animation/interpolation_engine.h>
|
||||
#include <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 <animation/frame_property.h>
|
||||
#include <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 <animation/animation_cache.h>
|
||||
#include <animation/animation_clip.h>
|
||||
#include <animation/animation_controller.h>
|
||||
#include <animation/animation_event.h>
|
||||
#include <animation/frame_renderer.h>
|
||||
#include <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 <animation/als_parser.h>
|
||||
#include <animation/animation_event.h>
|
||||
#include <animation/animation_node.h>
|
||||
#include <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 <core/color.h>
|
||||
#include <core/math_types.h>
|
||||
#include <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 <animation/animation_frame.h>
|
||||
#include <animation/interpolation_engine.h>
|
||||
#include <animation/sprite_frame.h>
|
||||
#include <animation/sprite_frame_cache.h>
|
||||
#include <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 <animation/animation_frame.h>
|
||||
#include <core/color.h>
|
||||
#include <core/math_types.h>
|
||||
#include <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
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
#pragma once
|
||||
|
||||
#include <animation/tween_easing.h>
|
||||
#include <core/color.h>
|
||||
#include <core/math_types.h>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
class Node;
|
||||
|
||||
struct TweenOptions {
|
||||
TweenEasing easing = TweenEasing::Linear;
|
||||
std::function<void()> onStart = nullptr;
|
||||
std::function<void(float progress)> onUpdate = nullptr;
|
||||
std::function<void()> onComplete = nullptr;
|
||||
};
|
||||
|
||||
struct TweenProperty {
|
||||
std::optional<Vec2> position;
|
||||
std::optional<Vec2> scale;
|
||||
std::optional<float> rotation;
|
||||
std::optional<float> opacity;
|
||||
std::optional<Color> color;
|
||||
};
|
||||
|
||||
class TweenAction {
|
||||
public:
|
||||
enum class Type : uint8_t {
|
||||
Interval,
|
||||
Delay,
|
||||
Call,
|
||||
Parallel,
|
||||
};
|
||||
|
||||
virtual ~TweenAction() = default;
|
||||
virtual void update(float dt) = 0;
|
||||
virtual bool isFinished() const = 0;
|
||||
virtual float getDuration() const = 0;
|
||||
virtual float getElapsed() const = 0;
|
||||
virtual void reset() = 0;
|
||||
|
||||
Type getType() const { return type_; }
|
||||
|
||||
protected:
|
||||
TweenAction(Type type) : type_(type) {}
|
||||
Type type_;
|
||||
};
|
||||
|
||||
class Tween {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<Tween>;
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
explicit Tween(Node *target);
|
||||
Tween(Node *target, const std::string &name);
|
||||
~Tween();
|
||||
|
||||
Tween(const Tween &) = delete;
|
||||
Tween &operator=(const Tween &) = delete;
|
||||
Tween(Tween &&) = default;
|
||||
Tween &operator=(Tween &&) = default;
|
||||
|
||||
static Tween create(Node *target);
|
||||
static Tween create(Node *target, const std::string &name);
|
||||
|
||||
Tween &to(float duration, const TweenProperty &props,
|
||||
const TweenOptions &options = {});
|
||||
Tween &by(float duration, const TweenProperty &props,
|
||||
const TweenOptions &options = {});
|
||||
|
||||
Tween &set(const TweenProperty &props);
|
||||
Tween &delay(float seconds);
|
||||
Tween &call(Callback callback);
|
||||
|
||||
Tween ¶llel();
|
||||
Tween &endParallel();
|
||||
|
||||
Tween &union_();
|
||||
Tween &repeat(int times);
|
||||
Tween &repeatForever();
|
||||
Tween &yoyo(bool enabled = true);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void pause();
|
||||
void resume();
|
||||
|
||||
void update(float dt);
|
||||
|
||||
bool isFinished() const;
|
||||
bool isPlaying() const;
|
||||
bool isPaused() const;
|
||||
float getTotalDuration() const;
|
||||
|
||||
void setName(const std::string &name) { name_ = name; }
|
||||
const std::string &getName() const { return name_; }
|
||||
|
||||
private:
|
||||
Node *target_ = nullptr;
|
||||
std::string name_;
|
||||
std::vector<std::unique_ptr<TweenAction>> actions_;
|
||||
size_t currentActionIndex_ = 0;
|
||||
|
||||
bool playing_ = false;
|
||||
bool paused_ = false;
|
||||
bool finished_ = false;
|
||||
|
||||
int repeatCount_ = 1;
|
||||
int currentRepeat_ = 0;
|
||||
bool yoyo_ = false;
|
||||
bool reverse_ = false;
|
||||
bool repeatForever_ = false;
|
||||
|
||||
bool inParallel_ = false;
|
||||
std::vector<std::unique_ptr<TweenAction>> parallelActions_;
|
||||
|
||||
void addAction(std::unique_ptr<TweenAction> action);
|
||||
void advanceToNextAction();
|
||||
void resetAllActions();
|
||||
};
|
||||
|
||||
namespace tween {
|
||||
|
||||
inline TweenProperty pos(const Vec2 &p) {
|
||||
TweenProperty props;
|
||||
props.position = p;
|
||||
return props;
|
||||
}
|
||||
|
||||
inline TweenProperty pos(float x, float y) { return pos(Vec2(x, y)); }
|
||||
|
||||
inline TweenProperty scl(const Vec2 &s) {
|
||||
TweenProperty props;
|
||||
props.scale = s;
|
||||
return props;
|
||||
}
|
||||
|
||||
inline TweenProperty scl(float s) { return scl(Vec2(s, s)); }
|
||||
|
||||
inline TweenProperty rot(float degrees) {
|
||||
TweenProperty props;
|
||||
props.rotation = degrees;
|
||||
return props;
|
||||
}
|
||||
|
||||
inline TweenProperty opa(float opacity) {
|
||||
TweenProperty props;
|
||||
props.opacity = opacity;
|
||||
return props;
|
||||
}
|
||||
|
||||
inline TweenProperty col(const Color &c) {
|
||||
TweenProperty props;
|
||||
props.color = c;
|
||||
return props;
|
||||
}
|
||||
|
||||
TweenProperty combine(std::initializer_list<TweenProperty> props);
|
||||
|
||||
} // namespace tween
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
enum class TweenEasing : uint8_t {
|
||||
Linear,
|
||||
QuadIn,
|
||||
QuadOut,
|
||||
QuadInOut,
|
||||
CubicIn,
|
||||
CubicOut,
|
||||
CubicInOut,
|
||||
QuartIn,
|
||||
QuartOut,
|
||||
QuartInOut,
|
||||
QuintIn,
|
||||
QuintOut,
|
||||
QuintInOut,
|
||||
SineIn,
|
||||
SineOut,
|
||||
SineInOut,
|
||||
ExpoIn,
|
||||
ExpoOut,
|
||||
ExpoInOut,
|
||||
CircIn,
|
||||
CircOut,
|
||||
CircInOut,
|
||||
ElasticIn,
|
||||
ElasticOut,
|
||||
ElasticInOut,
|
||||
BackIn,
|
||||
BackOut,
|
||||
BackInOut,
|
||||
BounceIn,
|
||||
BounceOut,
|
||||
BounceInOut,
|
||||
Smooth,
|
||||
Fade,
|
||||
};
|
||||
|
||||
class EasingFunctions {
|
||||
public:
|
||||
using EasingFunc = float (*)(float);
|
||||
|
||||
static float apply(float t, TweenEasing easing);
|
||||
static EasingFunc get(TweenEasing easing);
|
||||
|
||||
static float linear(float t);
|
||||
static float quadIn(float t);
|
||||
static float quadOut(float t);
|
||||
static float quadInOut(float t);
|
||||
static float cubicIn(float t);
|
||||
static float cubicOut(float t);
|
||||
static float cubicInOut(float t);
|
||||
static float quartIn(float t);
|
||||
static float quartOut(float t);
|
||||
static float quartInOut(float t);
|
||||
static float quintIn(float t);
|
||||
static float quintOut(float t);
|
||||
static float quintInOut(float t);
|
||||
static float sineIn(float t);
|
||||
static float sineOut(float t);
|
||||
static float sineInOut(float t);
|
||||
static float expoIn(float t);
|
||||
static float expoOut(float t);
|
||||
static float expoInOut(float t);
|
||||
static float circIn(float t);
|
||||
static float circOut(float t);
|
||||
static float circInOut(float t);
|
||||
static float elasticIn(float t);
|
||||
static float elasticOut(float t);
|
||||
static float elasticInOut(float t);
|
||||
static float backIn(float t);
|
||||
static float backOut(float t);
|
||||
static float backInOut(float t);
|
||||
static float bounceIn(float t);
|
||||
static float bounceOut(float t);
|
||||
static float bounceInOut(float t);
|
||||
static float smooth(float t);
|
||||
static float fade(float t);
|
||||
|
||||
private:
|
||||
static constexpr float PI = 3.14159265358979323846f;
|
||||
static constexpr float HALF_PI = 1.57079632679489661923f;
|
||||
static constexpr float BACK_CONST = 1.70158f;
|
||||
static constexpr float ELASTIC_CONST = 2.0f * PI / 3.0f;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -39,20 +39,8 @@
|
|||
// Animation
|
||||
#include <animation/sprite_frame.h>
|
||||
#include <animation/sprite_frame_cache.h>
|
||||
#include <animation/frame_property.h>
|
||||
#include <animation/animation_frame.h>
|
||||
#include <animation/animation_clip.h>
|
||||
#include <animation/animation_controller.h>
|
||||
#include <animation/animation_cache.h>
|
||||
#include <animation/interpolation_engine.h>
|
||||
#include <animation/animated_sprite.h>
|
||||
#include <animation/frame_renderer.h>
|
||||
#include <animation/animation_event.h>
|
||||
#include <animation/animation_node.h>
|
||||
#include <animation/composite_animation.h>
|
||||
#include <animation/ani_parser.h>
|
||||
#include <animation/ani_binary_parser.h>
|
||||
#include <animation/als_parser.h>
|
||||
#include <animation/tween_easing.h>
|
||||
#include <animation/tween.h>
|
||||
|
||||
// UI
|
||||
#include <ui/widget.h>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 鼠标按钮枚举
|
||||
// ============================================================================
|
||||
enum class MouseButton {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
|
|
@ -24,43 +21,27 @@ enum class MouseButton {
|
|||
Count = 8
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Input 类 - 跨平台输入管理
|
||||
// 支持: 键盘、鼠标、手柄、触摸屏
|
||||
// ============================================================================
|
||||
class Input {
|
||||
public:
|
||||
Input();
|
||||
~Input();
|
||||
|
||||
// 初始化
|
||||
void init();
|
||||
void shutdown();
|
||||
|
||||
// 每帧更新
|
||||
void update();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 键盘输入
|
||||
// ------------------------------------------------------------------------
|
||||
bool isKeyDown(int keyCode) const;
|
||||
bool isKeyPressed(int keyCode) const;
|
||||
bool isKeyReleased(int keyCode) const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 手柄按钮
|
||||
// ------------------------------------------------------------------------
|
||||
bool isButtonDown(int button) const;
|
||||
bool isButtonPressed(int button) const;
|
||||
bool isButtonReleased(int button) const;
|
||||
|
||||
// 摇杆
|
||||
Vec2 getLeftStick() const;
|
||||
Vec2 getRightStick() const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 鼠标输入
|
||||
// ------------------------------------------------------------------------
|
||||
bool isMouseDown(MouseButton button) const;
|
||||
bool isMousePressed(MouseButton button) const;
|
||||
bool isMouseReleased(MouseButton button) const;
|
||||
|
|
@ -74,40 +55,34 @@ public:
|
|||
void setMouseVisible(bool visible);
|
||||
void setMouseLocked(bool locked);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 触摸屏 (Switch 原生支持,PC 端模拟或禁用)
|
||||
// ------------------------------------------------------------------------
|
||||
bool isTouching() const { return touching_; }
|
||||
Vec2 getTouchPosition() const { return touchPosition_; }
|
||||
int getTouchCount() const { return touchCount_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 便捷方法
|
||||
// ------------------------------------------------------------------------
|
||||
bool isAnyKeyDown() const;
|
||||
bool isAnyMouseDown() const;
|
||||
|
||||
void onControllerAdded(int deviceIndex);
|
||||
void onControllerRemoved(int instanceId);
|
||||
void onMouseWheel(float x, float y);
|
||||
|
||||
private:
|
||||
static constexpr int MAX_BUTTONS = SDL_CONTROLLER_BUTTON_MAX;
|
||||
static constexpr int MAX_KEYS = SDL_NUM_SCANCODES;
|
||||
|
||||
SDL_GameController *controller_;
|
||||
|
||||
// 键盘状态 (PC 端使用)
|
||||
std::array<bool, MAX_KEYS> keysDown_;
|
||||
std::array<bool, MAX_KEYS> prevKeysDown_;
|
||||
|
||||
// 手柄按钮状态
|
||||
std::array<bool, MAX_BUTTONS> buttonsDown_;
|
||||
std::array<bool, MAX_BUTTONS> prevButtonsDown_;
|
||||
|
||||
// 摇杆状态
|
||||
float leftStickX_;
|
||||
float leftStickY_;
|
||||
float rightStickX_;
|
||||
float rightStickY_;
|
||||
|
||||
// 鼠标状态 (PC 端使用)
|
||||
Vec2 mousePosition_;
|
||||
Vec2 prevMousePosition_;
|
||||
float mouseScroll_;
|
||||
|
|
@ -115,26 +90,15 @@ private:
|
|||
std::array<bool, 8> mouseButtonsDown_;
|
||||
std::array<bool, 8> prevMouseButtonsDown_;
|
||||
|
||||
// 触摸屏状态 (Switch 原生)
|
||||
bool touching_;
|
||||
bool prevTouching_;
|
||||
Vec2 touchPosition_;
|
||||
Vec2 prevTouchPosition_;
|
||||
int touchCount_;
|
||||
|
||||
// 映射键盘 keyCode 到 SDL GameController 按钮 (Switch 兼容模式)
|
||||
SDL_GameControllerButton mapKeyToButton(int keyCode) const;
|
||||
|
||||
// 更新键盘状态
|
||||
void updateKeyboard();
|
||||
|
||||
// 更新鼠标状态
|
||||
void updateMouse();
|
||||
|
||||
// 更新手柄状态
|
||||
void updateGamepad();
|
||||
|
||||
// 更新触摸屏状态
|
||||
void updateTouch();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <event/event_dispatcher.h>
|
||||
#include <graphics/render_backend.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ namespace extra2d {
|
|||
class Scene;
|
||||
class RenderBackend;
|
||||
struct RenderCommand;
|
||||
class Tween;
|
||||
enum class TweenEasing : uint8_t;
|
||||
|
||||
// ============================================================================
|
||||
// 节点基类 - 场景图的基础
|
||||
|
|
@ -159,6 +162,83 @@ public:
|
|||
// ------------------------------------------------------------------------
|
||||
EventDispatcher &getEventDispatcher() { return eventDispatcher_; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Tween 动画系统
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 创建 Tween 动画
|
||||
* @return Tween 对象引用,支持链式调用
|
||||
*/
|
||||
Tween& tween();
|
||||
|
||||
/**
|
||||
* @brief 创建具名 Tween 动画
|
||||
*/
|
||||
Tween& tween(const std::string &name);
|
||||
|
||||
/**
|
||||
* @brief 移动到目标位置
|
||||
*/
|
||||
Tween &moveTo(const Vec2 &pos, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 移动指定距离
|
||||
*/
|
||||
Tween &moveBy(const Vec2 &delta, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 缩放到目标值
|
||||
*/
|
||||
Tween &scaleTo(float scale, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 缩放指定增量
|
||||
*/
|
||||
Tween &scaleBy(float delta, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 旋转到目标角度
|
||||
*/
|
||||
Tween &rotateTo(float degrees, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 旋转指定角度
|
||||
*/
|
||||
Tween &rotateBy(float degrees, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 淡入(透明度从当前到1)
|
||||
*/
|
||||
Tween &fadeIn(float duration, TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 淡出(透明度从当前到0)
|
||||
*/
|
||||
Tween &fadeOut(float duration, TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 透明度变化到目标值
|
||||
*/
|
||||
Tween &fadeTo(float opacity, float duration,
|
||||
TweenEasing easing = static_cast<TweenEasing>(0));
|
||||
|
||||
/**
|
||||
* @brief 停止所有 Tween 动画
|
||||
*/
|
||||
void stopAllTweens();
|
||||
|
||||
/**
|
||||
* @brief 更新所有 Tween 动画
|
||||
*/
|
||||
void updateTweens(float dt);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 内部方法
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
@ -246,7 +326,9 @@ private:
|
|||
bool visible_ = true; // 1 byte
|
||||
bool running_ = false; // 1 byte
|
||||
bool spatialIndexed_ = true; // 1 byte
|
||||
// 填充 2 bytes 到 8 字节对齐
|
||||
|
||||
// 13. Tween 动画列表
|
||||
std::vector<std::shared_ptr<Tween>> tweens_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
#include <animation/als_parser.h>
|
||||
#include <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 <animation/ani_binary_parser.h>
|
||||
#include <animation/sprite_frame_cache.h>
|
||||
#include <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 <animation/ani_parser.h>
|
||||
#include <animation/sprite_frame_cache.h>
|
||||
#include <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 <animation/animated_sprite.h>
|
||||
#include <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 <animation/ani_binary_parser.h>
|
||||
#include <animation/ani_parser.h>
|
||||
#include <animation/animation_cache.h>
|
||||
#include <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 <animation/animation_clip.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// AnimationClip 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
#include <cassert>
|
||||
#include <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 <animation/animation_frame.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// AnimationFrame 的实现全部在头文件中以内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
#include <animation/animation_node.h>
|
||||
#include <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 <animation/animation_cache.h>
|
||||
#include <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 <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 <animation/frame_renderer.h>
|
||||
#include <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 <animation/interpolation_engine.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// InterpolationEngine 的实现全部在头文件中以静态内联方式完成
|
||||
// 此文件保留用于未来可能需要的非内联实现
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
#include <animation/tween.h>
|
||||
#include <scene/node.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
class IntervalAction : public TweenAction {
|
||||
public:
|
||||
IntervalAction(Node *target, float duration, const TweenProperty &props,
|
||||
const TweenOptions &options, bool isRelative)
|
||||
: TweenAction(Type::Interval), target_(target), duration_(duration),
|
||||
props_(props), options_(options), isRelative_(isRelative) {}
|
||||
|
||||
void update(float dt) override {
|
||||
if (finished_)
|
||||
return;
|
||||
|
||||
elapsed_ += dt;
|
||||
float t = std::min(elapsed_ / duration_, 1.0f);
|
||||
float easedT = EasingFunctions::apply(t, options_.easing);
|
||||
|
||||
applyProperties(easedT);
|
||||
|
||||
if (options_.onUpdate) {
|
||||
options_.onUpdate(t);
|
||||
}
|
||||
|
||||
if (t >= 1.0f) {
|
||||
finished_ = true;
|
||||
if (options_.onComplete) {
|
||||
options_.onComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isFinished() const override { return finished_; }
|
||||
float getDuration() const override { return duration_; }
|
||||
float getElapsed() const override { return elapsed_; }
|
||||
|
||||
void reset() override {
|
||||
elapsed_ = 0.0f;
|
||||
finished_ = false;
|
||||
started_ = false;
|
||||
}
|
||||
|
||||
void captureStartValues() {
|
||||
if (started_)
|
||||
return;
|
||||
started_ = true;
|
||||
|
||||
if (target_) {
|
||||
startPos_ = target_->getPosition();
|
||||
startScale_ = target_->getScale();
|
||||
startRotation_ = target_->getRotation();
|
||||
startOpacity_ = target_->getOpacity();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Node *target_;
|
||||
float duration_;
|
||||
TweenProperty props_;
|
||||
TweenOptions options_;
|
||||
bool isRelative_;
|
||||
bool started_ = false;
|
||||
|
||||
float elapsed_ = 0.0f;
|
||||
bool finished_ = false;
|
||||
|
||||
Vec2 startPos_;
|
||||
Vec2 startScale_;
|
||||
float startRotation_ = 0.0f;
|
||||
float startOpacity_ = 1.0f;
|
||||
|
||||
void applyProperties(float t) {
|
||||
if (!target_)
|
||||
return;
|
||||
|
||||
if (props_.position.has_value()) {
|
||||
Vec2 targetPos = isRelative_ ? startPos_ + props_.position.value()
|
||||
: props_.position.value();
|
||||
Vec2 newPos = Vec2::lerp(startPos_, targetPos, t);
|
||||
target_->setPosition(newPos);
|
||||
}
|
||||
|
||||
if (props_.scale.has_value()) {
|
||||
Vec2 targetScale =
|
||||
isRelative_ ? startScale_ + props_.scale.value()
|
||||
: props_.scale.value();
|
||||
Vec2 newScale = Vec2::lerp(startScale_, targetScale, t);
|
||||
target_->setScale(newScale);
|
||||
}
|
||||
|
||||
if (props_.rotation.has_value()) {
|
||||
float targetRot = isRelative_ ? startRotation_ + props_.rotation.value()
|
||||
: props_.rotation.value();
|
||||
float newRot = startRotation_ + (targetRot - startRotation_) * t;
|
||||
target_->setRotation(newRot);
|
||||
}
|
||||
|
||||
if (props_.opacity.has_value()) {
|
||||
float targetOpa = isRelative_ ? startOpacity_ + props_.opacity.value()
|
||||
: props_.opacity.value();
|
||||
float newOpa = startOpacity_ + (targetOpa - startOpacity_) * t;
|
||||
target_->setOpacity(newOpa);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class DelayAction : public TweenAction {
|
||||
public:
|
||||
DelayAction(float duration)
|
||||
: TweenAction(Type::Delay), duration_(duration) {}
|
||||
|
||||
void update(float dt) override {
|
||||
elapsed_ += dt;
|
||||
finished_ = elapsed_ >= duration_;
|
||||
}
|
||||
|
||||
bool isFinished() const override { return finished_; }
|
||||
float getDuration() const override { return duration_; }
|
||||
float getElapsed() const override { return elapsed_; }
|
||||
void reset() override {
|
||||
elapsed_ = 0.0f;
|
||||
finished_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
float duration_;
|
||||
float elapsed_ = 0.0f;
|
||||
bool finished_ = false;
|
||||
};
|
||||
|
||||
class CallAction : public TweenAction {
|
||||
public:
|
||||
CallAction(std::function<void()> callback)
|
||||
: TweenAction(Type::Call), callback_(std::move(callback)) {}
|
||||
|
||||
void update(float) override {
|
||||
if (!called_) {
|
||||
if (callback_) {
|
||||
callback_();
|
||||
}
|
||||
called_ = true;
|
||||
}
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
bool isFinished() const override { return finished_; }
|
||||
float getDuration() const override { return 0.0f; }
|
||||
float getElapsed() const override { return 0.0f; }
|
||||
void reset() override {
|
||||
called_ = false;
|
||||
finished_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void()> callback_;
|
||||
bool called_ = false;
|
||||
bool finished_ = false;
|
||||
};
|
||||
|
||||
class SetAction : public TweenAction {
|
||||
public:
|
||||
SetAction(Node *target, const TweenProperty &props)
|
||||
: TweenAction(Type::Call), target_(target), props_(props) {}
|
||||
|
||||
void update(float) override {
|
||||
if (!applied_ && target_) {
|
||||
if (props_.position.has_value())
|
||||
target_->setPosition(props_.position.value());
|
||||
if (props_.scale.has_value())
|
||||
target_->setScale(props_.scale.value());
|
||||
if (props_.rotation.has_value())
|
||||
target_->setRotation(props_.rotation.value());
|
||||
if (props_.opacity.has_value())
|
||||
target_->setOpacity(props_.opacity.value());
|
||||
applied_ = true;
|
||||
}
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
bool isFinished() const override { return finished_; }
|
||||
float getDuration() const override { return 0.0f; }
|
||||
float getElapsed() const override { return 0.0f; }
|
||||
void reset() override {
|
||||
applied_ = false;
|
||||
finished_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
Node *target_;
|
||||
TweenProperty props_;
|
||||
bool applied_ = false;
|
||||
bool finished_ = false;
|
||||
};
|
||||
|
||||
class ParallelAction : public TweenAction {
|
||||
public:
|
||||
ParallelAction() : TweenAction(Type::Parallel) {}
|
||||
|
||||
void addAction(std::unique_ptr<TweenAction> action) {
|
||||
actions_.push_back(std::move(action));
|
||||
}
|
||||
|
||||
void update(float dt) override {
|
||||
if (finished_)
|
||||
return;
|
||||
|
||||
bool allFinished = true;
|
||||
for (auto &action : actions_) {
|
||||
if (!action->isFinished()) {
|
||||
action->update(dt);
|
||||
allFinished = false;
|
||||
}
|
||||
}
|
||||
|
||||
finished_ = allFinished;
|
||||
}
|
||||
|
||||
bool isFinished() const override { return finished_; }
|
||||
float getDuration() const override {
|
||||
float maxDur = 0.0f;
|
||||
for (const auto &action : actions_) {
|
||||
maxDur = std::max(maxDur, action->getDuration());
|
||||
}
|
||||
return maxDur;
|
||||
}
|
||||
float getElapsed() const override {
|
||||
float maxElapsed = 0.0f;
|
||||
for (const auto &action : actions_) {
|
||||
maxElapsed = std::max(maxElapsed, action->getElapsed());
|
||||
}
|
||||
return maxElapsed;
|
||||
}
|
||||
void reset() override {
|
||||
for (auto &action : actions_) {
|
||||
action->reset();
|
||||
}
|
||||
finished_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<TweenAction>> actions_;
|
||||
bool finished_ = false;
|
||||
};
|
||||
|
||||
Tween::Tween(Node *target) : target_(target) {}
|
||||
|
||||
Tween::Tween(Node *target, const std::string &name)
|
||||
: target_(target), name_(name) {}
|
||||
|
||||
Tween::~Tween() = default;
|
||||
|
||||
Tween Tween::create(Node *target) { return Tween(target); }
|
||||
|
||||
Tween Tween::create(Node *target, const std::string &name) {
|
||||
return Tween(target, name);
|
||||
}
|
||||
|
||||
Tween &Tween::to(float duration, const TweenProperty &props,
|
||||
const TweenOptions &options) {
|
||||
auto action =
|
||||
std::make_unique<IntervalAction>(target_, duration, props, options, false);
|
||||
addAction(std::move(action));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::by(float duration, const TweenProperty &props,
|
||||
const TweenOptions &options) {
|
||||
auto action =
|
||||
std::make_unique<IntervalAction>(target_, duration, props, options, true);
|
||||
addAction(std::move(action));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::set(const TweenProperty &props) {
|
||||
auto action = std::make_unique<SetAction>(target_, props);
|
||||
addAction(std::move(action));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::delay(float seconds) {
|
||||
auto action = std::make_unique<DelayAction>(seconds);
|
||||
addAction(std::move(action));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::call(Callback callback) {
|
||||
auto action = std::make_unique<CallAction>(std::move(callback));
|
||||
addAction(std::move(action));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::parallel() {
|
||||
inParallel_ = true;
|
||||
parallelActions_.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::endParallel() {
|
||||
if (inParallel_ && !parallelActions_.empty()) {
|
||||
auto parallel = std::make_unique<ParallelAction>();
|
||||
for (auto &action : parallelActions_) {
|
||||
parallel->addAction(std::move(action));
|
||||
}
|
||||
actions_.push_back(std::move(parallel));
|
||||
parallelActions_.clear();
|
||||
}
|
||||
inParallel_ = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::union_() { return *this; }
|
||||
|
||||
Tween &Tween::repeat(int times) {
|
||||
repeatCount_ = times;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::repeatForever() {
|
||||
repeatForever_ = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Tween &Tween::yoyo(bool enabled) {
|
||||
yoyo_ = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Tween::start() {
|
||||
if (actions_.empty())
|
||||
return;
|
||||
|
||||
playing_ = true;
|
||||
paused_ = false;
|
||||
finished_ = false;
|
||||
currentActionIndex_ = 0;
|
||||
currentRepeat_ = 0;
|
||||
|
||||
for (auto &action : actions_) {
|
||||
action->reset();
|
||||
}
|
||||
|
||||
if (auto *interval =
|
||||
dynamic_cast<IntervalAction *>(actions_[0].get())) {
|
||||
interval->captureStartValues();
|
||||
}
|
||||
}
|
||||
|
||||
void Tween::stop() {
|
||||
playing_ = false;
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
void Tween::pause() { paused_ = true; }
|
||||
|
||||
void Tween::resume() { paused_ = false; }
|
||||
|
||||
void Tween::update(float dt) {
|
||||
if (!playing_ || paused_ || finished_)
|
||||
return;
|
||||
|
||||
if (currentActionIndex_ >= actions_.size()) {
|
||||
if (repeatForever_ || currentRepeat_ < repeatCount_ - 1) {
|
||||
currentRepeat_++;
|
||||
resetAllActions();
|
||||
currentActionIndex_ = 0;
|
||||
if (yoyo_) {
|
||||
reverse_ = !reverse_;
|
||||
}
|
||||
} else {
|
||||
finished_ = true;
|
||||
playing_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto &action = actions_[currentActionIndex_];
|
||||
action->update(dt);
|
||||
|
||||
if (action->isFinished()) {
|
||||
currentActionIndex_++;
|
||||
if (currentActionIndex_ < actions_.size()) {
|
||||
actions_[currentActionIndex_]->reset();
|
||||
if (auto *interval = dynamic_cast<IntervalAction *>(
|
||||
actions_[currentActionIndex_].get())) {
|
||||
interval->captureStartValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Tween::isFinished() const { return finished_; }
|
||||
|
||||
bool Tween::isPlaying() const { return playing_; }
|
||||
|
||||
bool Tween::isPaused() const { return paused_; }
|
||||
|
||||
float Tween::getTotalDuration() const {
|
||||
float total = 0.0f;
|
||||
for (const auto &action : actions_) {
|
||||
total += action->getDuration();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void Tween::addAction(std::unique_ptr<TweenAction> action) {
|
||||
if (inParallel_) {
|
||||
parallelActions_.push_back(std::move(action));
|
||||
} else {
|
||||
actions_.push_back(std::move(action));
|
||||
}
|
||||
}
|
||||
|
||||
void Tween::advanceToNextAction() {
|
||||
currentActionIndex_++;
|
||||
if (currentActionIndex_ < actions_.size()) {
|
||||
actions_[currentActionIndex_]->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Tween::resetAllActions() {
|
||||
for (auto &action : actions_) {
|
||||
action->reset();
|
||||
}
|
||||
}
|
||||
|
||||
namespace tween {
|
||||
|
||||
TweenProperty combine(std::initializer_list<TweenProperty> props) {
|
||||
TweenProperty result;
|
||||
for (const auto &p : props) {
|
||||
if (p.position.has_value())
|
||||
result.position = p.position;
|
||||
if (p.scale.has_value())
|
||||
result.scale = p.scale;
|
||||
if (p.rotation.has_value())
|
||||
result.rotation = p.rotation;
|
||||
if (p.opacity.has_value())
|
||||
result.opacity = p.opacity;
|
||||
if (p.color.has_value())
|
||||
result.color = p.color;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace tween
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
#include <animation/tween_easing.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
float EasingFunctions::apply(float t, TweenEasing easing) {
|
||||
return get(easing)(t);
|
||||
}
|
||||
|
||||
EasingFunctions::EasingFunc EasingFunctions::get(TweenEasing easing) {
|
||||
switch (easing) {
|
||||
case TweenEasing::Linear: return linear;
|
||||
case TweenEasing::QuadIn: return quadIn;
|
||||
case TweenEasing::QuadOut: return quadOut;
|
||||
case TweenEasing::QuadInOut: return quadInOut;
|
||||
case TweenEasing::CubicIn: return cubicIn;
|
||||
case TweenEasing::CubicOut: return cubicOut;
|
||||
case TweenEasing::CubicInOut: return cubicInOut;
|
||||
case TweenEasing::QuartIn: return quartIn;
|
||||
case TweenEasing::QuartOut: return quartOut;
|
||||
case TweenEasing::QuartInOut: return quartInOut;
|
||||
case TweenEasing::QuintIn: return quintIn;
|
||||
case TweenEasing::QuintOut: return quintOut;
|
||||
case TweenEasing::QuintInOut: return quintInOut;
|
||||
case TweenEasing::SineIn: return sineIn;
|
||||
case TweenEasing::SineOut: return sineOut;
|
||||
case TweenEasing::SineInOut: return sineInOut;
|
||||
case TweenEasing::ExpoIn: return expoIn;
|
||||
case TweenEasing::ExpoOut: return expoOut;
|
||||
case TweenEasing::ExpoInOut: return expoInOut;
|
||||
case TweenEasing::CircIn: return circIn;
|
||||
case TweenEasing::CircOut: return circOut;
|
||||
case TweenEasing::CircInOut: return circInOut;
|
||||
case TweenEasing::ElasticIn: return elasticIn;
|
||||
case TweenEasing::ElasticOut: return elasticOut;
|
||||
case TweenEasing::ElasticInOut: return elasticInOut;
|
||||
case TweenEasing::BackIn: return backIn;
|
||||
case TweenEasing::BackOut: return backOut;
|
||||
case TweenEasing::BackInOut: return backInOut;
|
||||
case TweenEasing::BounceIn: return bounceIn;
|
||||
case TweenEasing::BounceOut: return bounceOut;
|
||||
case TweenEasing::BounceInOut: return bounceInOut;
|
||||
case TweenEasing::Smooth: return smooth;
|
||||
case TweenEasing::Fade: return fade;
|
||||
default: return linear;
|
||||
}
|
||||
}
|
||||
|
||||
float EasingFunctions::linear(float t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
float EasingFunctions::quadIn(float t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
float EasingFunctions::quadOut(float t) {
|
||||
return t * (2.0f - t);
|
||||
}
|
||||
|
||||
float EasingFunctions::quadInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 2.0f * t * t;
|
||||
}
|
||||
return -1.0f + (4.0f - 2.0f * t) * t;
|
||||
}
|
||||
|
||||
float EasingFunctions::cubicIn(float t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
float EasingFunctions::cubicOut(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return t1 * t1 * t1 + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::cubicInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 4.0f * t * t * t;
|
||||
}
|
||||
float t1 = 2.0f * t - 2.0f;
|
||||
return 0.5f * t1 * t1 * t1 + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::quartIn(float t) {
|
||||
return t * t * t * t;
|
||||
}
|
||||
|
||||
float EasingFunctions::quartOut(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return 1.0f - t1 * t1 * t1 * t1;
|
||||
}
|
||||
|
||||
float EasingFunctions::quartInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 8.0f * t * t * t * t;
|
||||
}
|
||||
float t1 = t - 1.0f;
|
||||
return 1.0f - 8.0f * t1 * t1 * t1 * t1;
|
||||
}
|
||||
|
||||
float EasingFunctions::quintIn(float t) {
|
||||
return t * t * t * t * t;
|
||||
}
|
||||
|
||||
float EasingFunctions::quintOut(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return t1 * t1 * t1 * t1 * t1 + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::quintInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 16.0f * t * t * t * t * t;
|
||||
}
|
||||
float t1 = 2.0f * t - 2.0f;
|
||||
return 0.5f * t1 * t1 * t1 * t1 * t1 + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::sineIn(float t) {
|
||||
return 1.0f - std::cos(t * HALF_PI);
|
||||
}
|
||||
|
||||
float EasingFunctions::sineOut(float t) {
|
||||
return std::sin(t * HALF_PI);
|
||||
}
|
||||
|
||||
float EasingFunctions::sineInOut(float t) {
|
||||
return -0.5f * (std::cos(PI * t) - 1.0f);
|
||||
}
|
||||
|
||||
float EasingFunctions::expoIn(float t) {
|
||||
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
|
||||
}
|
||||
|
||||
float EasingFunctions::expoOut(float t) {
|
||||
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
|
||||
}
|
||||
|
||||
float EasingFunctions::expoInOut(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
|
||||
}
|
||||
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
|
||||
}
|
||||
|
||||
float EasingFunctions::circIn(float t) {
|
||||
return 1.0f - std::sqrt(1.0f - t * t);
|
||||
}
|
||||
|
||||
float EasingFunctions::circOut(float t) {
|
||||
return std::sqrt(2.0f * t - t * t);
|
||||
}
|
||||
|
||||
float EasingFunctions::circInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
|
||||
}
|
||||
return 0.5f * (std::sqrt(4.0f * t - 4.0f * t * t - 3.0f) + 1.0f);
|
||||
}
|
||||
|
||||
float EasingFunctions::elasticIn(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
return -std::pow(2.0f, 10.0f * (t - 1.0f)) * std::sin((t - 1.1f) * ELASTIC_CONST);
|
||||
}
|
||||
|
||||
float EasingFunctions::elasticOut(float t) {
|
||||
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 - 0.1f) * ELASTIC_CONST) + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::elasticInOut(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
if (t < 0.5f) {
|
||||
return -0.5f * std::pow(2.0f, 20.0f * t - 10.0f) * std::sin((20.0f * t - 11.125f) * PI / 4.5f);
|
||||
}
|
||||
return std::pow(2.0f, -20.0f * t + 10.0f) * std::sin((20.0f * t - 11.125f) * PI / 4.5f) * 0.5f + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::backIn(float t) {
|
||||
return t * t * ((BACK_CONST + 1.0f) * t - BACK_CONST);
|
||||
}
|
||||
|
||||
float EasingFunctions::backOut(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return t1 * t1 * ((BACK_CONST + 1.0f) * t1 + BACK_CONST) + 1.0f;
|
||||
}
|
||||
|
||||
float EasingFunctions::backInOut(float t) {
|
||||
float s = BACK_CONST * 1.525f;
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * (t * 2.0f) * (t * 2.0f) * ((s + 1.0f) * 2.0f * t - s);
|
||||
}
|
||||
float t1 = 2.0f * t - 2.0f;
|
||||
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
|
||||
}
|
||||
|
||||
float EasingFunctions::bounceIn(float t) {
|
||||
return 1.0f - bounceOut(1.0f - t);
|
||||
}
|
||||
|
||||
float EasingFunctions::bounceOut(float t) {
|
||||
if (t < 1.0f / 2.75f) {
|
||||
return 7.5625f * t * t;
|
||||
} else if (t < 2.0f / 2.75f) {
|
||||
float t1 = t - 1.5f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.75f;
|
||||
} else if (t < 2.5f / 2.75f) {
|
||||
float t1 = t - 2.25f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.9375f;
|
||||
} else {
|
||||
float t1 = t - 2.625f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.984375f;
|
||||
}
|
||||
}
|
||||
|
||||
float EasingFunctions::bounceInOut(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * bounceIn(t * 2.0f);
|
||||
}
|
||||
return 0.5f * bounceOut(t * 2.0f - 1.0f) + 0.5f;
|
||||
}
|
||||
|
||||
float EasingFunctions::smooth(float t) {
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
float EasingFunctions::fade(float t) {
|
||||
return t * t * t * (t * (6.0f * t - 15.0f) + 10.0f);
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -5,13 +5,11 @@
|
|||
namespace extra2d {
|
||||
|
||||
Input::Input()
|
||||
: controller_(nullptr),
|
||||
leftStickX_(0.0f), leftStickY_(0.0f),
|
||||
rightStickX_(0.0f), rightStickY_(0.0f),
|
||||
mouseScroll_(0.0f), prevMouseScroll_(0.0f),
|
||||
touching_(false), prevTouching_(false), touchCount_(0) {
|
||||
: controller_(nullptr), leftStickX_(0.0f), leftStickY_(0.0f),
|
||||
rightStickX_(0.0f), rightStickY_(0.0f), mouseScroll_(0.0f),
|
||||
prevMouseScroll_(0.0f), touching_(false), prevTouching_(false),
|
||||
touchCount_(0) {
|
||||
|
||||
// 初始化所有状态数组
|
||||
keysDown_.fill(false);
|
||||
prevKeysDown_.fill(false);
|
||||
buttonsDown_.fill(false);
|
||||
|
|
@ -23,7 +21,6 @@ Input::Input()
|
|||
Input::~Input() { shutdown(); }
|
||||
|
||||
void Input::init() {
|
||||
// 打开第一个可用的游戏控制器
|
||||
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
controller_ = SDL_GameControllerOpen(i);
|
||||
|
|
@ -39,13 +36,10 @@ void Input::init() {
|
|||
E2D_LOG_WARN("No game controller found");
|
||||
}
|
||||
|
||||
// PC 端获取初始鼠标状态
|
||||
#ifndef PLATFORM_SWITCH
|
||||
int mouseX, mouseY;
|
||||
SDL_GetMouseState(&mouseX, &mouseY);
|
||||
mousePosition_ = Vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
|
||||
prevMousePosition_ = mousePosition_;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Input::shutdown() {
|
||||
|
|
@ -56,7 +50,6 @@ void Input::shutdown() {
|
|||
}
|
||||
|
||||
void Input::update() {
|
||||
// 保存上一帧状态
|
||||
prevKeysDown_ = keysDown_;
|
||||
prevButtonsDown_ = buttonsDown_;
|
||||
prevMouseButtonsDown_ = mouseButtonsDown_;
|
||||
|
|
@ -65,7 +58,6 @@ void Input::update() {
|
|||
prevTouching_ = touching_;
|
||||
prevTouchPosition_ = touchPosition_;
|
||||
|
||||
// 更新各输入设备状态
|
||||
updateKeyboard();
|
||||
updateMouse();
|
||||
updateGamepad();
|
||||
|
|
@ -73,50 +65,44 @@ void Input::update() {
|
|||
}
|
||||
|
||||
void Input::updateKeyboard() {
|
||||
// 获取当前键盘状态
|
||||
const Uint8* state = SDL_GetKeyboardState(nullptr);
|
||||
const Uint8 *state = SDL_GetKeyboardState(nullptr);
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
keysDown_[i] = state[i] != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Input::updateMouse() {
|
||||
#ifndef PLATFORM_SWITCH
|
||||
// 更新鼠标位置
|
||||
int mouseX, mouseY;
|
||||
Uint32 buttonState = SDL_GetMouseState(&mouseX, &mouseY);
|
||||
mousePosition_ = Vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
|
||||
|
||||
// 更新鼠标按钮状态
|
||||
mouseButtonsDown_[0] = (buttonState & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
|
||||
mouseButtonsDown_[1] = (buttonState & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
|
||||
mouseButtonsDown_[2] = (buttonState & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0;
|
||||
mouseButtonsDown_[3] = (buttonState & SDL_BUTTON(SDL_BUTTON_X1)) != 0;
|
||||
mouseButtonsDown_[4] = (buttonState & SDL_BUTTON(SDL_BUTTON_X2)) != 0;
|
||||
|
||||
// 处理鼠标滚轮事件(需要在事件循环中处理,这里简化处理)
|
||||
// 实际滚轮值通过 SDL_MOUSEWHEEL 事件更新
|
||||
#endif
|
||||
}
|
||||
|
||||
void Input::updateGamepad() {
|
||||
if (controller_) {
|
||||
// 更新按钮状态
|
||||
for (int i = 0; i < MAX_BUTTONS; ++i) {
|
||||
buttonsDown_[i] =
|
||||
SDL_GameControllerGetButton(
|
||||
controller_, static_cast<SDL_GameControllerButton>(i)) != 0;
|
||||
}
|
||||
|
||||
// 读取摇杆(归一化到 -1.0 ~ 1.0)
|
||||
leftStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
|
||||
controller_, SDL_CONTROLLER_AXIS_LEFTX)) / 32767.0f;
|
||||
controller_, SDL_CONTROLLER_AXIS_LEFTX)) /
|
||||
32767.0f;
|
||||
leftStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
|
||||
controller_, SDL_CONTROLLER_AXIS_LEFTY)) / 32767.0f;
|
||||
controller_, SDL_CONTROLLER_AXIS_LEFTY)) /
|
||||
32767.0f;
|
||||
rightStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
|
||||
controller_, SDL_CONTROLLER_AXIS_RIGHTX)) / 32767.0f;
|
||||
controller_, SDL_CONTROLLER_AXIS_RIGHTX)) /
|
||||
32767.0f;
|
||||
rightStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
|
||||
controller_, SDL_CONTROLLER_AXIS_RIGHTY)) / 32767.0f;
|
||||
controller_, SDL_CONTROLLER_AXIS_RIGHTY)) /
|
||||
32767.0f;
|
||||
} else {
|
||||
buttonsDown_.fill(false);
|
||||
leftStickX_ = leftStickY_ = rightStickX_ = rightStickY_ = 0.0f;
|
||||
|
|
@ -124,8 +110,6 @@ void Input::updateGamepad() {
|
|||
}
|
||||
|
||||
void Input::updateTouch() {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
// Switch 原生触摸屏支持
|
||||
SDL_TouchID touchId = SDL_GetTouchDevice(0);
|
||||
if (touchId != 0) {
|
||||
touchCount_ = SDL_GetNumTouchFingers(touchId);
|
||||
|
|
@ -133,33 +117,12 @@ void Input::updateTouch() {
|
|||
SDL_Finger *finger = SDL_GetTouchFinger(touchId, 0);
|
||||
if (finger) {
|
||||
touching_ = true;
|
||||
// SDL 触摸坐标是归一化 0.0~1.0,转换为像素坐标
|
||||
touchPosition_ = Vec2(finger->x * 1280.0f, finger->y * 720.0f);
|
||||
} else {
|
||||
touching_ = false;
|
||||
}
|
||||
} else {
|
||||
touching_ = false;
|
||||
}
|
||||
} else {
|
||||
touchCount_ = 0;
|
||||
touching_ = false;
|
||||
}
|
||||
#else
|
||||
// PC 端:触摸屏可选支持(如果有触摸设备)
|
||||
SDL_TouchID touchId = SDL_GetTouchDevice(0);
|
||||
if (touchId != 0) {
|
||||
touchCount_ = SDL_GetNumTouchFingers(touchId);
|
||||
if (touchCount_ > 0) {
|
||||
SDL_Finger *finger = SDL_GetTouchFinger(touchId, 0);
|
||||
if (finger) {
|
||||
touching_ = true;
|
||||
// PC 端需要根据窗口大小转换坐标
|
||||
int windowWidth, windowHeight;
|
||||
SDL_Window* window = SDL_GL_GetCurrentWindow();
|
||||
SDL_Window *window = SDL_GL_GetCurrentWindow();
|
||||
if (window) {
|
||||
SDL_GetWindowSize(window, &windowWidth, &windowHeight);
|
||||
touchPosition_ = Vec2(finger->x * windowWidth, finger->y * windowHeight);
|
||||
touchPosition_ =
|
||||
Vec2(finger->x * windowWidth, finger->y * windowHeight);
|
||||
} else {
|
||||
touchPosition_ = Vec2(finger->x * 1280.0f, finger->y * 720.0f);
|
||||
}
|
||||
|
|
@ -173,113 +136,34 @@ void Input::updateTouch() {
|
|||
touchCount_ = 0;
|
||||
touching_ = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 键盘输入
|
||||
// ============================================================================
|
||||
|
||||
SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
|
||||
switch (keyCode) {
|
||||
// 方向键 → DPad
|
||||
case Key::Up:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
case Key::Down:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
case Key::Left:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
case Key::Right:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
|
||||
// WASD → 也映射到 DPad
|
||||
case Key::W:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
case Key::S:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
case Key::A:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
case Key::D:
|
||||
return SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
|
||||
// 常用键 → 手柄按钮
|
||||
case Key::Z:
|
||||
return SDL_CONTROLLER_BUTTON_B;
|
||||
case Key::X:
|
||||
return SDL_CONTROLLER_BUTTON_A;
|
||||
case Key::C:
|
||||
return SDL_CONTROLLER_BUTTON_Y;
|
||||
case Key::V:
|
||||
return SDL_CONTROLLER_BUTTON_X;
|
||||
case Key::Space:
|
||||
return SDL_CONTROLLER_BUTTON_A;
|
||||
case Key::Enter:
|
||||
return SDL_CONTROLLER_BUTTON_A;
|
||||
case Key::Escape:
|
||||
return SDL_CONTROLLER_BUTTON_START;
|
||||
|
||||
// 肩键
|
||||
case Key::Q:
|
||||
return SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
case Key::E:
|
||||
return SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
|
||||
// Start/Select
|
||||
case Key::Tab:
|
||||
return SDL_CONTROLLER_BUTTON_BACK;
|
||||
case Key::Backspace:
|
||||
return SDL_CONTROLLER_BUTTON_START;
|
||||
|
||||
default:
|
||||
return SDL_CONTROLLER_BUTTON_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
bool Input::isKeyDown(int keyCode) const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
// Switch: 映射到手柄按钮
|
||||
SDL_GameControllerButton button = mapKeyToButton(keyCode);
|
||||
if (button == SDL_CONTROLLER_BUTTON_INVALID)
|
||||
return false;
|
||||
return buttonsDown_[button];
|
||||
#else
|
||||
// PC: 直接使用键盘扫描码
|
||||
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
|
||||
if (scancode >= 0 && scancode < MAX_KEYS) {
|
||||
return keysDown_[scancode];
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Input::isKeyPressed(int keyCode) const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
SDL_GameControllerButton button = mapKeyToButton(keyCode);
|
||||
if (button == SDL_CONTROLLER_BUTTON_INVALID)
|
||||
return false;
|
||||
return buttonsDown_[button] && !prevButtonsDown_[button];
|
||||
#else
|
||||
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
|
||||
if (scancode >= 0 && scancode < MAX_KEYS) {
|
||||
return keysDown_[scancode] && !prevKeysDown_[scancode];
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Input::isKeyReleased(int keyCode) const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
SDL_GameControllerButton button = mapKeyToButton(keyCode);
|
||||
if (button == SDL_CONTROLLER_BUTTON_INVALID)
|
||||
return false;
|
||||
return !buttonsDown_[button] && prevButtonsDown_[button];
|
||||
#else
|
||||
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
|
||||
if (scancode >= 0 && scancode < MAX_KEYS) {
|
||||
return !keysDown_[scancode] && prevKeysDown_[scancode];
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -316,103 +200,40 @@ bool Input::isMouseDown(MouseButton button) const {
|
|||
int index = static_cast<int>(button);
|
||||
if (index < 0 || index >= 8)
|
||||
return false;
|
||||
|
||||
#ifdef PLATFORM_SWITCH
|
||||
// Switch: 左键映射到触摸,右键映射到 A 键
|
||||
if (button == MouseButton::Left) {
|
||||
return touching_;
|
||||
}
|
||||
if (button == MouseButton::Right) {
|
||||
return buttonsDown_[SDL_CONTROLLER_BUTTON_A];
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
// PC: 直接使用鼠标按钮
|
||||
return mouseButtonsDown_[index];
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Input::isMousePressed(MouseButton button) const {
|
||||
int index = static_cast<int>(button);
|
||||
if (index < 0 || index >= 8)
|
||||
return false;
|
||||
|
||||
#ifdef PLATFORM_SWITCH
|
||||
if (button == MouseButton::Left) {
|
||||
return touching_ && !prevTouching_;
|
||||
}
|
||||
if (button == MouseButton::Right) {
|
||||
return buttonsDown_[SDL_CONTROLLER_BUTTON_A] &&
|
||||
!prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
return mouseButtonsDown_[index] && !prevMouseButtonsDown_[index];
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Input::isMouseReleased(MouseButton button) const {
|
||||
int index = static_cast<int>(button);
|
||||
if (index < 0 || index >= 8)
|
||||
return false;
|
||||
|
||||
#ifdef PLATFORM_SWITCH
|
||||
if (button == MouseButton::Left) {
|
||||
return !touching_ && prevTouching_;
|
||||
}
|
||||
if (button == MouseButton::Right) {
|
||||
return !buttonsDown_[SDL_CONTROLLER_BUTTON_A] &&
|
||||
prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
return !mouseButtonsDown_[index] && prevMouseButtonsDown_[index];
|
||||
#endif
|
||||
}
|
||||
|
||||
Vec2 Input::getMousePosition() const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
return touchPosition_;
|
||||
#else
|
||||
return mousePosition_;
|
||||
#endif
|
||||
}
|
||||
Vec2 Input::getMousePosition() const { return mousePosition_; }
|
||||
|
||||
Vec2 Input::getMouseDelta() const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
if (touching_ && prevTouching_) {
|
||||
return touchPosition_ - prevTouchPosition_;
|
||||
}
|
||||
return Vec2::Zero();
|
||||
#else
|
||||
return mousePosition_ - prevMousePosition_;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Input::setMousePosition(const Vec2 &position) {
|
||||
#ifndef PLATFORM_SWITCH
|
||||
SDL_WarpMouseInWindow(SDL_GL_GetCurrentWindow(),
|
||||
static_cast<int>(position.x),
|
||||
SDL_WarpMouseInWindow(SDL_GL_GetCurrentWindow(), static_cast<int>(position.x),
|
||||
static_cast<int>(position.y));
|
||||
#else
|
||||
(void)position;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Input::setMouseVisible(bool visible) {
|
||||
#ifndef PLATFORM_SWITCH
|
||||
SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE);
|
||||
#else
|
||||
(void)visible;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Input::setMouseLocked(bool locked) {
|
||||
#ifndef PLATFORM_SWITCH
|
||||
SDL_SetRelativeMouseMode(locked ? SDL_TRUE : SDL_FALSE);
|
||||
#else
|
||||
(void)locked;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -420,30 +241,66 @@ void Input::setMouseLocked(bool locked) {
|
|||
// ============================================================================
|
||||
|
||||
bool Input::isAnyKeyDown() const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
for (int i = 0; i < MAX_BUTTONS; ++i) {
|
||||
if (buttonsDown_[i])
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
if (keysDown_[i])
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Input::isAnyMouseDown() const {
|
||||
#ifdef PLATFORM_SWITCH
|
||||
return touching_;
|
||||
#else
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (mouseButtonsDown_[i])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 事件处理
|
||||
// ============================================================================
|
||||
|
||||
void Input::onControllerAdded(int deviceIndex) {
|
||||
if (SDL_IsGameController(deviceIndex)) {
|
||||
SDL_GameController *controller = SDL_GameControllerOpen(deviceIndex);
|
||||
if (controller) {
|
||||
if (controller_) {
|
||||
SDL_GameControllerClose(controller_);
|
||||
}
|
||||
controller_ = controller;
|
||||
E2D_LOG_INFO("GameController connected: {}",
|
||||
SDL_GameControllerName(controller_));
|
||||
} else {
|
||||
E2D_LOG_ERROR("Failed to open GameController: {}", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Input::onControllerRemoved(int instanceId) {
|
||||
if (controller_) {
|
||||
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller_);
|
||||
if (joystick) {
|
||||
SDL_JoystickID joyInstanceId = SDL_JoystickInstanceID(joystick);
|
||||
if (joyInstanceId == instanceId) {
|
||||
E2D_LOG_INFO("GameController disconnected");
|
||||
SDL_GameControllerClose(controller_);
|
||||
controller_ = nullptr;
|
||||
|
||||
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
controller_ = SDL_GameControllerOpen(i);
|
||||
if (controller_) {
|
||||
E2D_LOG_INFO("Switched to GameController: {}",
|
||||
SDL_GameControllerName(controller_));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Input::onMouseWheel(float x, float y) { mouseScroll_ += y; }
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ namespace extra2d {
|
|||
Window::Window()
|
||||
: sdlWindow_(nullptr), glContext_(nullptr), currentCursor_(nullptr),
|
||||
width_(1280), height_(720), vsync_(true), shouldClose_(false),
|
||||
fullscreen_(true), focused_(true), contentScaleX_(1.0f), contentScaleY_(1.0f),
|
||||
enableDpiScale_(true), userData_(nullptr), eventQueue_(nullptr) {
|
||||
// 初始化光标数组
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
sdlCursors_[i] = nullptr;
|
||||
}
|
||||
fullscreen_(true), focused_(true), contentScaleX_(1.0f),
|
||||
contentScaleY_(1.0f), enableDpiScale_(true), userData_(nullptr),
|
||||
eventQueue_(nullptr) {
|
||||
// 初始化光标数组
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
sdlCursors_[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Window::~Window() { destroy(); }
|
||||
|
|
@ -55,7 +56,8 @@ bool Window::create(const WindowConfig &config) {
|
|||
|
||||
bool Window::initSDL(const WindowConfig &config) {
|
||||
// SDL2 全局初始化(视频 + 游戏控制器 + 音频)
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) != 0) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) !=
|
||||
0) {
|
||||
E2D_LOG_ERROR("SDL_Init failed: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
|
@ -129,7 +131,8 @@ bool Window::initSDL(const WindowConfig &config) {
|
|||
}
|
||||
|
||||
// 加载 OpenGL ES 函数指针
|
||||
if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == 0) {
|
||||
if (gladLoadGLES2Loader(
|
||||
reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == 0) {
|
||||
E2D_LOG_ERROR("gladLoadGLES2Loader failed");
|
||||
SDL_GL_DeleteContext(glContext_);
|
||||
glContext_ = nullptr;
|
||||
|
|
@ -217,6 +220,28 @@ void Window::pollEvents() {
|
|||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
if (input_) {
|
||||
input_->onControllerAdded(event.cdevice.which);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
if (input_) {
|
||||
input_->onControllerRemoved(event.cdevice.which);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERDEVICEREMAPPED:
|
||||
E2D_LOG_INFO("Controller device remapped");
|
||||
break;
|
||||
|
||||
case SDL_MOUSEWHEEL:
|
||||
if (input_) {
|
||||
input_->onMouseWheel(event.wheel.x, event.wheel.y);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <animation/tween.h>
|
||||
#include <graphics/render_command.h>
|
||||
#include <scene/node.h>
|
||||
#include <scene/scene.h>
|
||||
|
|
@ -334,6 +335,7 @@ void Node::onExit() {
|
|||
}
|
||||
|
||||
void Node::onUpdate(float dt) {
|
||||
updateTweens(dt);
|
||||
onUpdateNode(dt);
|
||||
|
||||
// Update children
|
||||
|
|
@ -460,4 +462,142 @@ void Node::collectRenderCommands(std::vector<RenderCommand> &commands,
|
|||
}
|
||||
}
|
||||
|
||||
Tween& Node::tween() {
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween& Node::tween(const std::string &name) {
|
||||
auto tw = std::make_shared<Tween>(this, name);
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::moveTo(const Vec2 &pos, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.position = pos;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::moveBy(const Vec2 &delta, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.position = delta;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->by(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::scaleTo(float scale, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.scale = Vec2(scale, scale);
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::scaleBy(float delta, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.scale = Vec2(delta, delta);
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->by(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::rotateTo(float degrees, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.rotation = degrees;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::rotateBy(float degrees, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.rotation = degrees;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->by(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::fadeIn(float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.opacity = 1.0f;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::fadeOut(float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.opacity = 0.0f;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
Tween &Node::fadeTo(float opacity, float duration, TweenEasing easing) {
|
||||
TweenProperty props;
|
||||
props.opacity = opacity;
|
||||
TweenOptions options;
|
||||
options.easing = easing;
|
||||
auto tw = std::make_shared<Tween>(this);
|
||||
tw->to(duration, props, options);
|
||||
tw->start();
|
||||
tweens_.push_back(tw);
|
||||
return *tweens_.back();
|
||||
}
|
||||
|
||||
void Node::stopAllTweens() {
|
||||
for (auto &tw : tweens_) {
|
||||
tw->stop();
|
||||
}
|
||||
tweens_.clear();
|
||||
}
|
||||
|
||||
void Node::updateTweens(float dt) {
|
||||
for (auto it = tweens_.begin(); it != tweens_.end();) {
|
||||
(*it)->update(dt);
|
||||
if ((*it)->isFinished()) {
|
||||
it = tweens_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
Loading…
Reference in New Issue