feat: 添加SDL2平台支持及核心功能模块

实现SDL2平台初始化、窗口管理、输入处理和文件系统服务
重构服务管理机制,支持暂停/恢复状态
添加事件总线系统,实现引擎事件分发
优化应用主循环和更新机制
完善跨平台文件操作接口
This commit is contained in:
ChestnutYueyue 2026-02-27 22:59:17 +08:00
parent 5ef1873a44
commit 8abf58e3d5
19 changed files with 2339 additions and 24 deletions

View File

@ -5,17 +5,28 @@
namespace extra2d {
enum class PlatformType { Auto = 0, PC, Switch };
/**
* @brief
*/
enum class PlatformType {
Auto = 0,
PC,
Switch
};
/**
* @brief
*/
struct AppConfig {
std::string title = "Extra2D Application";
int width = 800;
int height = 600;
int32 width = 1280;
int32 height = 720;
bool fullscreen = false;
bool resizable = true;
bool vsync = true;
int fpsLimit = 0;
int msaaSamples = 0;
int32 fpsLimit = 0;
int32 glMajor = 3;
int32 glMinor = 3;
PlatformType platform = PlatformType::Auto;
bool enableCursors = true;
bool enableDpiScale = false;
@ -28,25 +39,72 @@ struct AppConfig {
*/
class Application {
public:
/**
* @brief
*/
static Application& instance();
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
/**
* @brief
*/
bool init(const AppConfig& config);
/**
* @brief
*/
void shutdown();
/**
* @brief
*/
void run();
/**
* @brief 退
*/
void quit();
/**
* @brief
*/
void pause();
/**
* @brief
*/
void resume();
/**
* @brief
*/
bool isPaused() const { return paused_; }
/**
* @brief
*/
bool isRunning() const { return running_; }
/**
* @brief
*/
float deltaTime() const { return deltaTime_; }
float totalTime() const { return totalTime_; }
int fps() const { return currentFps_; }
/**
* @brief
*/
float totalTime() const { return totalTime_; }
/**
* @brief FPS
*/
int32 fps() const { return currentFps_; }
/**
* @brief
*/
const AppConfig& getConfig() const { return config_; }
private:
@ -66,9 +124,9 @@ private:
float deltaTime_ = 0.0f;
float totalTime_ = 0.0f;
double lastFrameTime_ = 0.0;
int frameCount_ = 0;
int32 frameCount_ = 0;
float fpsTimer_ = 0.0f;
int currentFps_ = 0;
int32 currentFps_ = 0;
};
#define APP extra2d::Application::instance()

View File

@ -0,0 +1,177 @@
#pragma once
#include <types/base/types.h>
#include <functional>
#include <tuple>
#include <vector>
#include <cstddef>
namespace extra2d::event {
/**
* @brief
*/
struct ListenerEntry {
ListenerEntry* next = nullptr;
ListenerEntry* prev = nullptr;
class ListenerBase* listener = nullptr;
};
/**
* @brief
*/
class ListenerBase {
protected:
ListenerEntry* entry_ = nullptr;
friend class ListenerContainer;
};
/**
* @brief
*/
class ListenerContainer {
public:
ListenerContainer() = default;
~ListenerContainer();
template<typename EHandler, typename... Args>
bool broadcast(Args&&... args);
protected:
ListenerEntry* listenerList_ = nullptr;
ListenerEntry* listenersToAdd_ = nullptr;
std::vector<ListenerEntry*> listenersToRemove_;
int broadcasting_ = 0;
void addListener(ListenerBase* listener);
void removeListener(ListenerBase* listener);
void processPendingListeners();
bool hasPendingListeners() const;
template<typename EHandler>
friend class Listener;
};
/**
* @brief
*/
template<typename EHandler>
class ListenerDB {
public:
static ListenerContainer* container() {
static ListenerContainer* ctn = new ListenerContainer();
return ctn;
}
};
/**
* @brief
*/
template<typename BusT, typename... Args>
struct EventTrait {
using Bus = BusT;
using ArgTuple = std::tuple<Args...>;
static constexpr size_t ARG_COUNT = sizeof...(Args);
};
/**
* @brief
*/
template<typename T>
struct TupleExtractor {
using FuncType = void();
using StdFuncType = std::function<void()>;
};
template<typename... Args>
struct TupleExtractor<std::tuple<Args...>> {
using FuncType = void(Args...);
using StdFuncType = std::function<void(Args...)>;
};
/**
* @brief
*
*
*/
template<typename EHandler>
class Listener : public ListenerBase {
public:
using ArgTuple = typename EHandler::ArgTuple;
using StdFuncType = typename TupleExtractor<ArgTuple>::StdFuncType;
Listener();
~Listener();
template<typename Fn>
void bind(Fn&& func) {
callback_ = std::forward<Fn>(func);
}
void enable() { enabled_ = true; }
void disable() { enabled_ = false; }
bool isEnabled() const { return enabled_; }
void reset() { callback_ = nullptr; }
template<typename... Args>
void invoke(Args&&... args) {
if (callback_ && enabled_) {
callback_(std::forward<Args>(args)...);
}
}
const char* busName() const { return EHandler::BUS_NAME; }
const char* eventName() const { return EHandler::NAME; }
private:
bool enabled_ = true;
StdFuncType callback_;
};
template<typename EHandler>
Listener<EHandler>::Listener() {
entry_ = new ListenerEntry();
entry_->listener = this;
ListenerDB<EHandler>::container()->addListener(this);
}
template<typename EHandler>
Listener<EHandler>::~Listener() {
ListenerDB<EHandler>::container()->removeListener(this);
}
#define EVENT_LIST_LOOP_BEGIN(curr, list) \
for (ListenerEntry* curr = list; curr != nullptr; curr = curr->next) {
#define EVENT_LIST_LOOP_END(curr, list) }
template<typename EHandler, typename... Args>
bool ListenerContainer::broadcast(Args&&... args) {
broadcasting_++;
EVENT_LIST_LOOP_BEGIN(curr, listenerList_)
if (curr->listener) {
static_cast<Listener<EHandler>*>(curr->listener)->invoke(std::forward<Args>(args)...);
}
EVENT_LIST_LOOP_END(curr, listenerList_)
broadcasting_--;
if (!broadcasting_ && hasPendingListeners()) {
processPendingListeners();
}
return true;
}
/**
* @brief 广
*/
template<typename EHandler, typename... Args>
void broadcast(Args&&... args) {
static_assert(sizeof...(Args) == EHandler::ARG_COUNT, "Parameter count incorrect");
auto* container = ListenerDB<EHandler>::container();
container->template broadcast<EHandler, Args...>(std::forward<Args>(args)...);
}
} // namespace extra2d::event

View File

@ -0,0 +1,112 @@
#pragma once
#include <core/event/event_bus.h>
#define E2D_EVENT_BUS_NAME_(n) n##_Bus
/**
* @brief 线
*/
#define DECLARE_EVENT_BUS(BusName) \
struct BusName##_Bus { \
static constexpr const char* NAME = #BusName; \
};
/**
* @brief
*/
#define DECLARE_EVENT_0(EventName, BusName) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName)> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit() { ::extra2d::event::broadcast<EventName>(); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_1(EventName, BusName, Arg0) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0) { ::extra2d::event::broadcast<EventName>(arg0); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_2(EventName, BusName, Arg0, Arg1) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1) { ::extra2d::event::broadcast<EventName>(arg0, arg1); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_3(EventName, BusName, Arg0, Arg1, Arg2) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_4(EventName, BusName, Arg0, Arg1, Arg2, Arg3) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2, Arg3> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2, arg3); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_5(EventName, BusName, Arg0, Arg1, Arg2, Arg3, Arg4) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2, Arg3, Arg4> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2, arg3, arg4); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_6(EventName, BusName, Arg0, Arg1, Arg2, Arg3, Arg4, Arg5) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2, Arg3, Arg4, Arg5> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2, arg3, arg4, arg5); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_7(EventName, BusName, Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2, arg3, arg4, arg5, arg6); } \
};
/**
* @brief
*/
#define DECLARE_EVENT_8(EventName, BusName, Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7) \
struct EventName : ::extra2d::event::EventTrait<E2D_EVENT_BUS_NAME_(BusName), Arg0, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7> { \
using Listener = ::extra2d::event::Listener<EventName>; \
static constexpr const char* NAME = #EventName; \
static constexpr const char* BUS_NAME = E2D_EVENT_BUS_NAME_(BusName)::NAME; \
static void emit(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6, Arg7 arg7) { ::extra2d::event::broadcast<EventName>(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); } \
};

194
include/core/event/events.h Normal file
View File

@ -0,0 +1,194 @@
#pragma once
#include <core/event/event_bus_macros.h>
#include <types/base/types.h>
#include <platform/input.h>
namespace extra2d::events {
/**
* @brief 线
*/
DECLARE_EVENT_BUS(Engine)
// ============================================================================
// 应用生命周期事件
// ============================================================================
/**
* @brief
*/
DECLARE_EVENT_0(OnInit, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnShutdown, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnPause, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnResume, Engine)
/**
* @brief
* @param dt
*/
DECLARE_EVENT_1(OnUpdate, Engine, float)
/**
* @brief
* @param dt
*/
DECLARE_EVENT_1(OnLateUpdate, Engine, float)
/**
* @brief
* @param dt
*/
DECLARE_EVENT_1(OnFixedUpdate, Engine, float)
// ============================================================================
// 窗口事件
// ============================================================================
/**
* @brief
* @param width
* @param height
*/
DECLARE_EVENT_2(OnResize, Engine, int32, int32)
/**
* @brief
*/
DECLARE_EVENT_0(OnClose, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnFocus, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnBlur, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnShow, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnHide, Engine)
// ============================================================================
// 输入事件
// ============================================================================
/**
* @brief
* @param key
*/
DECLARE_EVENT_1(OnKeyDown, Engine, int32)
/**
* @brief
* @param key
*/
DECLARE_EVENT_1(OnKeyUp, Engine, int32)
/**
* @brief
* @param button
* @param x X
* @param y Y
*/
DECLARE_EVENT_3(OnMouseDown, Engine, int32, int32, int32)
/**
* @brief
* @param button
* @param x X
* @param y Y
*/
DECLARE_EVENT_3(OnMouseUp, Engine, int32, int32, int32)
/**
* @brief
* @param x X
* @param y Y
*/
DECLARE_EVENT_2(OnMouseMove, Engine, int32, int32)
/**
* @brief
* @param delta
*/
DECLARE_EVENT_1(OnMouseWheel, Engine, int32)
/**
* @brief
* @param touch
*/
DECLARE_EVENT_1(OnTouchBegan, Engine, const TouchPoint&)
/**
* @brief
* @param touch
*/
DECLARE_EVENT_1(OnTouchMoved, Engine, const TouchPoint&)
/**
* @brief
* @param touch
*/
DECLARE_EVENT_1(OnTouchEnded, Engine, const TouchPoint&)
/**
* @brief
* @param touch
*/
DECLARE_EVENT_1(OnTouchCancelled, Engine, const TouchPoint&)
// ============================================================================
// 场景事件
// ============================================================================
/**
* @brief
*/
DECLARE_EVENT_0(OnSceneLoad, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnSceneUnload, Engine)
// ============================================================================
// 系统事件
// ============================================================================
/**
* @brief
*/
DECLARE_EVENT_0(OnLowMemory, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnEnterBackground, Engine)
/**
* @brief
*/
DECLARE_EVENT_0(OnEnterForeground, Engine)
} // namespace extra2d::events

View File

@ -4,10 +4,12 @@
#include <types/ptr/ref_counted.h>
#include <types/ptr/intrusive_ptr.h>
#include <types/const/priority.h>
#include <tbb/concurrent_vector.h>
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_priority_queue.h>
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
#include <atomic>
#include <vector>
namespace extra2d {
@ -105,7 +107,7 @@ private:
bool operator<(const UpdateEntry& o) const { return pri > o.pri; }
};
tbb::concurrent_vector<UpdateEntry> updates_;
std::vector<UpdateEntry> updates_;
tbb::concurrent_hash_map<TimerTarget*, size_t> updateIndex_;
tbb::concurrent_hash_map<TimerHdl, Ptr<Timer>> timers_;
tbb::concurrent_priority_queue<UpdateEntry> updateQueue_;

View File

@ -10,6 +10,17 @@
namespace extra2d {
/**
* @brief
*/
enum class SvcState : uint8 {
None,
Inited,
Running,
Paused,
Shutdown
};
/**
* @brief
*
@ -21,6 +32,8 @@ public:
virtual bool init() { return true; }
virtual void shutdown() {}
virtual void pause() {}
virtual void resume() {}
virtual void update(float dt) {}
virtual void lateUpdate(float dt) {}
@ -29,13 +42,14 @@ public:
virtual const char* name() const = 0;
virtual int pri() const { return Pri::Default; }
bool isInited() const { return inited_; }
SvcState state() const { return state_; }
bool isInited() const { return state_ >= SvcState::Inited; }
bool isEnabled() const { return enabled_; }
void setEnabled(bool v) { enabled_ = v; }
protected:
IService() = default;
bool inited_ = false;
SvcState state_ = SvcState::None;
bool enabled_ = true;
friend class SvcMgr;
@ -50,7 +64,7 @@ class SvcMgr {
public:
static SvcMgr& inst();
void reg(Ptr<IService> svc);
void reg(IService* svc);
void unreg(const char* name);
Ptr<IService> get(const char* name);
@ -60,6 +74,8 @@ public:
}
bool initAll();
void pauseAll();
void resumeAll();
void shutdownAll();
void updateAll(float dt);

View File

@ -22,6 +22,12 @@
#include <core/scheduler.h>
#include <core/service.h>
// Platform
#include <platform/sdl2.h>
#include <platform/input.h>
#include <platform/window.h>
#include <platform/file.h>
// Utils
#include <utils/logger.h>
#include <utils/random.h>

156
include/platform/file.h Normal file
View File

@ -0,0 +1,156 @@
#pragma once
#include <types/base/types.h>
#include <core/service.h>
#include <string>
#include <vector>
#include <functional>
namespace extra2d {
/**
* @brief
*/
struct FileInfo {
std::string path;
std::string name;
bool isDir = false;
int64 size = 0;
};
/**
* @brief
*/
struct FileData {
bool ok = false;
std::vector<uint8> data;
std::string error;
operator bool() const { return ok; }
const uint8* ptr() const { return data.data(); }
size_t size() const { return data.size(); }
};
/**
* @brief
*
*
*/
class FileSvc : public IService {
public:
/**
* @brief
*/
static FileSvc& inst();
const char* name() const override { return "FileSvc"; }
int pri() const override { return Pri::System; }
bool init() override;
void shutdown() override;
/**
* @brief
*/
bool exists(const std::string& path) const;
/**
* @brief
*/
bool isDir(const std::string& path) const;
/**
* @brief
*/
FileData read(const std::string& path) const;
/**
* @brief
*/
std::string readString(const std::string& path) const;
/**
* @brief
*/
bool write(const std::string& path, const void* data, size_t size) const;
/**
* @brief
*/
bool writeString(const std::string& path, const std::string& content) const;
/**
* @brief
*/
bool append(const std::string& path, const void* data, size_t size) const;
/**
* @brief
*/
bool remove(const std::string& path) const;
/**
* @brief
*/
bool mkdir(const std::string& path) const;
/**
* @brief
*/
std::vector<FileInfo> listDir(const std::string& path) const;
/**
* @brief
*/
int64 fileSize(const std::string& path) const;
/**
* @brief
*/
std::string ext(const std::string& path) const;
/**
* @brief
*/
std::string fileName(const std::string& path) const;
/**
* @brief
*/
std::string dirName(const std::string& path) const;
/**
* @brief
*/
std::string join(const std::string& a, const std::string& b) const;
/**
* @brief
*/
std::string writableDir() const;
/**
* @brief
*/
void setAssetRoot(const std::string& root) { assetRoot_ = root; }
/**
* @brief
*/
const std::string& assetRoot() const { return assetRoot_; }
/**
* @brief
*/
std::string assetPath(const std::string& relPath) const;
private:
FileSvc() = default;
std::string assetRoot_;
std::string writableDir_;
};
#define FILE_SVC extra2d::FileSvc::inst()
} // namespace extra2d

369
include/platform/input.h Normal file
View File

@ -0,0 +1,369 @@
#pragma once
#include <SDL.h>
#include <array>
#include <core/service.h>
#include <types/base/types.h>
#include <types/ptr/ref_counted.h>
#include <vector>
namespace extra2d {
/**
* @brief
*/
using Key = SDL_Scancode;
/**
* @brief
*/
namespace Keys {
constexpr Key Unknown = SDL_SCANCODE_UNKNOWN;
constexpr Key A = SDL_SCANCODE_A;
constexpr Key B = SDL_SCANCODE_B;
constexpr Key C = SDL_SCANCODE_C;
constexpr Key D = SDL_SCANCODE_D;
constexpr Key E = SDL_SCANCODE_E;
constexpr Key F = SDL_SCANCODE_F;
constexpr Key G = SDL_SCANCODE_G;
constexpr Key H = SDL_SCANCODE_H;
constexpr Key I = SDL_SCANCODE_I;
constexpr Key J = SDL_SCANCODE_J;
constexpr Key K = SDL_SCANCODE_K;
constexpr Key L = SDL_SCANCODE_L;
constexpr Key M = SDL_SCANCODE_M;
constexpr Key N = SDL_SCANCODE_N;
constexpr Key O = SDL_SCANCODE_O;
constexpr Key P = SDL_SCANCODE_P;
constexpr Key Q = SDL_SCANCODE_Q;
constexpr Key R = SDL_SCANCODE_R;
constexpr Key S = SDL_SCANCODE_S;
constexpr Key T = SDL_SCANCODE_T;
constexpr Key U = SDL_SCANCODE_U;
constexpr Key V = SDL_SCANCODE_V;
constexpr Key W = SDL_SCANCODE_W;
constexpr Key X = SDL_SCANCODE_X;
constexpr Key Y = SDL_SCANCODE_Y;
constexpr Key Z = SDL_SCANCODE_Z;
constexpr Key Num0 = SDL_SCANCODE_0;
constexpr Key Num1 = SDL_SCANCODE_1;
constexpr Key Num2 = SDL_SCANCODE_2;
constexpr Key Num3 = SDL_SCANCODE_3;
constexpr Key Num4 = SDL_SCANCODE_4;
constexpr Key Num5 = SDL_SCANCODE_5;
constexpr Key Num6 = SDL_SCANCODE_6;
constexpr Key Num7 = SDL_SCANCODE_7;
constexpr Key Num8 = SDL_SCANCODE_8;
constexpr Key Num9 = SDL_SCANCODE_9;
constexpr Key F1 = SDL_SCANCODE_F1;
constexpr Key F2 = SDL_SCANCODE_F2;
constexpr Key F3 = SDL_SCANCODE_F3;
constexpr Key F4 = SDL_SCANCODE_F4;
constexpr Key F5 = SDL_SCANCODE_F5;
constexpr Key F6 = SDL_SCANCODE_F6;
constexpr Key F7 = SDL_SCANCODE_F7;
constexpr Key F8 = SDL_SCANCODE_F8;
constexpr Key F9 = SDL_SCANCODE_F9;
constexpr Key F10 = SDL_SCANCODE_F10;
constexpr Key F11 = SDL_SCANCODE_F11;
constexpr Key F12 = SDL_SCANCODE_F12;
constexpr Key Space = SDL_SCANCODE_SPACE;
constexpr Key Enter = SDL_SCANCODE_RETURN;
constexpr Key Escape = SDL_SCANCODE_ESCAPE;
constexpr Key Tab = SDL_SCANCODE_TAB;
constexpr Key Backspace = SDL_SCANCODE_BACKSPACE;
constexpr Key Insert = SDL_SCANCODE_INSERT;
constexpr Key Delete = SDL_SCANCODE_DELETE;
constexpr Key Home = SDL_SCANCODE_HOME;
constexpr Key End = SDL_SCANCODE_END;
constexpr Key PageUp = SDL_SCANCODE_PAGEUP;
constexpr Key PageDown = SDL_SCANCODE_PAGEDOWN;
constexpr Key Left = SDL_SCANCODE_LEFT;
constexpr Key Right = SDL_SCANCODE_RIGHT;
constexpr Key Up = SDL_SCANCODE_UP;
constexpr Key Down = SDL_SCANCODE_DOWN;
constexpr Key LeftShift = SDL_SCANCODE_LSHIFT;
constexpr Key RightShift = SDL_SCANCODE_RSHIFT;
constexpr Key LeftCtrl = SDL_SCANCODE_LCTRL;
constexpr Key RightCtrl = SDL_SCANCODE_RCTRL;
constexpr Key LeftAlt = SDL_SCANCODE_LALT;
constexpr Key RightAlt = SDL_SCANCODE_RALT;
} // namespace Keys
/**
* @brief
*/
enum class MouseBtn : uint8 {
Left = 0,
Middle = 1,
Right = 2,
X1 = 3,
X2 = 4,
Count = 5
};
/**
* @brief
*/
enum class GamepadBtn : uint8 {
A = 0,
B = 1,
X = 2,
Y = 3,
Back = 4,
Guide = 5,
Start = 6,
LeftStick = 7,
RightStick = 8,
LeftShoulder = 9,
RightShoulder = 10,
DPadUp = 11,
DPadDown = 12,
DPadLeft = 13,
DPadRight = 14,
Count = 15
};
/**
* @brief
*/
enum class GamepadAxis : uint8 {
LeftX = 0,
LeftY = 1,
RightX = 2,
RightY = 3,
TriggerLeft = 4,
TriggerRight = 5,
Count = 6
};
/**
* @brief
*/
enum class TouchState : uint8 { None = 0, Began, Moved, Ended, Cancelled };
/**
* @brief
*/
struct TouchPoint {
int64 id = 0;
float x = 0.0f;
float y = 0.0f;
float prevX = 0.0f;
float prevY = 0.0f;
float deltaX = 0.0f;
float deltaY = 0.0f;
TouchState state = TouchState::None;
float pressure = 1.0f;
};
/**
* @brief
*/
using KeyCb = Fn<void(Key)>;
using MouseBtnCb = Fn<void(MouseBtn, int32 x, int32 y)>;
using TouchCb = Fn<void(const TouchPoint &)>;
/**
* @brief
*
*
*/
class InputSvc : public IService {
public:
/**
* @brief
*/
static InputSvc &inst();
const char *name() const override { return "InputSvc"; }
int pri() const override { return Pri::Input; }
bool init() override;
void shutdown() override;
void update(float dt) override;
// ========== 键盘 ==========
/**
* @brief
*/
bool isKeyDown(Key key) const;
/**
* @brief
*/
bool isKeyPressed(Key key) const;
/**
* @brief
*/
bool isKeyReleased(Key key) const;
// ========== 鼠标 ==========
/**
* @brief
*/
void getMousePos(int32 &x, int32 &y) const;
/**
* @brief
*/
void getMousePos(float &x, float &y) const;
/**
* @brief
*/
bool isMouseBtnDown(MouseBtn btn) const;
/**
* @brief
*/
bool isMouseBtnPressed(MouseBtn btn) const;
/**
* @brief
*/
bool isMouseBtnReleased(MouseBtn btn) const;
/**
* @brief
*/
int32 getMouseWheel() const;
// ========== 触摸 ==========
/**
* @brief
*/
int32 touchCount() const;
/**
* @brief
* @param idx
* @return nullptr
*/
const TouchPoint *getTouch(int32 idx) const;
/**
* @brief ID
*/
const TouchPoint *getTouchById(int64 id) const;
/**
* @brief
*/
bool hasTouch() const { return touchCount() > 0; }
/**
* @brief
*/
const std::vector<TouchPoint> &getTouches() const { return activeTouches_; }
// ========== 游戏手柄 ==========
/**
* @brief
*/
int32 gamepadCount() const;
/**
* @brief
*/
bool isGamepadBtnDown(int32 idx, GamepadBtn btn) const;
/**
* @brief
*/
bool isGamepadBtnPressed(int32 idx, GamepadBtn btn) const;
/**
* @brief (-1.0 1.0)
*/
float getGamepadAxis(int32 idx, GamepadAxis axis) const;
// ========== 回调设置 ==========
/**
* @brief
*/
void setOnKeyDown(KeyCb cb) { onKeyDown_ = std::move(cb); }
/**
* @brief
*/
void setOnKeyUp(KeyCb cb) { onKeyUp_ = std::move(cb); }
/**
* @brief
*/
void setOnMouseDown(MouseBtnCb cb) { onMouseDown_ = std::move(cb); }
/**
* @brief
*/
void setOnMouseUp(MouseBtnCb cb) { onMouseUp_ = std::move(cb); }
/**
* @brief
*/
void setOnTouchBegan(TouchCb cb) { onTouchBegan_ = std::move(cb); }
/**
* @brief
*/
void setOnTouchMoved(TouchCb cb) { onTouchMoved_ = std::move(cb); }
/**
* @brief
*/
void setOnTouchEnded(TouchCb cb) { onTouchEnded_ = std::move(cb); }
private:
InputSvc() = default;
static constexpr int32 KEY_COUNT = SDL_NUM_SCANCODES;
static constexpr int32 MAX_GAMEPADS = 4;
static constexpr int32 MAX_TOUCHES = 10;
std::array<uint8, KEY_COUNT> keyState_{};
std::array<uint8, KEY_COUNT> keyPrev_{};
int32 mouseX_ = 0;
int32 mouseY_ = 0;
int32 mouseWheel_ = 0;
std::array<uint8, static_cast<size_t>(MouseBtn::Count)> mouseState_{};
std::array<uint8, static_cast<size_t>(MouseBtn::Count)> mousePrev_{};
std::vector<TouchPoint> activeTouches_;
std::vector<TouchPoint> endedTouches_;
SDL_GameController *gamepads_[MAX_GAMEPADS] = {};
std::array<uint8, static_cast<size_t>(GamepadBtn::Count)>
padState_[MAX_GAMEPADS];
std::array<uint8, static_cast<size_t>(GamepadBtn::Count)>
padPrev_[MAX_GAMEPADS];
KeyCb onKeyDown_;
KeyCb onKeyUp_;
MouseBtnCb onMouseDown_;
MouseBtnCb onMouseUp_;
TouchCb onTouchBegan_;
TouchCb onTouchMoved_;
TouchCb onTouchEnded_;
void processEvent(const SDL_Event &evt);
void openGamepad(int32 idx);
void closeGamepad(int32 idx);
void processTouchDown(const SDL_TouchFingerEvent &evt);
void processTouchUp(const SDL_TouchFingerEvent &evt);
void processTouchMotion(const SDL_TouchFingerEvent &evt);
friend class WindowSvc;
};
#define INPUT_SVC extra2d::InputSvc::inst()
} // namespace extra2d

59
include/platform/sdl2.h Normal file
View File

@ -0,0 +1,59 @@
#pragma once
#include <SDL.h>
#include <SDL_syswm.h>
namespace extra2d {
/**
* @brief SDL2
*
* SDL2
*/
class Sdl2 {
public:
/**
* @brief SDL2
* @return true
*/
static bool initCore();
/**
* @brief
* @return true
*/
static bool initVideo();
/**
* @brief
* @return true
*/
static bool initAudio();
/**
* @brief
* @return true
*/
static bool initGamepad();
/**
* @brief
* @return true
*/
static bool initAll();
/**
* @brief SDL2
*/
static void shutdown();
/**
* @brief
*/
static bool isInited() { return inited_; }
private:
static bool inited_;
};
} // namespace extra2d

161
include/platform/window.h Normal file
View File

@ -0,0 +1,161 @@
#pragma once
#include <SDL.h>
#include <core/service.h>
#include <string>
#include <types/base/types.h>
#include <types/math/size.h>
#include <types/math/vec2.h>
#include <types/ptr/ref_counted.h>
namespace extra2d {
/**
* @brief
*/
struct WindowCfg {
std::string title = "Extra2D";
int32 width = 1280;
int32 height = 720;
bool fullscreen = false;
bool resizable = true;
bool vsync = true;
int32 glMajor = 3;
int32 glMinor = 3;
};
/**
* @brief
*/
using ResizeCb = Fn<void(int32 w, int32 h)>;
using CloseCb = Fn<void()>;
/**
* @brief
*
* SDL2 OpenGL
*/
class WindowSvc : public IService {
public:
/**
* @brief
*/
static WindowSvc &inst();
const char *name() const override { return "WindowSvc"; }
int pri() const override { return Pri::System; }
bool init() override;
void shutdown() override;
/**
* @brief 使
*/
bool create(const WindowCfg &cfg);
/**
* @brief
* @return true false 退
*/
bool pollEvents();
/**
* @brief
*/
void swapBuffers();
/**
* @brief SDL
*/
SDL_Window *handle() const { return window_; }
/**
* @brief OpenGL
*/
SDL_GLContext glContext() const { return glCtx_; }
/**
* @brief
*/
Size getSize() const;
/**
* @brief
*/
Vec2 getPosition() const;
/**
* @brief
*/
void setSize(int32 w, int32 h);
/**
* @brief
*/
void setTitle(const std::string &title);
/**
* @brief
*/
void setFullscreen(bool fullscreen);
/**
* @brief
*/
bool isFullscreen() const;
/**
* @brief
*/
void setVsync(bool vsync);
/**
* @brief
*/
bool isVsync() const;
/**
* @brief /
*/
void setVisible(bool visible);
/**
* @brief
*/
bool isVisible() const;
/**
* @brief
*/
void setOnClose(CloseCb cb) { onClose_ = std::move(cb); }
/**
* @brief
*/
void setOnResize(ResizeCb cb) { onResize_ = std::move(cb); }
/**
* @brief
*/
void requestClose() { shouldClose_ = true; }
/**
* @brief
*/
bool shouldClose() const { return shouldClose_; }
private:
WindowSvc() = default;
SDL_Window *window_ = nullptr;
SDL_GLContext glCtx_ = nullptr;
bool shouldClose_ = false;
bool vsync_ = true;
CloseCb onClose_;
ResizeCb onResize_;
};
#define WINDOW extra2d::WindowSvc::inst()
} // namespace extra2d

View File

@ -1,5 +1,11 @@
#include <app/application.h>
#include <core/director.h>
#include <core/service.h>
#include <core/event/events.h>
#include <platform/sdl2.h>
#include <platform/window.h>
#include <platform/input.h>
#include <platform/file.h>
#include <utils/logger.h>
#include <chrono>
@ -11,6 +17,9 @@
namespace extra2d {
/**
* @brief
*/
static double getTimeSeconds() {
#ifdef __SWITCH__
struct timespec ts;
@ -67,24 +76,65 @@ bool Application::init(const AppConfig& config) {
#endif
}
if (!Sdl2::initAll()) {
E2D_LOG_ERROR("Failed to initialize SDL2");
return false;
}
SVC_MGR.reg(&WINDOW);
SVC_MGR.reg(&INPUT_SVC);
SVC_MGR.reg(&FILE_SVC);
if (!SVC_MGR.initAll()) {
E2D_LOG_ERROR("Failed to initialize services");
return false;
}
WindowCfg winCfg;
winCfg.title = config_.title;
winCfg.width = config_.width;
winCfg.height = config_.height;
winCfg.fullscreen = config_.fullscreen;
winCfg.resizable = config_.resizable;
winCfg.vsync = config_.vsync;
winCfg.glMajor = config_.glMajor;
winCfg.glMinor = config_.glMinor;
if (!WINDOW.create(winCfg)) {
E2D_LOG_ERROR("Failed to create window");
return false;
}
if (!DIRECTOR.init()) {
E2D_LOG_ERROR("Failed to initialize Director");
return false;
}
WINDOW.setOnClose([this]() {
quit();
});
initialized_ = true;
running_ = true;
events::OnInit::emit();
E2D_LOG_INFO("Application initialized successfully");
E2D_LOG_INFO("Window: {}x{}, Fullscreen: {}, VSync: {}",
config_.width, config_.height, config_.fullscreen, config_.vsync);
return true;
}
void Application::shutdown() {
if (!initialized_) return;
events::OnShutdown::emit();
E2D_LOG_INFO("Shutting down application...");
DIRECTOR.shutdown();
SVC_MGR.shutdownAll();
Sdl2::shutdown();
PlatformType platform = config_.platform;
if (platform == PlatformType::Auto) {
@ -116,7 +166,7 @@ void Application::run() {
lastFrameTime_ = getTimeSeconds();
while (running_) {
while (running_ && !WINDOW.shouldClose()) {
mainLoop();
}
}
@ -124,12 +174,15 @@ void Application::run() {
void Application::quit() {
shouldQuit_ = true;
running_ = false;
WINDOW.requestClose();
}
void Application::pause() {
if (!paused_) {
paused_ = true;
SVC_MGR.pauseAll();
DIRECTOR.pause();
events::OnPause::emit();
E2D_LOG_INFO("Application paused");
}
}
@ -137,13 +190,20 @@ void Application::pause() {
void Application::resume() {
if (paused_) {
paused_ = false;
SVC_MGR.resumeAll();
DIRECTOR.resume();
lastFrameTime_ = getTimeSeconds();
events::OnResume::emit();
E2D_LOG_INFO("Application resumed");
}
}
void Application::mainLoop() {
if (!WINDOW.pollEvents()) {
running_ = false;
return;
}
double currentTime = getTimeSeconds();
deltaTime_ = static_cast<float>(currentTime - lastFrameTime_);
lastFrameTime_ = currentTime;
@ -162,6 +222,8 @@ void Application::mainLoop() {
update();
}
WINDOW.swapBuffers();
if (!config_.vsync && config_.fpsLimit > 0) {
double frameEndTime = getTimeSeconds();
double frameTime = frameEndTime - currentTime;
@ -173,6 +235,8 @@ void Application::mainLoop() {
}
void Application::update() {
SVC_MGR.updateAll(deltaTime_);
events::OnUpdate::emit(deltaTime_);
DIRECTOR.mainLoop(deltaTime_);
}

View File

@ -0,0 +1,96 @@
#include <core/event/event_bus.h>
namespace extra2d::event {
ListenerContainer::~ListenerContainer() {
ListenerEntry* curr = listenerList_;
while (curr) {
ListenerEntry* next = curr->next;
delete curr;
curr = next;
}
curr = listenersToAdd_;
while (curr) {
ListenerEntry* next = curr->next;
delete curr;
curr = next;
}
for (auto* entry : listenersToRemove_) {
delete entry;
}
}
void ListenerContainer::addListener(ListenerBase* listener) {
if (!listener || !listener->entry_) return;
if (broadcasting_ > 0) {
listener->entry_->next = listenersToAdd_;
listenersToAdd_ = listener->entry_;
} else {
listener->entry_->next = listenerList_;
listener->entry_->prev = nullptr;
if (listenerList_) {
listenerList_->prev = listener->entry_;
}
listenerList_ = listener->entry_;
}
}
void ListenerContainer::removeListener(ListenerBase* listener) {
if (!listener || !listener->entry_) return;
if (broadcasting_ > 0) {
listenersToRemove_.push_back(listener->entry_);
listener->entry_->listener = nullptr;
} else {
if (listener->entry_->prev) {
listener->entry_->prev->next = listener->entry_->next;
} else {
listenerList_ = listener->entry_->next;
}
if (listener->entry_->next) {
listener->entry_->next->prev = listener->entry_->prev;
}
delete listener->entry_;
listener->entry_ = nullptr;
}
}
void ListenerContainer::processPendingListeners() {
while (listenersToAdd_) {
ListenerEntry* entry = listenersToAdd_;
listenersToAdd_ = entry->next;
entry->next = listenerList_;
entry->prev = nullptr;
if (listenerList_) {
listenerList_->prev = entry;
}
listenerList_ = entry;
}
for (auto* entry : listenersToRemove_) {
if (entry->prev) {
entry->prev->next = entry->next;
} else {
listenerList_ = entry->next;
}
if (entry->next) {
entry->next->prev = entry->prev;
}
delete entry;
}
listenersToRemove_.clear();
}
bool ListenerContainer::hasPendingListeners() const {
return listenersToAdd_ != nullptr || !listenersToRemove_.empty();
}
} // namespace extra2d::event

View File

@ -8,11 +8,11 @@ SvcMgr& SvcMgr::inst() {
return instance;
}
void SvcMgr::reg(Ptr<IService> svc) {
void SvcMgr::reg(IService* svc) {
if (!svc) return;
SvcMap::accessor acc;
svcMap_.insert(acc, svc->name());
acc->second = svc;
acc->second = Ptr<IService>(svc);
sortSvcs();
}
@ -31,28 +31,49 @@ Ptr<IService> SvcMgr::get(const char* name) {
bool SvcMgr::initAll() {
for (auto& svc : sortedSvcs_) {
if (svc && !svc->isInited()) {
if (svc && svc->state_ == SvcState::None) {
if (!svc->init()) {
return false;
}
svc->inited_ = true;
svc->state_ = SvcState::Inited;
}
}
return true;
}
void SvcMgr::pauseAll() {
for (auto& svc : sortedSvcs_) {
if (svc && svc->state_ == SvcState::Running) {
svc->pause();
svc->state_ = SvcState::Paused;
}
}
}
void SvcMgr::resumeAll() {
for (auto& svc : sortedSvcs_) {
if (svc && svc->state_ == SvcState::Paused) {
svc->resume();
svc->state_ = SvcState::Running;
}
}
}
void SvcMgr::shutdownAll() {
for (auto it = sortedSvcs_.rbegin(); it != sortedSvcs_.rend(); ++it) {
if (*it && (*it)->isInited()) {
if (*it && (*it)->state_ >= SvcState::Inited) {
(*it)->shutdown();
(*it)->inited_ = false;
(*it)->state_ = SvcState::Shutdown;
}
}
}
void SvcMgr::updateAll(float dt) {
for (auto& svc : sortedSvcs_) {
if (svc && svc->isInited() && svc->isEnabled()) {
if (svc && svc->state_ >= SvcState::Inited && svc->isEnabled()) {
if (svc->state_ == SvcState::Inited) {
svc->state_ = SvcState::Running;
}
svc->update(dt);
}
}
@ -60,7 +81,7 @@ void SvcMgr::updateAll(float dt) {
void SvcMgr::lateUpdateAll(float dt) {
for (auto& svc : sortedSvcs_) {
if (svc && svc->isInited() && svc->isEnabled()) {
if (svc && svc->state_ >= SvcState::Running && svc->isEnabled()) {
svc->lateUpdate(dt);
}
}
@ -68,7 +89,7 @@ void SvcMgr::lateUpdateAll(float dt) {
void SvcMgr::fixedUpdateAll(float dt) {
for (auto& svc : sortedSvcs_) {
if (svc && svc->isInited() && svc->isEnabled()) {
if (svc && svc->state_ >= SvcState::Running && svc->isEnabled()) {
svc->fixedUpdate(dt);
}
}

214
src/platform/file.cpp Normal file
View File

@ -0,0 +1,214 @@
#include <platform/file.h>
#include <SDL.h>
#include <sys/stat.h>
#include <fstream>
#include <sstream>
#ifdef _WIN32
#include <direct.h>
#include <windows.h>
#define mkdir_impl(path, mode) _mkdir(path)
#else
#include <dirent.h>
#include <unistd.h>
#define mkdir_impl(path, mode) mkdir(path, mode)
#endif
namespace extra2d {
FileSvc& FileSvc::inst() {
static FileSvc instance;
return instance;
}
bool FileSvc::init() {
if (isInited()) return true;
writableDir_ = SDL_GetPrefPath("Extra2D", "Extra2D");
if (writableDir_.empty()) {
writableDir_ = "./";
}
state_ = SvcState::Inited;
return true;
}
void FileSvc::shutdown() {
state_ = SvcState::Shutdown;
}
bool FileSvc::exists(const std::string& path) const {
struct stat st;
return stat(path.c_str(), &st) == 0;
}
bool FileSvc::isDir(const std::string& path) const {
struct stat st;
if (stat(path.c_str(), &st) != 0) return false;
return (st.st_mode & S_IFDIR) != 0;
}
FileData FileSvc::read(const std::string& path) const {
FileData result;
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
result.error = "Cannot open file: " + path;
return result;
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
result.data.resize(static_cast<size_t>(size));
if (!file.read(reinterpret_cast<char*>(result.data.data()), size)) {
result.error = "Failed to read file: " + path;
return result;
}
result.ok = true;
return result;
}
std::string FileSvc::readString(const std::string& path) const {
std::ifstream file(path);
if (!file.is_open()) return "";
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
bool FileSvc::write(const std::string& path, const void* data, size_t size) const {
std::ofstream file(path, std::ios::binary);
if (!file.is_open()) return false;
file.write(static_cast<const char*>(data), static_cast<std::streamsize>(size));
return file.good();
}
bool FileSvc::writeString(const std::string& path, const std::string& content) const {
return write(path, content.data(), content.size());
}
bool FileSvc::append(const std::string& path, const void* data, size_t size) const {
std::ofstream file(path, std::ios::binary | std::ios::app);
if (!file.is_open()) return false;
file.write(static_cast<const char*>(data), static_cast<std::streamsize>(size));
return file.good();
}
bool FileSvc::remove(const std::string& path) const {
return std::remove(path.c_str()) == 0;
}
bool FileSvc::mkdir(const std::string& path) const {
#ifdef _WIN32
return mkdir_impl(path.c_str(), 0755) == 0 || errno == EEXIST;
#else
return mkdir_impl(path.c_str(), 0755) == 0 || errno == EEXIST;
#endif
}
std::vector<FileInfo> FileSvc::listDir(const std::string& path) const {
std::vector<FileInfo> result;
#ifdef _WIN32
WIN32_FIND_DATAA findData;
std::string searchPath = path + "\\*";
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData);
if (hFind == INVALID_HANDLE_VALUE) return result;
do {
std::string name = findData.cFileName;
if (name == "." || name == "..") continue;
FileInfo info;
info.name = name;
info.path = join(path, name);
info.isDir = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
if (!info.isDir) {
info.size = static_cast<int64>(findData.nFileSizeLow) |
(static_cast<int64>(findData.nFileSizeHigh) << 32);
}
result.push_back(info);
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
#else
DIR* dir = opendir(path.c_str());
if (!dir) return result;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
std::string name = entry->d_name;
if (name == "." || name == "..") continue;
FileInfo info;
info.name = name;
info.path = join(path, name);
info.isDir = isDir(info.path);
if (!info.isDir) {
info.size = fileSize(info.path);
}
result.push_back(info);
}
closedir(dir);
#endif
return result;
}
int64 FileSvc::fileSize(const std::string& path) const {
struct stat st;
if (stat(path.c_str(), &st) != 0) return -1;
return static_cast<int64>(st.st_size);
}
std::string FileSvc::ext(const std::string& path) const {
size_t pos = path.find_last_of('.');
if (pos == std::string::npos || pos == 0) return "";
size_t lastSep = path.find_last_of("/\\");
if (lastSep != std::string::npos && pos < lastSep) return "";
return path.substr(pos + 1);
}
std::string FileSvc::fileName(const std::string& path) const {
size_t pos = path.find_last_of("/\\");
if (pos == std::string::npos) return path;
return path.substr(pos + 1);
}
std::string FileSvc::dirName(const std::string& path) const {
size_t pos = path.find_last_of("/\\");
if (pos == std::string::npos) return ".";
if (pos == 0) return "/";
return path.substr(0, pos);
}
std::string FileSvc::join(const std::string& a, const std::string& b) const {
if (a.empty()) return b;
if (b.empty()) return a;
char last = a.back();
if (last == '/' || last == '\\') {
return a + b;
}
return a + "/" + b;
}
std::string FileSvc::writableDir() const {
return writableDir_;
}
std::string FileSvc::assetPath(const std::string& relPath) const {
if (assetRoot_.empty()) return relPath;
return join(assetRoot_, relPath);
}
} // namespace extra2d

365
src/platform/input.cpp Normal file
View File

@ -0,0 +1,365 @@
#include <platform/input.h>
#include <platform/window.h>
#include <SDL.h>
#include <cstring>
#include <algorithm>
namespace extra2d {
InputSvc& InputSvc::inst() {
static InputSvc instance;
return instance;
}
bool InputSvc::init() {
if (isInited()) return true;
std::memset(keyState_.data(), 0, KEY_COUNT);
std::memset(keyPrev_.data(), 0, KEY_COUNT);
std::memset(mouseState_.data(), 0, static_cast<size_t>(MouseBtn::Count));
std::memset(mousePrev_.data(), 0, static_cast<size_t>(MouseBtn::Count));
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
gamepads_[i] = nullptr;
std::memset(padState_[i].data(), 0, static_cast<size_t>(GamepadBtn::Count));
std::memset(padPrev_[i].data(), 0, static_cast<size_t>(GamepadBtn::Count));
}
activeTouches_.clear();
activeTouches_.reserve(MAX_TOUCHES);
endedTouches_.clear();
int32 numJoysticks = SDL_NumJoysticks();
for (int32 i = 0; i < numJoysticks && i < MAX_GAMEPADS; ++i) {
if (SDL_IsGameController(i)) {
openGamepad(i);
}
}
state_ = SvcState::Inited;
return true;
}
void InputSvc::shutdown() {
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
closeGamepad(i);
}
activeTouches_.clear();
endedTouches_.clear();
state_ = SvcState::Shutdown;
}
void InputSvc::update(float dt) {
std::memcpy(keyPrev_.data(), keyState_.data(), KEY_COUNT);
std::memcpy(mousePrev_.data(), mouseState_.data(), static_cast<size_t>(MouseBtn::Count));
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
if (gamepads_[i]) {
std::memcpy(padPrev_[i].data(), padState_[i].data(), static_cast<size_t>(GamepadBtn::Count));
}
}
mouseWheel_ = 0;
const uint8* state = SDL_GetKeyboardState(nullptr);
std::memcpy(keyState_.data(), state, KEY_COUNT);
uint32 btnState = SDL_GetMouseState(&mouseX_, &mouseY_);
mouseState_[static_cast<size_t>(MouseBtn::Left)] = (btnState & SDL_BUTTON_LMASK) ? 1 : 0;
mouseState_[static_cast<size_t>(MouseBtn::Middle)] = (btnState & SDL_BUTTON_MMASK) ? 1 : 0;
mouseState_[static_cast<size_t>(MouseBtn::Right)] = (btnState & SDL_BUTTON_RMASK) ? 1 : 0;
mouseState_[static_cast<size_t>(MouseBtn::X1)] = (btnState & SDL_BUTTON_X1MASK) ? 1 : 0;
mouseState_[static_cast<size_t>(MouseBtn::X2)] = (btnState & SDL_BUTTON_X2MASK) ? 1 : 0;
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
if (gamepads_[i]) {
SDL_GameController* gc = gamepads_[i];
padState_[i][static_cast<size_t>(GamepadBtn::A)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_A);
padState_[i][static_cast<size_t>(GamepadBtn::B)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_B);
padState_[i][static_cast<size_t>(GamepadBtn::X)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_X);
padState_[i][static_cast<size_t>(GamepadBtn::Y)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_Y);
padState_[i][static_cast<size_t>(GamepadBtn::Back)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_BACK);
padState_[i][static_cast<size_t>(GamepadBtn::Guide)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_GUIDE);
padState_[i][static_cast<size_t>(GamepadBtn::Start)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_START);
padState_[i][static_cast<size_t>(GamepadBtn::LeftStick)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_LEFTSTICK);
padState_[i][static_cast<size_t>(GamepadBtn::RightStick)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_RIGHTSTICK);
padState_[i][static_cast<size_t>(GamepadBtn::LeftShoulder)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
padState_[i][static_cast<size_t>(GamepadBtn::RightShoulder)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
padState_[i][static_cast<size_t>(GamepadBtn::DPadUp)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_UP);
padState_[i][static_cast<size_t>(GamepadBtn::DPadDown)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_DOWN);
padState_[i][static_cast<size_t>(GamepadBtn::DPadLeft)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_LEFT);
padState_[i][static_cast<size_t>(GamepadBtn::DPadRight)] = SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_RIGHT);
}
}
endedTouches_.clear();
for (auto& touch : activeTouches_) {
if (touch.state == TouchState::Ended || touch.state == TouchState::Cancelled) {
endedTouches_.push_back(touch);
}
}
activeTouches_.erase(
std::remove_if(activeTouches_.begin(), activeTouches_.end(),
[](const TouchPoint& t) {
return t.state == TouchState::Ended || t.state == TouchState::Cancelled;
}),
activeTouches_.end());
for (auto& touch : activeTouches_) {
touch.prevX = touch.x;
touch.prevY = touch.y;
touch.deltaX = 0.0f;
touch.deltaY = 0.0f;
if (touch.state == TouchState::Began) {
touch.state = TouchState::Moved;
}
}
}
void InputSvc::processEvent(const SDL_Event& evt) {
switch (evt.type) {
case SDL_KEYDOWN:
if (evt.key.repeat == 0 && onKeyDown_) {
onKeyDown_(static_cast<Key>(evt.key.keysym.scancode));
}
break;
case SDL_KEYUP:
if (onKeyUp_) {
onKeyUp_(static_cast<Key>(evt.key.keysym.scancode));
}
break;
case SDL_MOUSEBUTTONDOWN:
if (onMouseDown_) {
onMouseDown_(static_cast<MouseBtn>(evt.button.button - 1), evt.button.x, evt.button.y);
}
break;
case SDL_MOUSEBUTTONUP:
if (onMouseUp_) {
onMouseUp_(static_cast<MouseBtn>(evt.button.button - 1), evt.button.x, evt.button.y);
}
break;
case SDL_MOUSEWHEEL:
mouseWheel_ = evt.wheel.y;
break;
case SDL_CONTROLLERDEVICEADDED:
openGamepad(evt.cdevice.which);
break;
case SDL_CONTROLLERDEVICEREMOVED:
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
if (gamepads_[i] && SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(gamepads_[i])) == evt.cdevice.which) {
closeGamepad(i);
break;
}
}
break;
case SDL_FINGERDOWN:
processTouchDown(evt.tfinger);
break;
case SDL_FINGERUP:
processTouchUp(evt.tfinger);
break;
case SDL_FINGERMOTION:
processTouchMotion(evt.tfinger);
break;
}
}
void InputSvc::processTouchDown(const SDL_TouchFingerEvent& evt) {
if (static_cast<int32>(activeTouches_.size()) >= MAX_TOUCHES) return;
int32 winW = 0, winH = 0;
SDL_GetWindowSize(SDL_GetWindowFromID(evt.windowID), &winW, &winH);
TouchPoint touch;
touch.id = static_cast<int64>(evt.fingerId);
touch.x = evt.x * static_cast<float>(winW);
touch.y = evt.y * static_cast<float>(winH);
touch.prevX = touch.x;
touch.prevY = touch.y;
touch.deltaX = 0.0f;
touch.deltaY = 0.0f;
touch.state = TouchState::Began;
touch.pressure = evt.pressure;
activeTouches_.push_back(touch);
if (onTouchBegan_) {
onTouchBegan_(touch);
}
}
void InputSvc::processTouchUp(const SDL_TouchFingerEvent& evt) {
int64 fingerId = static_cast<int64>(evt.fingerId);
for (auto& touch : activeTouches_) {
if (touch.id == fingerId) {
int32 winW = 0, winH = 0;
SDL_GetWindowSize(SDL_GetWindowFromID(evt.windowID), &winW, &winH);
touch.x = evt.x * static_cast<float>(winW);
touch.y = evt.y * static_cast<float>(winH);
touch.deltaX = touch.x - touch.prevX;
touch.deltaY = touch.y - touch.prevY;
touch.state = TouchState::Ended;
touch.pressure = evt.pressure;
if (onTouchEnded_) {
onTouchEnded_(touch);
}
break;
}
}
}
void InputSvc::processTouchMotion(const SDL_TouchFingerEvent& evt) {
int64 fingerId = static_cast<int64>(evt.fingerId);
for (auto& touch : activeTouches_) {
if (touch.id == fingerId) {
int32 winW = 0, winH = 0;
SDL_GetWindowSize(SDL_GetWindowFromID(evt.windowID), &winW, &winH);
touch.x = evt.x * static_cast<float>(winW);
touch.y = evt.y * static_cast<float>(winH);
touch.deltaX = touch.x - touch.prevX;
touch.deltaY = touch.y - touch.prevY;
touch.state = TouchState::Moved;
touch.pressure = evt.pressure;
if (onTouchMoved_) {
onTouchMoved_(touch);
}
break;
}
}
}
void InputSvc::openGamepad(int32 idx) {
if (idx < 0 || idx >= MAX_GAMEPADS) return;
if (gamepads_[idx]) return;
SDL_GameController* gc = SDL_GameControllerOpen(idx);
if (gc) {
gamepads_[idx] = gc;
std::memset(padState_[idx].data(), 0, static_cast<size_t>(GamepadBtn::Count));
std::memset(padPrev_[idx].data(), 0, static_cast<size_t>(GamepadBtn::Count));
}
}
void InputSvc::closeGamepad(int32 idx) {
if (idx < 0 || idx >= MAX_GAMEPADS) return;
if (gamepads_[idx]) {
SDL_GameControllerClose(gamepads_[idx]);
gamepads_[idx] = nullptr;
}
}
bool InputSvc::isKeyDown(Key key) const {
if (key < 0 || key >= KEY_COUNT) return false;
return keyState_[key] != 0;
}
bool InputSvc::isKeyPressed(Key key) const {
if (key < 0 || key >= KEY_COUNT) return false;
return keyState_[key] != 0 && keyPrev_[key] == 0;
}
bool InputSvc::isKeyReleased(Key key) const {
if (key < 0 || key >= KEY_COUNT) return false;
return keyState_[key] == 0 && keyPrev_[key] != 0;
}
void InputSvc::getMousePos(int32& x, int32& y) const {
x = mouseX_;
y = mouseY_;
}
void InputSvc::getMousePos(float& x, float& y) const {
x = static_cast<float>(mouseX_);
y = static_cast<float>(mouseY_);
}
bool InputSvc::isMouseBtnDown(MouseBtn btn) const {
size_t idx = static_cast<size_t>(btn);
if (idx >= static_cast<size_t>(MouseBtn::Count)) return false;
return mouseState_[idx] != 0;
}
bool InputSvc::isMouseBtnPressed(MouseBtn btn) const {
size_t idx = static_cast<size_t>(btn);
if (idx >= static_cast<size_t>(MouseBtn::Count)) return false;
return mouseState_[idx] != 0 && mousePrev_[idx] == 0;
}
bool InputSvc::isMouseBtnReleased(MouseBtn btn) const {
size_t idx = static_cast<size_t>(btn);
if (idx >= static_cast<size_t>(MouseBtn::Count)) return false;
return mouseState_[idx] == 0 && mousePrev_[idx] != 0;
}
int32 InputSvc::getMouseWheel() const {
return mouseWheel_;
}
int32 InputSvc::touchCount() const {
return static_cast<int32>(activeTouches_.size());
}
const TouchPoint* InputSvc::getTouch(int32 idx) const {
if (idx < 0 || idx >= static_cast<int32>(activeTouches_.size())) return nullptr;
return &activeTouches_[idx];
}
const TouchPoint* InputSvc::getTouchById(int64 id) const {
for (const auto& touch : activeTouches_) {
if (touch.id == id) return &touch;
}
return nullptr;
}
int32 InputSvc::gamepadCount() const {
int32 count = 0;
for (int32 i = 0; i < MAX_GAMEPADS; ++i) {
if (gamepads_[i]) ++count;
}
return count;
}
bool InputSvc::isGamepadBtnDown(int32 idx, GamepadBtn btn) const {
if (idx < 0 || idx >= MAX_GAMEPADS) return false;
if (!gamepads_[idx]) return false;
size_t b = static_cast<size_t>(btn);
if (b >= static_cast<size_t>(GamepadBtn::Count)) return false;
return padState_[idx][b] != 0;
}
bool InputSvc::isGamepadBtnPressed(int32 idx, GamepadBtn btn) const {
if (idx < 0 || idx >= MAX_GAMEPADS) return false;
if (!gamepads_[idx]) return false;
size_t b = static_cast<size_t>(btn);
if (b >= static_cast<size_t>(GamepadBtn::Count)) return false;
return padState_[idx][b] != 0 && padPrev_[idx][b] == 0;
}
float InputSvc::getGamepadAxis(int32 idx, GamepadAxis axis) const {
if (idx < 0 || idx >= MAX_GAMEPADS) return 0.0f;
if (!gamepads_[idx]) return 0.0f;
SDL_GameControllerAxis sdlAxis = SDL_CONTROLLER_AXIS_INVALID;
switch (axis) {
case GamepadAxis::LeftX: sdlAxis = SDL_CONTROLLER_AXIS_LEFTX; break;
case GamepadAxis::LeftY: sdlAxis = SDL_CONTROLLER_AXIS_LEFTY; break;
case GamepadAxis::RightX: sdlAxis = SDL_CONTROLLER_AXIS_RIGHTX; break;
case GamepadAxis::RightY: sdlAxis = SDL_CONTROLLER_AXIS_RIGHTY; break;
case GamepadAxis::TriggerLeft: sdlAxis = SDL_CONTROLLER_AXIS_TRIGGERLEFT; break;
case GamepadAxis::TriggerRight: sdlAxis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; break;
default: return 0.0f;
}
int16 value = SDL_GameControllerGetAxis(gamepads_[idx], sdlAxis);
return static_cast<float>(value) / 32767.0f;
}
} // namespace extra2d

66
src/platform/sdl2.cpp Normal file
View File

@ -0,0 +1,66 @@
#include <platform/sdl2.h>
namespace extra2d {
bool Sdl2::inited_ = false;
bool Sdl2::initCore() {
if (inited_) return true;
if (SDL_Init(SDL_INIT_EVENTS) != 0) {
return false;
}
inited_ = true;
return true;
}
bool Sdl2::initVideo() {
if (!initCore()) return false;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
return false;
}
return true;
}
bool Sdl2::initAudio() {
if (!initCore()) return false;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
return false;
}
return true;
}
bool Sdl2::initGamepad() {
if (!initCore()) return false;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
return false;
}
return true;
}
bool Sdl2::initAll() {
if (inited_) return true;
if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
return false;
}
inited_ = true;
return true;
}
void Sdl2::shutdown() {
if (inited_) {
SDL_Quit();
inited_ = false;
}
}
} // namespace extra2d

177
src/platform/window.cpp Normal file
View File

@ -0,0 +1,177 @@
#include <platform/window.h>
#include <platform/input.h>
#include <platform/sdl2.h>
#include <SDL.h>
namespace extra2d {
WindowSvc& WindowSvc::inst() {
static WindowSvc instance;
return instance;
}
bool WindowSvc::init() {
if (isInited()) return true;
if (!Sdl2::initVideo()) {
return false;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
state_ = SvcState::Inited;
return true;
}
void WindowSvc::shutdown() {
if (glCtx_) {
SDL_GL_DeleteContext(glCtx_);
glCtx_ = nullptr;
}
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
}
state_ = SvcState::Shutdown;
}
bool WindowSvc::create(const WindowCfg& cfg) {
if (window_) return true;
uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
if (cfg.resizable) {
flags |= SDL_WINDOW_RESIZABLE;
}
if (cfg.fullscreen) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, cfg.glMajor);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, cfg.glMinor);
window_ = SDL_CreateWindow(
cfg.title.c_str(),
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
cfg.width,
cfg.height,
flags
);
if (!window_) {
return false;
}
glCtx_ = SDL_GL_CreateContext(window_);
if (!glCtx_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
return false;
}
setVsync(cfg.vsync);
vsync_ = cfg.vsync;
return true;
}
bool WindowSvc::pollEvents() {
SDL_Event evt;
while (SDL_PollEvent(&evt)) {
switch (evt.type) {
case SDL_QUIT:
shouldClose_ = true;
if (onClose_) onClose_();
break;
case SDL_WINDOWEVENT:
if (evt.window.event == SDL_WINDOWEVENT_RESIZED) {
if (onResize_) {
onResize_(evt.window.data1, evt.window.data2);
}
}
break;
default:
if (INPUT_SVC.isInited()) {
INPUT_SVC.processEvent(evt);
}
break;
}
}
return !shouldClose_;
}
void WindowSvc::swapBuffers() {
if (window_ && glCtx_) {
SDL_GL_SwapWindow(window_);
}
}
Size WindowSvc::getSize() const {
if (!window_) return Size(0, 0);
int w, h;
SDL_GetWindowSize(window_, &w, &h);
return Size(w, h);
}
Vec2 WindowSvc::getPosition() const {
if (!window_) return Vec2(0, 0);
int x, y;
SDL_GetWindowPosition(window_, &x, &y);
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
void WindowSvc::setSize(int32 w, int32 h) {
if (window_) {
SDL_SetWindowSize(window_, w, h);
}
}
void WindowSvc::setTitle(const std::string& title) {
if (window_) {
SDL_SetWindowTitle(window_, title.c_str());
}
}
void WindowSvc::setFullscreen(bool fullscreen) {
if (window_) {
SDL_SetWindowFullscreen(window_, fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
}
}
bool WindowSvc::isFullscreen() const {
if (!window_) return false;
uint32 flags = SDL_GetWindowFlags(window_);
return (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;
}
void WindowSvc::setVsync(bool vsync) {
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
vsync_ = vsync;
}
bool WindowSvc::isVsync() const {
return vsync_;
}
void WindowSvc::setVisible(bool visible) {
if (window_) {
if (visible) {
SDL_ShowWindow(window_);
} else {
SDL_HideWindow(window_);
}
}
}
bool WindowSvc::isVisible() const {
if (!window_) return false;
uint32 flags = SDL_GetWindowFlags(window_);
return (flags & SDL_WINDOW_SHOWN) != 0;
}
} // namespace extra2d

View File

@ -24,7 +24,9 @@ function define_tbb_target()
local plat = get_current_plat()
if plat == "mingw" then
add_defines("_UNICODE", "UNICODE")
-- TBB uses ANSI Windows APIs, don't define UNICODE
-- MinGW doesn't define LOAD_LIBRARY_SAFE_CURRENT_DIRS
add_defines("LOAD_LIBRARY_SAFE_CURRENT_DIRS=0x00002000")
elseif plat == "switch" then
add_defines("__TBB_USE_THREAD_SANITIZER=0")
add_defines("TBB_USE_ASSERT=0")