This commit is contained in:
wcjbr
2026-08-16 13:32:51 +08:00
parent 9b5d76a6ac
commit d36fd6d2ee
25 changed files with 2836 additions and 4 deletions
+6
View File
@@ -0,0 +1,6 @@
Language: Cpp
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortIfStatementsOnASingleLine: false
AlignTrailingComments: true
+4
View File
@@ -0,0 +1,4 @@
out
.vscode
.cache
.idea
-3
View File
@@ -1,3 +0,0 @@
[submodule "third_party/ConsoleLib"]
path = third_party/ConsoleLib
url = https://github.com/ZeroOSProject/ConsoleLib.git
+25
View File
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.10.0)
project(opencge VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_library(opencge STATIC
src/opencge.cpp
src/game.cpp
src/render.cpp
src/config.cpp
src/engine.cpp
)
target_include_directories(opencge PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(opencge PUBLIC cxx_std_17)
find_package(glfw3 REQUIRED)
find_package(Vulkan REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(JSONCPP REQUIRED IMPORTED_TARGET jsoncpp)
target_link_libraries(opencge PUBLIC glfw Vulkan::Vulkan PkgConfig::JSONCPP)
add_executable(square_example examples/square/main.cpp)
target_link_libraries(square_example PRIVATE opencge)
add_executable(opencge_engine src/engine_main.cpp)
target_link_libraries(opencge_engine PRIVATE opencge)
+17
View File
@@ -0,0 +1,17 @@
{
"version": 8,
"configurePresets": [
{
"name": "Clang",
"displayName": "使用工具链文件配置预设",
"description": "设置 Ninja 生成器、版本和安装目录",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
}
]
}
+1
View File
@@ -0,0 +1 @@
out/build/Clang/compile_commands.json
+26
View File
@@ -0,0 +1,26 @@
{
"window": {
"width": 800,
"height": 600,
"title": "OpenCGE - config.ceg"
},
"input": [
{
"action": "quit",
"keys": ["Escape"]
}
],
"actors": [
{
"name": "square",
"position": { "x": 0, "y": 0 },
"facing": { "x": 1, "y": 0 },
"mesh": {
"shape": "quad",
"color": { "r": 0.5, "g": 0.5, "b": 0.5, "a": 1.0 }
}
}
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"window": {
"width": 800,
"height": 600,
"title": "OpenCGE - Square Example"
},
"input": [
{ "action": "quit", "keys": ["Escape"] },
{ "action": "reset", "mouse_buttons": ["Left"] },
{ "action": "move_left", "keys": ["Left"] },
{ "action": "move_right", "keys": ["Right"] },
{ "action": "move_up", "keys": ["Up"] },
{ "action": "move_down", "keys": ["Down"] }
],
"actors": [
{
"name": "square",
"init": "square_init",
"tick": "square_tick",
"position": { "x": 0, "y": 0 },
"facing": { "x": 1, "y": 0 },
"mesh": {
"shape": "quad",
"color": { "r": 0.2, "g": 0.8, "b": 0.9, "a": 1.0 }
}
}
]
}
+49
View File
@@ -0,0 +1,49 @@
#include <cmath>
#include <openceg.hpp>
namespace {
void register_square_behaviors() {
auto& behaviors = openceg::BehaviorRegistry::getInstance();
behaviors.register_init("square_init", [](openceg::Actor& actor) {
actor.set_position({0.0F, 0.0F});
actor.set_facing({1.0F, 0.0F});
if (actor.mesh() != nullptr) {
actor.mesh()->set_color({0.2F, 0.8F, 0.9F, 1.0F});
}
});
behaviors.register_tick(
"square_tick",
[](openceg::Actor& actor, float dt, const openceg::render::InputMap& input) {
const float t = actor.age();
actor.set_facing({
std::cos(t * 1.5F),
std::sin(t * 1.5F),
});
if (actor.mesh() != nullptr) {
actor.mesh()->set_color({
0.5F + 0.5F * std::sin(t * 2.0F),
0.5F + 0.5F * std::sin(t * 2.0F + 2.094F),
0.5F + 0.5F * std::sin(t * 2.0F + 4.189F),
1.0F,
});
}
auto position = actor.position();
const float move_speed = 0.7F;
position[0] += input.get_axis("move_left", "move_right") * move_speed * dt;
position[1] += input.get_axis("move_up", "move_down") * move_speed * dt;
actor.set_position(position);
});
}
} // namespace
int main(int argc, char** argv) {
register_square_behaviors();
return openceg::run_engine(argc, argv, "examples/square/config.ceg");
}
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <game.hpp>
#include <mesh.hpp>
namespace openceg::render {
class InputMap;
}
namespace openceg {
class Actor {
private:
size_t id;
std::string nickName_;
mutable std::mutex transform_mutex_;
std::array<float, 2> position_{0.0F, 0.0F};
std::array<float, 2> facing_{1.0F, 0.0F};
std::shared_ptr<Mesh> mesh_;
float age_ = 0.0F;
bool initialized_ = false;
std::function<void(Actor&)> init_handler_;
std::function<void(Actor&, float, const render::InputMap&)> tick_handler_;
public:
Actor() noexcept = default;
virtual ~Actor() = default;
Actor(const Actor&) = delete;
Actor& operator=(const Actor&) = delete;
Actor(Actor&&) noexcept = delete;
Actor& operator=(Actor&&) noexcept = delete;
Actor(std::string nickName, Game& game) {
id = game.new_id();
nickName_ = std::move(nickName);
}
Actor(std::string nickName, Game& game, std::shared_ptr<Mesh> mesh)
: Actor(std::move(nickName), game) {
mesh_ = std::move(mesh);
}
static Actor& getInstance() {
static Actor instance;
return instance;
}
const std::string& name() const { return nickName_; }
float age() const { return age_; }
void set_init_handler(std::function<void(Actor&)> handler) {
init_handler_ = std::move(handler);
}
void set_tick_handler(
std::function<void(Actor&, float, const render::InputMap&)> handler) {
tick_handler_ = std::move(handler);
}
// 类似 Godot 的 _ready()。
void initialize() {
if (initialized_) {
return;
}
initialized_ = true;
if (init_handler_) {
init_handler_(*this);
}
}
// 类似 Godot 的 _process(delta)。
void process(float dt, const render::InputMap& input) {
age_ += dt;
if (tick_handler_) {
tick_handler_(*this, dt, input);
}
}
std::array<float, 2> position() const {
std::lock_guard<std::mutex> lock(transform_mutex_);
return position_;
}
void set_position(std::array<float, 2> position) {
std::lock_guard<std::mutex> lock(transform_mutex_);
position_ = position;
}
std::array<float, 2> facing() const {
std::lock_guard<std::mutex> lock(transform_mutex_);
return facing_;
}
void set_facing(std::array<float, 2> facing) {
std::lock_guard<std::mutex> lock(transform_mutex_);
facing_ = facing;
}
const std::shared_ptr<Mesh>& mesh() const { return mesh_; }
void set_mesh(std::shared_ptr<Mesh> mesh) { mesh_ = std::move(mesh); }
};
} // namespace openceg
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>
#include <actor.hpp>
namespace openceg::render {
class InputMap;
}
namespace openceg {
using ActorInitHandler = std::function<void(Actor&)>;
using ActorTickHandler = std::function<void(Actor&, float, const render::InputMap&)>;
// 全局行为函数注册表。config.ceg 中的 init/tick 名字在这里解析为 C++ 处理函数。
class BehaviorRegistry {
public:
static BehaviorRegistry& getInstance() {
static BehaviorRegistry instance;
return instance;
}
BehaviorRegistry(const BehaviorRegistry&) = delete;
BehaviorRegistry& operator=(const BehaviorRegistry&) = delete;
void register_init(const std::string& name, ActorInitHandler handler) {
std::lock_guard<std::mutex> lock(mutex_);
init_handlers_[name] = std::move(handler);
}
void register_tick(const std::string& name, ActorTickHandler handler) {
std::lock_guard<std::mutex> lock(mutex_);
tick_handlers_[name] = std::move(handler);
}
ActorInitHandler init(const std::string& name) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = init_handlers_.find(name);
if (it == init_handlers_.end()) {
return {};
}
return it->second;
}
ActorTickHandler tick(const std::string& name) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = tick_handlers_.find(name);
if (it == tick_handlers_.end()) {
return {};
}
return it->second;
}
private:
BehaviorRegistry() = default;
mutable std::mutex mutex_;
std::unordered_map<std::string, ActorInitHandler> init_handlers_;
std::unordered_map<std::string, ActorTickHandler> tick_handlers_;
};
} // namespace openceg
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
namespace openceg {
struct WindowConfig {
int width = 800;
int height = 600;
std::string title = "OpenCGE";
};
struct MeshConfig {
std::string shape = "quad";
std::array<float, 4> color{0.5F, 0.5F, 0.5F, 1.0F};
};
struct ActorConfig {
std::string name;
std::string init_function;
std::string tick_function;
std::array<float, 2> position{0.0F, 0.0F};
std::array<float, 2> facing{1.0F, 0.0F};
MeshConfig mesh;
};
struct InputBindingConfig {
std::string action;
std::vector<int> keys;
std::vector<int> mouse_buttons;
};
struct GameConfig {
WindowConfig window;
std::vector<InputBindingConfig> input_bindings;
std::vector<ActorConfig> actors;
};
// 从 JSON 加载游戏场景配置(类似 Godot 的场景描述)。
class ConfigLoader {
public:
bool load(const std::string& path);
const GameConfig& config() const;
const std::string& last_error() const;
private:
GameConfig config_;
std::string last_error_;
};
} // namespace openceg
+9
View File
@@ -0,0 +1,9 @@
#pragma once
namespace openceg {
// 通用引擎入口:读取 config.ceg,实例化场景并驱动 Actor 的 init/tick。
// 具体游戏行为通过 BehaviorRegistry 注册,因此引擎本身不内置任何游戏逻辑。
int run_engine(int argc, char** argv, const char* default_config_path = "config.ceg");
} // namespace openceg
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstddef>
namespace openceg {
class Game {
private:
size_t id_cnt;
public:
Game();
~Game();
Game(const Game&) = delete;
Game& operator=(const Game&) = delete;
Game(Game&&) noexcept = delete;
Game& operator=(Game&&) noexcept = delete;
static Game& getInstance();
void launch();
size_t new_id();
};
} // namespace openceg
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <array>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
namespace openceg {
// 渲染组件:描述 Actor 的几何形状、颜色和纹理占位。
class Mesh {
public:
struct Vertex {
std::array<float, 2> position;
std::array<float, 4> color;
};
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
std::string texture; // 纹理路径占位,暂未采样。
std::array<float, 4> color() const {
std::lock_guard<std::mutex> lock(*color_mutex_);
return color_;
}
void set_color(std::array<float, 4> color) {
std::lock_guard<std::mutex> lock(*color_mutex_);
color_ = color;
}
static Mesh quad() {
Mesh mesh;
mesh.vertices = {
{{-1.0F, -1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}},
{{1.0F, -1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}},
{{1.0F, 1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}},
{{-1.0F, 1.0F}, {1.0F, 1.0F, 1.0F, 1.0F}},
};
mesh.indices = {0, 1, 2, 2, 3, 0};
return mesh;
}
private:
std::unique_ptr<std::mutex> color_mutex_ = std::make_unique<std::mutex>();
std::array<float, 4> color_{1.0F, 1.0F, 1.0F, 1.0F};
};
} // namespace openceg
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <game.hpp>
#include <mesh.hpp>
#include <actor.hpp>
#include <behavior.hpp>
#include <render.hpp>
#include <config.hpp>
#include <engine.hpp>
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <array>
#include <atomic>
#include <cstddef>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <actor.hpp>
namespace openceg::render {
struct InputEvent {
enum class Type {
Key,
MouseButton,
CursorMove,
Scroll,
};
enum class Action {
Release = 0,
Press = 1,
Repeat = 2,
};
Type type = Type::Key;
Action action = Action::Press;
int key = -1;
int scancode = -1;
int mods = 0;
int button = -1;
double x = 0.0;
double y = 0.0;
};
class Renderer {
public:
Renderer();
~Renderer();
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
Renderer(Renderer&&) = delete;
Renderer& operator=(Renderer&&) = delete;
void init(int width, int height, const char* title);
void draw(const Actor* const* actors, size_t actor_count);
bool should_close() const;
void poll_events() const;
using InputCallback = std::function<void(const InputEvent&)>;
void set_input_callback(InputCallback callback);
bool is_key_down(int key) const;
bool is_mouse_button_down(int button) const;
std::array<double, 2> cursor_position() const;
void destroy();
private:
struct Impl;
Impl* impl_;
};
// 类似 Godot InputMap 的按键动作绑定。
class InputMap {
public:
void add_action(const std::string& action);
void bind_key(const std::string& action, int key);
void unbind_key(const std::string& action, int key);
void bind_mouse_button(const std::string& action, int button);
void unbind_mouse_button(const std::string& action, int button);
void handle_event(const InputEvent& event);
void end_frame();
bool is_action_pressed(const std::string& action) const;
bool is_action_just_pressed(const std::string& action) const;
bool is_action_just_released(const std::string& action) const;
float get_action_strength(const std::string& action) const;
float get_axis(const std::string& negative_action,
const std::string& positive_action) const;
private:
struct ActionState {
std::unordered_set<int> keys;
std::unordered_set<int> mouse_buttons;
std::unordered_map<int, bool> held_keys;
std::unordered_map<int, bool> held_buttons;
size_t held_key_count = 0;
size_t held_button_count = 0;
bool pressed = false;
bool just_pressed = false;
bool just_released = false;
};
mutable std::mutex mutex_;
std::unordered_map<std::string, ActionState> actions_;
};
// 渲染线程包装:主线程更新 Actor,渲染线程持续读取并提交绘制。
class RenderLoop {
public:
RenderLoop() = default;
~RenderLoop();
RenderLoop(const RenderLoop&) = delete;
RenderLoop& operator=(const RenderLoop&) = delete;
RenderLoop(RenderLoop&&) = delete;
RenderLoop& operator=(RenderLoop&&) = delete;
void start(int width, int height, const char* title);
void stop();
void set_actors(std::vector<Actor*> actors);
void poll_events() const;
bool should_close() const;
InputMap& input();
const InputMap& input() const;
private:
void run();
Renderer renderer_;
InputMap input_map_;
std::thread thread_;
std::atomic<bool> running_{false};
mutable std::mutex actors_mutex_;
std::vector<Actor*> actors_;
};
} // namespace openceg::render
+323
View File
@@ -0,0 +1,323 @@
#include <config.hpp>
#include <json/json.h>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <string>
#include <vector>
namespace openceg {
namespace {
std::string lowercase(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
int parse_key_name(const std::string& name) {
const std::string key = lowercase(name);
if (key == "space") return 32;
if (key == "apostrophe" || key == "'") return 39;
if (key == "comma" || key == ",") return 44;
if (key == "minus" || key == "-") return 45;
if (key == "period" || key == ".") return 46;
if (key == "slash" || key == "/") return 47;
if (key.size() == 1 && key[0] >= '0' && key[0] <= '9') return key[0];
if (key.size() == 1 && key[0] >= 'a' && key[0] <= 'z') return key[0] - 'a' + 'A';
if (key == "semicolon" || key == ";") return 59;
if (key == "equal" || key == "=") return 61;
if (key == "leftbracket" || key == "[") return 91;
if (key == "backslash" || key == "\\") return 92;
if (key == "rightbracket" || key == "]") return 93;
if (key == "graveaccent" || key == "`") return 96;
if (key == "escape" || key == "esc") return 256;
if (key == "enter" || key == "return") return 257;
if (key == "tab") return 258;
if (key == "backspace") return 259;
if (key == "insert") return 260;
if (key == "delete" || key == "del") return 261;
if (key == "right") return 262;
if (key == "left") return 263;
if (key == "down") return 264;
if (key == "up") return 265;
if (key == "pageup" || key == "pgup") return 266;
if (key == "pagedown" || key == "pgdown" || key == "pgdn") return 267;
if (key == "home") return 268;
if (key == "end") return 269;
if (key == "capslock" || key == "caps") return 280;
if (key == "scrolllock") return 281;
if (key == "numlock") return 282;
if (key == "printscreen" || key == "prtscr") return 283;
if (key == "pause") return 284;
if (key.size() > 1 && key[0] == 'f') {
try {
int number = std::stoi(key.substr(1));
if (number >= 1 && number <= 25) return 290 + number - 1;
} catch (...) {
}
}
if (key == "kp0" || key == "keypad0") return 320;
if (key == "kp1" || key == "keypad1") return 321;
if (key == "kp2" || key == "keypad2") return 322;
if (key == "kp3" || key == "keypad3") return 323;
if (key == "kp4" || key == "keypad4") return 324;
if (key == "kp5" || key == "keypad5") return 325;
if (key == "kp6" || key == "keypad6") return 326;
if (key == "kp7" || key == "keypad7") return 327;
if (key == "kp8" || key == "keypad8") return 328;
if (key == "kp9" || key == "keypad9") return 329;
if (key == "leftshift" || key == "lshift") return 340;
if (key == "rightshift" || key == "rshift") return 344;
if (key == "leftcontrol" || key == "leftctrl" || key == "lctrl") return 341;
if (key == "rightcontrol" || key == "rightctrl" || key == "rctrl") return 345;
if (key == "leftalt" || key == "lalt") return 342;
if (key == "rightalt" || key == "ralt") return 346;
if (key == "leftsuper" || key == "lsuper") return 343;
if (key == "rightsuper" || key == "rsuper") return 347;
try {
return std::stoi(name);
} catch (...) {
return -1;
}
}
int parse_mouse_button(const std::string& name) {
const std::string button = lowercase(name);
if (button == "left") return 0;
if (button == "right") return 1;
if (button == "middle") return 2;
try {
return std::stoi(name);
} catch (...) {
return -1;
}
}
int json_int(const Json::Value& value, int fallback) {
if (value.isNumeric()) {
return value.asInt();
}
if (value.isString()) {
try {
return std::stoi(value.asString());
} catch (...) {
}
}
return fallback;
}
float json_float(const Json::Value& value, float fallback) {
if (value.isNumeric()) {
return value.asFloat();
}
if (value.isString()) {
try {
return std::stof(value.asString());
} catch (...) {
}
}
return fallback;
}
std::string json_string(const Json::Value& value, const std::string& fallback) {
if (value.isString()) {
return value.asString();
}
if (value.isNumeric()) {
return std::to_string(value.asInt());
}
return fallback;
}
void add_binding_code(const Json::Value& value, int type, std::vector<int>& output) {
if (value.isArray()) {
for (Json::ArrayIndex i = 0; i < value.size(); ++i) {
add_binding_code(value[i], type, output);
}
return;
}
int code = -1;
if (value.isNumeric()) {
code = value.asInt();
} else if (value.isString()) {
code = type == 0 ? parse_key_name(value.asString())
: parse_mouse_button(value.asString());
}
if (code >= 0) {
output.push_back(code);
}
}
void parse_window(const Json::Value& root, WindowConfig& window) {
if (!root.isMember("window") || !root["window"].isObject()) {
return;
}
const Json::Value& value = root["window"];
if (value.isMember("width")) {
window.width = json_int(value["width"], window.width);
}
if (value.isMember("height")) {
window.height = json_int(value["height"], window.height);
}
if (value.isMember("title")) {
window.title = json_string(value["title"], window.title);
}
}
void parse_input(const Json::Value& root, std::vector<InputBindingConfig>& bindings) {
if (!root.isMember("input") || !root["input"].isArray()) {
return;
}
for (const Json::Value& item : root["input"]) {
if (!item.isObject()) {
continue;
}
InputBindingConfig binding;
binding.action = item.isMember("action")
? json_string(item["action"], "")
: json_string(item["name"], "");
if (item.isMember("key")) {
add_binding_code(item["key"], 0, binding.keys);
}
if (item.isMember("keys")) {
add_binding_code(item["keys"], 0, binding.keys);
}
if (item.isMember("mouse")) {
add_binding_code(item["mouse"], 1, binding.mouse_buttons);
}
if (item.isMember("mouse_buttons")) {
add_binding_code(item["mouse_buttons"], 1, binding.mouse_buttons);
}
if (!binding.action.empty()) {
bindings.push_back(std::move(binding));
}
}
}
void parse_color(const Json::Value& mesh, MeshConfig& mesh_config) {
if (!mesh.isObject() || !mesh.isMember("color")) {
return;
}
const Json::Value& color = mesh["color"];
if (color.isObject()) {
if (color.isMember("r")) mesh_config.color[0] = json_float(color["r"], mesh_config.color[0]);
if (color.isMember("g")) mesh_config.color[1] = json_float(color["g"], mesh_config.color[1]);
if (color.isMember("b")) mesh_config.color[2] = json_float(color["b"], mesh_config.color[2]);
if (color.isMember("a")) mesh_config.color[3] = json_float(color["a"], mesh_config.color[3]);
} else if (color.isArray()) {
for (Json::ArrayIndex i = 0; i < color.size() && i < 4; ++i) {
mesh_config.color[i] = json_float(color[i], mesh_config.color[i]);
}
}
}
ActorConfig parse_actor(const Json::Value& value) {
ActorConfig actor;
if (value.isMember("name")) {
actor.name = json_string(value["name"], "");
}
if (value.isMember("init")) {
actor.init_function = json_string(value["init"], "");
}
if (value.isMember("tick")) {
actor.tick_function = json_string(value["tick"], "");
}
if (value.isMember("position") && value["position"].isObject()) {
const Json::Value& position = value["position"];
actor.position[0] = json_float(position["x"], actor.position[0]);
actor.position[1] = json_float(position["y"], actor.position[1]);
}
if (value.isMember("facing") && value["facing"].isObject()) {
const Json::Value& facing = value["facing"];
actor.facing[0] = json_float(facing["x"], actor.facing[0]);
actor.facing[1] = json_float(facing["y"], actor.facing[1]);
}
if (value.isMember("mesh") && value["mesh"].isObject()) {
const Json::Value& mesh = value["mesh"];
if (mesh.isMember("shape")) {
actor.mesh.shape = json_string(mesh["shape"], actor.mesh.shape);
}
parse_color(mesh, actor.mesh);
}
return actor;
}
void parse_actors(const Json::Value& root, std::vector<ActorConfig>& actors) {
const Json::Value* list = nullptr;
if (root.isMember("actors") && root["actors"].isArray()) {
list = &root["actors"];
} else if (root.isMember("scene")) {
const Json::Value& scene = root["scene"];
if (scene.isArray()) {
list = &scene;
} else if (scene.isObject() && scene.isMember("actors") && scene["actors"].isArray()) {
list = &scene["actors"];
}
}
if (list == nullptr) {
return;
}
for (const Json::Value& item : *list) {
if (!item.isObject()) {
continue;
}
ActorConfig actor = parse_actor(item);
if (!actor.name.empty()) {
actors.push_back(std::move(actor));
}
}
}
} // namespace
bool ConfigLoader::load(const std::string& path) {
Json::Value root;
Json::CharReaderBuilder builder;
std::ifstream file(path);
std::string errors;
if (!Json::parseFromStream(builder, file, &root, &errors)) {
last_error_ = "无法解析 JSON 配置文件: " + path + " - " + errors;
return false;
}
if (!root.isObject()) {
last_error_ = "JSON 根节点必须是对象: " + path;
return false;
}
config_ = GameConfig{};
parse_window(root, config_.window);
parse_input(root, config_.input_bindings);
parse_actors(root, config_.actors);
return true;
}
const GameConfig& ConfigLoader::config() const {
return config_;
}
const std::string& ConfigLoader::last_error() const {
return last_error_;
}
} // namespace openceg
+117
View File
@@ -0,0 +1,117 @@
#include <engine.hpp>
#include <chrono>
#include <cstdio>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <openceg.hpp>
namespace openceg {
int run_engine(int argc, char** argv, const char* default_config_path) {
std::string config_path = default_config_path != nullptr ? default_config_path : "config.ceg";
if (argc > 1) {
config_path = argv[1];
}
ConfigLoader loader;
if (!loader.load(config_path)) {
std::fprintf(stderr, "%s\n", loader.last_error().c_str());
return 1;
}
const GameConfig& config = loader.config();
Game& game = Game::getInstance();
BehaviorRegistry& behaviors = BehaviorRegistry::getInstance();
std::vector<std::shared_ptr<Actor>> actor_storage;
std::vector<Actor*> actors;
for (const ActorConfig& actor_config : config.actors) {
auto mesh = std::make_shared<Mesh>();
if (actor_config.mesh.shape == "quad") {
*mesh = Mesh::quad();
} else {
// 当前渲染器只提供 quad;未知形状先按 quad 创建,便于后续扩展。
*mesh = Mesh::quad();
}
mesh->set_color(actor_config.mesh.color);
auto actor = std::make_shared<Actor>(actor_config.name, game, mesh);
actor->set_position(actor_config.position);
actor->set_facing(actor_config.facing);
if (!actor_config.init_function.empty()) {
auto handler = behaviors.init(actor_config.init_function);
if (handler) {
actor->set_init_handler(std::move(handler));
} else {
std::fprintf(stderr, "未找到 init 处理函数: %s\n",
actor_config.init_function.c_str());
}
}
if (!actor_config.tick_function.empty()) {
auto handler = behaviors.tick(actor_config.tick_function);
if (handler) {
actor->set_tick_handler(std::move(handler));
} else {
std::fprintf(stderr, "未找到 tick 处理函数: %s\n",
actor_config.tick_function.c_str());
}
}
actors.push_back(actor.get());
actor_storage.push_back(std::move(actor));
}
for (auto& actor : actor_storage) {
actor->initialize();
}
render::RenderLoop render_loop;
for (const InputBindingConfig& binding : config.input_bindings) {
render_loop.input().add_action(binding.action);
for (int key : binding.keys) {
render_loop.input().bind_key(binding.action, key);
}
for (int button : binding.mouse_buttons) {
render_loop.input().bind_mouse_button(binding.action, button);
}
}
// 引擎级默认退出动作,不包含具体游戏逻辑。
render_loop.input().add_action("quit");
render_loop.input().bind_key("quit", 256);
render_loop.set_actors(std::move(actors));
render_loop.start(config.window.width, config.window.height,
config.window.title.c_str());
auto last = std::chrono::steady_clock::now();
while (!render_loop.should_close()) {
render_loop.poll_events();
if (render_loop.input().is_action_just_pressed("quit")) {
break;
}
const auto now = std::chrono::steady_clock::now();
const float dt = std::chrono::duration<float>(now - last).count();
last = now;
for (auto& actor : actor_storage) {
actor->process(dt, render_loop.input());
}
render_loop.input().end_frame();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
render_loop.stop();
return 0;
}
} // namespace openceg
+5
View File
@@ -0,0 +1,5 @@
#include <engine.hpp>
int main(int argc, char** argv) {
return openceg::run_engine(argc, argv);
}
+17
View File
@@ -0,0 +1,17 @@
#include <game.hpp>
namespace openceg {
Game::Game() { id_cnt = 0; }
Game::~Game() = default;
Game& Game::getInstance() {
static Game instance;
return instance;
}
void Game::launch() {}
size_t Game::new_id() { return id_cnt++; }
}
+415
View File
@@ -0,0 +1,415 @@
// Minimal GLFW-compatible Win32 backend for cross-compiling the example.
// It implements only the subset used by src/render.cpp.
#include <windows.h>
#include <windowsx.h>
#include <array>
#include <cstdint>
#include <vector>
#define GLFW_INCLUDE_VULKAN
#define VK_USE_PLATFORM_WIN32_KHR
#include <GLFW/glfw3.h>
#include <vulkan/vulkan.h>
#include <vulkan/vulkan_win32.h>
struct GLFWwindow {
HWND handle = nullptr;
void* user_pointer = nullptr;
bool should_close = false;
int width = 0;
int height = 0;
GLFWkeyfun key_callback = nullptr;
GLFWmousebuttonfun mouse_button_callback = nullptr;
GLFWcursorposfun cursor_pos_callback = nullptr;
GLFWscrollfun scroll_callback = nullptr;
std::array<unsigned char, 1024> key_state{};
std::array<unsigned char, 8> mouse_button_state{};
double cursor_x = 0.0;
double cursor_y = 0.0;
};
namespace {
constexpr wchar_t kWindowClassName[] = L"OpenCGE_GLFWStub";
HINSTANCE g_instance = nullptr;
bool g_initialized = false;
bool g_resizable = true;
int map_key(WPARAM vk) {
if (vk >= 'A' && vk <= 'Z') {
return GLFW_KEY_A + static_cast<int>(vk - 'A');
}
if (vk >= '0' && vk <= '9') {
return GLFW_KEY_0 + static_cast<int>(vk - '0');
}
switch (vk) {
case VK_ESCAPE:
return GLFW_KEY_ESCAPE;
case VK_LEFT:
return GLFW_KEY_LEFT;
case VK_RIGHT:
return GLFW_KEY_RIGHT;
case VK_UP:
return GLFW_KEY_UP;
case VK_DOWN:
return GLFW_KEY_DOWN;
case VK_SPACE:
return GLFW_KEY_SPACE;
case VK_RETURN:
return GLFW_KEY_ENTER;
case VK_TAB:
return GLFW_KEY_TAB;
case VK_BACK:
return GLFW_KEY_BACKSPACE;
case VK_DELETE:
return GLFW_KEY_DELETE;
case VK_LSHIFT:
return GLFW_KEY_LEFT_SHIFT;
case VK_RSHIFT:
return GLFW_KEY_RIGHT_SHIFT;
case VK_LCONTROL:
return GLFW_KEY_LEFT_CONTROL;
case VK_RCONTROL:
return GLFW_KEY_RIGHT_CONTROL;
case VK_LMENU:
return GLFW_KEY_LEFT_ALT;
case VK_RMENU:
return GLFW_KEY_RIGHT_ALT;
default:
break;
}
return GLFW_KEY_UNKNOWN;
}
int current_mods() {
int mods = 0;
if (GetKeyState(VK_SHIFT) & 0x8000) {
mods |= GLFW_MOD_SHIFT;
}
if (GetKeyState(VK_CONTROL) & 0x8000) {
mods |= GLFW_MOD_CONTROL;
}
if (GetKeyState(VK_MENU) & 0x8000) {
mods |= GLFW_MOD_ALT;
}
if ((GetKeyState(VK_LWIN) & 0x8000) || (GetKeyState(VK_RWIN) & 0x8000)) {
mods |= GLFW_MOD_SUPER;
}
return mods;
}
void handle_key(GLFWwindow* window, UINT message, WPARAM wparam, LPARAM lparam) {
if (window == nullptr || window->key_callback == nullptr) {
return;
}
const int key = map_key(wparam);
const int scancode = static_cast<int>((lparam >> 16) & 0x1ff);
const int action = message == WM_KEYDOWN || message == WM_SYSKEYDOWN
? (((lparam & (1U << 30)) != 0) ? GLFW_REPEAT : GLFW_PRESS)
: GLFW_RELEASE;
if (key >= 0 && key < static_cast<int>(window->key_state.size())) {
window->key_state[key] = action == GLFW_RELEASE ? GLFW_RELEASE : GLFW_PRESS;
}
window->key_callback(window, key, scancode, action, current_mods());
}
int map_mouse_button(UINT message, WPARAM wparam) {
switch (message) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
return GLFW_MOUSE_BUTTON_LEFT;
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
return GLFW_MOUSE_BUTTON_RIGHT;
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
return GLFW_MOUSE_BUTTON_MIDDLE;
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return GET_XBUTTON_WPARAM(wparam) == XBUTTON1 ? GLFW_MOUSE_BUTTON_4
: GLFW_MOUSE_BUTTON_5;
default:
return -1;
}
}
void handle_mouse_button(GLFWwindow* window, UINT message, WPARAM wparam) {
if (window == nullptr || window->mouse_button_callback == nullptr) {
return;
}
const int button = map_mouse_button(message, wparam);
if (button < 0) {
return;
}
const bool is_down = message == WM_LBUTTONDOWN || message == WM_RBUTTONDOWN ||
message == WM_MBUTTONDOWN || message == WM_XBUTTONDOWN;
window->mouse_button_state[button] = is_down ? GLFW_PRESS : GLFW_RELEASE;
window->mouse_button_callback(window, button, is_down ? GLFW_PRESS : GLFW_RELEASE,
current_mods());
}
LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
auto* window = reinterpret_cast<GLFWwindow*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_CLOSE:
if (window != nullptr) {
window->should_close = true;
}
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
handle_key(window, message, wparam, lparam);
return 0;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
handle_mouse_button(window, message, wparam);
return 0;
case WM_MOUSEMOVE:
if (window != nullptr) {
window->cursor_x = static_cast<double>(GET_X_LPARAM(lparam));
window->cursor_y = static_cast<double>(GET_Y_LPARAM(lparam));
if (window->cursor_pos_callback != nullptr) {
window->cursor_pos_callback(window, window->cursor_x, window->cursor_y);
}
}
return 0;
case WM_MOUSEWHEEL:
if (window != nullptr && window->scroll_callback != nullptr) {
const double delta = static_cast<double>(GET_WHEEL_DELTA_WPARAM(wparam)) /
static_cast<double>(WHEEL_DELTA);
window->scroll_callback(window, 0.0, delta);
}
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wparam, lparam);
}
} // namespace
extern "C" {
int glfwInit(void) {
if (g_initialized) {
return GLFW_TRUE;
}
g_instance = GetModuleHandleW(nullptr);
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = window_proc;
wc.hInstance = g_instance;
wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
wc.lpszClassName = kWindowClassName;
if (RegisterClassExW(&wc) == 0) {
return GLFW_FALSE;
}
g_initialized = true;
return GLFW_TRUE;
}
void glfwTerminate(void) {
g_initialized = false;
}
void glfwWindowHint(int hint, int value) {
if (hint == GLFW_RESIZABLE) {
g_resizable = value != GLFW_FALSE;
}
}
GLFWwindow* glfwCreateWindow(int width, int height, const char* title, GLFWmonitor*, GLFWwindow*) {
if (!g_initialized) {
return nullptr;
}
const int wide_length = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0);
std::vector<wchar_t> wide_title(wide_length);
MultiByteToWideChar(CP_UTF8, 0, title, -1, wide_title.data(), wide_length);
DWORD style = WS_OVERLAPPEDWINDOW;
if (!g_resizable) {
style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
}
RECT rect{0, 0, width, height};
AdjustWindowRectEx(&rect, style, FALSE, 0);
HWND hwnd = CreateWindowExW(
0, kWindowClassName, wide_title.data(), style, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, g_instance, nullptr);
if (hwnd == nullptr) {
return nullptr;
}
auto* window = new GLFWwindow();
window->handle = hwnd;
window->width = width;
window->height = height;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
return window;
}
void glfwDestroyWindow(GLFWwindow* window) {
if (window == nullptr) {
return;
}
if (window->handle != nullptr) {
DestroyWindow(window->handle);
}
delete window;
}
void glfwSetWindowUserPointer(GLFWwindow* window, void* pointer) {
if (window != nullptr) {
window->user_pointer = pointer;
}
}
void* glfwGetWindowUserPointer(GLFWwindow* window) {
return window != nullptr ? window->user_pointer : nullptr;
}
GLFWkeyfun glfwSetKeyCallback(GLFWwindow* window, GLFWkeyfun callback) {
if (window == nullptr) {
return nullptr;
}
GLFWkeyfun previous = window->key_callback;
window->key_callback = callback;
return previous;
}
GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* window, GLFWmousebuttonfun callback) {
if (window == nullptr) {
return nullptr;
}
GLFWmousebuttonfun previous = window->mouse_button_callback;
window->mouse_button_callback = callback;
return previous;
}
GLFWcursorposfun glfwSetCursorPosCallback(GLFWwindow* window, GLFWcursorposfun callback) {
if (window == nullptr) {
return nullptr;
}
GLFWcursorposfun previous = window->cursor_pos_callback;
window->cursor_pos_callback = callback;
return previous;
}
GLFWscrollfun glfwSetScrollCallback(GLFWwindow* window, GLFWscrollfun callback) {
if (window == nullptr) {
return nullptr;
}
GLFWscrollfun previous = window->scroll_callback;
window->scroll_callback = callback;
return previous;
}
void glfwPollEvents(void) {
MSG msg{};
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
int glfwWindowShouldClose(GLFWwindow* window) {
return window != nullptr && window->should_close ? GLFW_TRUE : GLFW_FALSE;
}
int glfwGetKey(GLFWwindow* window, int key) {
if (window == nullptr || key < 0 || key >= static_cast<int>(window->key_state.size())) {
return GLFW_RELEASE;
}
return window->key_state[key] != 0 ? GLFW_PRESS : GLFW_RELEASE;
}
int glfwGetMouseButton(GLFWwindow* window, int button) {
if (window == nullptr || button < 0 ||
button >= static_cast<int>(window->mouse_button_state.size())) {
return GLFW_RELEASE;
}
return window->mouse_button_state[button] != 0 ? GLFW_PRESS : GLFW_RELEASE;
}
void glfwGetCursorPos(GLFWwindow* window, double* xpos, double* ypos) {
if (xpos != nullptr) {
*xpos = window != nullptr ? window->cursor_x : 0.0;
}
if (ypos != nullptr) {
*ypos = window != nullptr ? window->cursor_y : 0.0;
}
}
void glfwGetFramebufferSize(GLFWwindow* window, int* width, int* height) {
if (window == nullptr || window->handle == nullptr) {
return;
}
RECT client{};
GetClientRect(window->handle, &client);
if (width != nullptr) {
*width = client.right - client.left;
}
if (height != nullptr) {
*height = client.bottom - client.top;
}
}
const char** glfwGetRequiredInstanceExtensions(uint32_t* count) {
static const char* extensions[] = {
VK_KHR_SURFACE_EXTENSION_NAME,
VK_KHR_WIN32_SURFACE_EXTENSION_NAME,
};
if (count != nullptr) {
*count = static_cast<uint32_t>(sizeof(extensions) / sizeof(extensions[0]));
}
return extensions;
}
VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window,
const VkAllocationCallbacks* allocator,
VkSurfaceKHR* surface) {
if (instance == VK_NULL_HANDLE || window == nullptr || surface == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
auto create_surface = reinterpret_cast<PFN_vkCreateWin32SurfaceKHR>(
vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR"));
if (create_surface == nullptr) {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
VkWin32SurfaceCreateInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
info.hinstance = g_instance;
info.hwnd = window->handle;
return create_surface(instance, &info, allocator, surface);
}
} // extern "C"
+1
View File
@@ -0,0 +1 @@
#include <openceg.hpp>
+1340
View File
File diff suppressed because it is too large Load Diff