GameMaker跨平台C++扩展开发:YYRunnerInterface接口与实战指南
1. 项目概述与核心价值
如果你在GameMaker社区里混过一段时间,尤其是当你需要做一些GameMaker内置功能无法直接实现的事情时,你肯定听说过“C++扩展”这个词。它能让你突破GML的限制,直接调用底层系统API、使用高性能的第三方C/C++库,或者实现一些对性能要求极高的算法。但说实话,从零开始制作一个跨平台的C++扩展,尤其是要覆盖Windows、Linux、Android、macOS和iOS,这个过程就像在迷宫里找路,充满了各种平台特有的编译工具链、ABI兼容性、JNI接口和签名机制等“坑”。
传统的做法是为每个平台单独写一套构建脚本,处理不同的导出宏(比如Windows的__declspec(dllexport)和Linux的extern “C”),再为移动端折腾JNI或Objective-C桥接。这个过程繁琐、易错,且难以维护。而“YYRunnerInterface”的出现,正是为了解决这个核心痛点。它不是一个具体的、功能性的扩展,而是一个标准化的接口层和构建范式。你可以把它理解为一套“游戏规则”或“最佳实践模板”,它定义了GameMaker的运行时(Runner)如何与你的原生代码安全、高效地对话。
这套接口的核心价值在于抽象与统一。它通过一组预定义的宏和函数签名,将不同平台下(YYC,即YoYo Compiler)的底层调用细节隐藏起来。对于扩展开发者而言,你不再需要为#ifdef _WIN32之类的平台判断而头疼,也不再需要手动编写复杂的JNI胶水代码。你只需要按照YYRunnerInterface约定的方式编写你的C++函数,剩下的跨平台适配工作,可以由配套的构建系统(比如基于CMake的模板)来完成。这极大地降低了开发门槛,提升了代码的可移植性和可维护性。简单来说,它让“写一次,到处编译”在GameMaker原生扩展开发中变得更接近现实。
2. YYRunnerInterface 深度解析:接口、宏与内存模型
要玩转YYRunnerInterface,不能只停留在“怎么用”的层面,必须理解它“为什么这么设计”。这涉及到GameMaker运行时与原生代码交互的几个根本性约束和设计哲学。
2.1 核心接口函数与数据类型
GameMaker的虚拟机(VM)与原生扩展通信时,数据交换被严格限制在几种基本类型上。YYRunnerInterface正是基于这些约束建立的桥梁。
基本数据类型映射:
- 双精度浮点数 (double): 这是GameMaker中
real类型在C++中的对应。所有数值(整数、浮点数)在跨边界传递时都会被提升为double。在YYRunnerInterface的约定下,你的C++函数如果返回或接收数值,就应该使用double类型。 - C风格字符串 (char*): 对应GameMaker的
string类型。这里有一个至关重要的细节:GameMaker传递给C++的字符串是只读的,并且其生命周期由GameMaker管理。你绝不能修改它,也不能假设它在你的函数返回后依然有效。如果需要返回字符串,你必须返回一个指向新分配内存的char*,并且GameMaker运行时会负责在拷贝后释放它(通过特定的释放回调,如果接口定义了的话)。更安全的做法是使用YYRunnerInterface可能提供的字符串创建函数。 - 缓冲区指针 (void* / int64_t as string): 这是处理复杂数据的核心。GameMaker不能直接传递结构体或对象指针。它通过
buffer_get_address()函数获得一个缓冲区的内存地址,这个地址值(一个64位整数)会被转换成字符串(16进制表示)传递给C++函数。你的C++函数需要将这个字符串解析回实际的指针。这就是为什么在基础教程中你会看到getGMSBuffAddress这样的辅助函数。YYRunnerInterface的理想形态是内置这个解析逻辑,或者提供更安全的包装函数。
一个典型的函数签名约定:一个遵循YYRunnerInterface规范的函数可能看起来像这样(假设接口提供了YYRValue等类型来封装参数):
// 假设的YYRunnerInterface核心宏,用于声明导出函数 #define YYR_EXPORT extern “C” // 使用接口提供的类型和函数 YYR_EXPORT double YYR_MyExtensionFunction(const YYRValue* args, int argCount, YYRValue* retVal) { // 1. 通过args数组和argCount解析参数 if (argCount < 1) { // 使用接口函数设置错误信息到retVal YYR_SetError(retVal, “Insufficient arguments”); return -1; // 或特定的错误码 } double inputValue = YYR_GetReal(args, 0); // 从第一个参数获取double值 // 2. 执行核心逻辑 double result = inputValue * 2.0; // 3. 通过接口函数设置返回值 YYR_SetReal(retVal, result); return 0; // 返回0表示成功 }这里的YYRValue是一个联合体(union)或结构体,可以表示double、string、buffer等多种类型,YYR_GetReal、YYR_SetReal是接口提供的辅助函数。这比直接使用裸的double func(double)要复杂,但功能强大得多,可以处理可变参数、不同类型参数和复杂的错误处理。
2.2 内存管理与生命周期陷阱
这是C++扩展开发中最容易崩溃的地方,YYRunnerInterface如果设计得好,必须提供清晰的指引。
- 字符串内存:如前所述,传入的
char*不要释放。传出的char*,如果你是自己new或malloc的,接口需要提供如YYR_CreateString这样的函数,它会复制字符串内容并登记内存,最后由GameMaker统一清理。绝对避免将局部变量的地址(栈内存)作为字符串返回,函数结束栈帧销毁后,指针就悬空了。 - 缓冲区操作:通过地址字符串解析得到的指针,指向的是GameMaker缓冲区对象内部的内存。你可以安全地读取和写入(在缓冲区大小范围内),但不能
realloc或free它。任何越界写入都会导致不可预知的崩溃,这种崩溃在GameMaker调试器中可能难以定位。 - 全局状态与静态变量:如果你的扩展需要维护全局状态(如连接句柄、配置缓存),要非常小心。GameMaker可能在多个房间(Room)间切换,触发多次扩展的加载和卸载(取决于平台和导出设置)。静态变量在DLL/SO的生命周期内是持久的,但这可能不符合预期。好的实践是提供显式的
Initialize和Cleanup函数,并在GML中成对调用。
注意:即使有YYRunnerInterface,内存管理责任依然在开发者。接口只是让“正确做事”的路径更清晰,并不能防止你写出内存泄漏或悬空指针的代码。务必为每个返回的、自己分配的内存设计好所有者,并在接口文档中明确说明。
2.3 跨平台宏与ABI兼容性
YYRunnerInterface的精髓在于用一套宏来屏蔽平台差异。一个完整的导出宏可能如下所示:
// 在某个统一的头文件,如 `YYRunnerInterface.h` 中 #if defined(_WIN32) || defined(_WIN64) #define YYR_PLATFORM_WINDOWS 1 #ifdef YYR_EXPORTING // 在构建扩展库时定义 #define YYR_API extern “C” __declspec(dllexport) #else // 在测试或其他地方包含头文件时 #define YYR_API extern “C” __declspec(dllimport) #endif #elif defined(__ANDROID__) #define YYR_PLATFORM_ANDROID 1 #define YYR_API extern “C” JNIEXPORT // JNIEXPORT 本身已包含 visibility 属性 #elif defined(__APPLE__) #include “TargetConditionals.h” #if TARGET_OS_IPHONE #define YYR_PLATFORM_IOS 1 #elif TARGET_OS_MAC #define YYR_PLATFORM_MACOS 1 #endif #define YYR_API extern “C” __attribute__((visibility(“default”))) #elif defined(__linux__) #define YYR_PLATFORM_LINUX 1 #define YYR_API extern “C” __attribute__((visibility(“default”))) #endif // 如果没有定义平台,给一个安全但可能无效的定义 #ifndef YYR_API #define YYR_API extern “C” #endif然后你的所有函数都使用YYR_API作为前缀:
YYR_API double MyExtensionFunction(double arg);这样,在Windows上编译动态库时,它会正确导出函数;在Linux/macOS上,它会设置正确的可见性属性;在Android通过JNI编译时,也能符合JNI的命名规范(如果宏与JNIEnv配合得当)。
3. 基于YYRunnerInterface的跨平台扩展实战
理解了原理,我们来看如何从零构建一个基于YYRunnerInterface理念的、真正跨平台的扩展。我们将创建一个简单的“数学工具”扩展,包含一个计算斐波那契数列的函数和一个反转字符串的函数。
3.1 项目结构与CMake配置
清晰的目录结构是成功的第一步。我建议的布局如下:
MyGMExtension/ ├── CMakeLists.txt # 根CMake配置 ├── CMakePresets.json # VS的CMake预设(方便多平台构建) ├── include/ │ └── YYRunnerInterface.h # 我们假想的统一接口头文件 ├── src/ │ ├── MyExtension.cpp # 扩展核心实现 │ └── MyExtension.h ├── platforms/ │ ├── android/ │ │ ├── CMakeLists.txt # Android特定配置 │ │ ├── AndroidManifest.xml # (如果需要额外权限) │ │ └── java/ # JNI胶水代码 │ └── ios/ │ └── CMakeLists.txt # iOS特定配置 └── gameMaker/ ├── extensions/ │ └── MyExtension.yyext # GMS扩展定义文件(可手动创建) └── scripts/ # 对应的GML包装脚本根目录的CMakeLists.txt是大脑:
cmake_minimum_required(VERSION 3.21) project(MyGMExtension LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 包含我们的接口头文件 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) # 根据平台选择不同的源文件和链接选项 if(ANDROID) add_subdirectory(platforms/android) elseif(IOS) add_subdirectory(platforms/ios) else() # 桌面平台 (Windows, Linux, macOS) add_library(MyExtension SHARED src/MyExtension.cpp) # 设置输出库名,避免平台差异(如Windows的.dll, Linux的.so) set_target_properties(MyExtension PROPERTIES OUTPUT_NAME “MyExtension”) # Windows下需要定义导出宏 if(WIN32) target_compile_definitions(MyExtension PRIVATE YYR_EXPORTING) endif() endif()CMakePresets.json用于简化Visual Studio的构建配置:
{ “version”: 3, “configurePresets”: [ { “name”: “windows-x64-release”, “displayName”: “Windows x64 Release”, “generator”: “Ninja”, “binaryDir”: “${sourceDir}/build/${presetName}”, “architecture”: { “value”: “x64”, “strategy”: “external” }, “cacheVariables”: { “CMAKE_BUILD_TYPE”: “Release” } }, { “name”: “linux-x64-release”, “displayName”: “Linux x64 Release”, “generator”: “Ninja”, “binaryDir”: “${sourceDir}/build/${presetName}”, “cacheVariables”: { “CMAKE_BUILD_TYPE”: “Release”, “CMAKE_TOOLCHAIN_FILE”: “$env{HOME}/vcpkg/scripts/buildsystems/vcpkg.cmake” # 示例,用于管理Linux依赖 }, “condition”: { “type”: “equals”, “lhs”: “${hostSystemName}”, “rhs”: “Linux” } } ] }3.2 核心C++实现(遵循接口)
首先,在include/YYRunnerInterface.h中定义我们的“接口”(这是一个简化示例,实际接口可能更复杂):
// YYRunnerInterface.h #pragma once #include <cstdint> // 跨平台导出宏 (简化版) #if defined(_WIN32) || defined(_WIN64) #ifdef YYR_EXPORTING #define YYR_API extern “C” __declspec(dllexport) #else #define YYR_API extern “C” __declspec(dllimport) #endif #else #define YYR_API extern “C” __attribute__((visibility(“default”))) #endif // 简单的返回值结构,实际接口可能更丰富 struct YYRValue { union { double real; const char* string; void* ptr; } data; int type; // 0=real, 1=string, 2=ptr, etc. }; // 辅助函数声明 (这些函数需要由扩展开发者实现,或由运行时提供) #ifdef __cplusplus extern “C” { #endif // 假设的运行时函数:用于分配一个将被GameMaker管理的字符串 YYR_API char* YYR_CreateString(const char* src); #ifdef __cplusplus } #endif然后,在src/MyExtension.cpp中实现功能:
// MyExtension.cpp #include “YYRunnerInterface.h” #include <cstring> #include <string> #include <algorithm> // 辅助函数:将GameMaker传来的地址字符串转换为指针 static void* GetBufferPointer(const char* addressStr) { // 简单实现:将16进制字符串转换为整数,再转为指针 // 注意:实际生产代码需要更严格的错误检查 uintptr_t addr = std::stoull(addressStr, nullptr, 16); return reinterpret_cast<void*>(addr); } // 1. 计算斐波那契数列 (迭代法,避免递归爆栈) YYR_API double YYR_Fibonacci(double n) { auto index = static_cast<int>(n); if (index <= 1) return static_cast<double>(index); double a = 0.0, b = 1.0, temp; for (int i = 2; i <= index; ++i) { temp = a + b; a = b; b = temp; } return b; } // 2. 反转字符串。注意:返回的字符串必须通过YYR_CreateString分配。 YYR_API const char* YYR_ReverseString(const char* input) { if (!input) return nullptr; std::string str(input); std::reverse(str.begin(), str.end()); // 关键步骤:使用假设的接口函数创建字符串,确保内存被正确管理。 // 如果YYR_CreateString不存在,你需要自己管理内存并告知GameMaker如何释放。 return YYR_CreateString(str.c_str()); } // 3. 通过缓冲区交换两个整数的值 (演示缓冲区操作) YYR_API double YYR_SwapIntsInBuffer(const char* bufferAddressStr) { void* buffer = GetBufferPointer(bufferAddressStr); if (!buffer) return -1.0; // 错误码 int* intPtr = static_cast<int*>(buffer); // 假设缓冲区足够大,包含至少两个int // 在实际应用中,你应该接收缓冲区大小作为参数并进行验证 int temp = intPtr[0]; intPtr[0] = intPtr[1]; intPtr[1] = temp; return 0.0; // 成功 }3.3 各平台构建与集成要点
Windows (x64):
- 使用上述CMake配置,选择
windows-x64-release预设。 - 构建后得到
MyExtension.dll。 - 在GameMaker中创建扩展,添加此DLL作为“Windows”平台的代理文件。
- 在扩展中定义外部函数,
External Name必须与C++函数名完全一致(如YYR_Fibonacci),并设置正确的参数和返回类型(double或string)。
Linux (Ubuntu):
- 需要在Linux环境(物理机、虚拟机或WSL2)中构建。确保安装了g++、cmake、ninja。
- 构建得到
libMyExtension.so。 - 在GameMaker扩展中添加此.so文件,并勾选“Ubuntu (Linux)”平台。
Android (通过JNI):这是最复杂的部分,YYRunnerInterface的理想形态是自动生成大部分胶水代码。手动操作步骤如下:
- 修改CMakeLists.txt:在
platforms/android/CMakeLists.txt中,你需要链接Android NDK的log库,并设置正确的编译标志。 - 创建JNI桥接层:在
platforms/android/java/目录下创建com_yoyogames_runner_MyExtension.cpp(名称需符合JNI规范)。// JNI桥接文件 #include <jni.h> #include “../../src/MyExtension.h” // 包含你的头文件 extern “C” JNIEXPORT jdouble JNICALL Java_com_yoyogames_runner_MyExtension_Fibonacci(JNIEnv* env, jclass clazz, jdouble n) { return (jdouble)YYR_Fibonacci((double)n); } extern “C” JNIEXPORT jstring JNICALL Java_com_yoyogames_runner_MyExtension_ReverseString(JNIEnv* env, jclass clazz, jstring input) { const char* nativeInput = env->GetStringUTFChars(input, nullptr); const char* result = YYR_ReverseString(nativeInput); env->ReleaseStringUTFChars(input, nativeInput); // 注意:YYR_ReverseString返回的字符串需要妥善处理生命周期。 // 这里假设result在JNI层是持久的,或者需要拷贝。 jstring jResult = env->NewStringUTF(result); // 如果YYR_CreateString分配了内存,这里可能需要一个对应的释放函数。 return jResult; } // … 其他函数 - 创建Java包装类:在
platforms/android/java/目录下创建MyExtension.java,其包名和函数需要与JNI函数签名匹配。 - 生成JAR/AAR包:将编译好的
.so文件(针对arm64-v8a,armeabi-v7a,x86_64)按照jniLibs的目录结构放入,并打包成JAR,或更好的方式——制作一个AAR库。 - GameMaker集成:在扩展的Android设置中,指定“Class name”为你Java类的全名(如
com.yoyogames.runner.MyExtension),并将包含.so的JAR/AAR文件添加到扩展的Android依赖中。
macOS / iOS:
- macOS:与Linux类似,构建得到
libMyExtension.dylib。在GameMaker中添加并选择“macOS”平台。 - iOS:最为特殊,因为iOS不允许动态加载库。你需要将C++代码编译成静态库(
.a文件),并作为“iOS原生框架”导入GameMaker项目。此外,所有导出给GameMaker的函数都需要用extern “C”包装,并且不能使用C++标准库中iOS禁止的部分(如异常、RTTI),通常需要编译为Objective-C++源文件(.mm)。YYRunnerInterface在这里的作用是提供一套预编译的、符合iOS审核规范的静态库模板和头文件,你只需要链接它并实现自己的函数。
实操心得:Android和iOS的构建过程最容易出错。一个实用的技巧是,先在桌面平台(Windows/Linux)上将核心逻辑调试通过,确保算法和内存管理无误。然后再移植到移动平台,集中精力解决JNI/Objective-C桥接和平台特定的构建问题。使用CMake的
add_subdirectory和条件编译可以很好地管理这些平台差异。
4. 高级主题:性能优化、调试与错误处理
当你掌握了基础构建流程后,接下来要关注的是让扩展变得健壮、高效。
4.1 性能关键路径优化
- 减少跨边界调用:每一次从GML调用C++函数都有开销。对于需要大量计算的任务,尽量设计成一次调用处理大量数据(通过缓冲区),而不是多次调用处理单个数据。
- 缓冲区而非字符串:传输大量数据时,使用缓冲区(
buffer)的性能远高于将其转换为字符串再传递。因为字符串涉及编码转换和内存分配,而缓冲区是原始的二进制数据块。 - 缓存与状态管理:如果扩展需要频繁初始化某个重型资源(如数据库连接、网络套接字),应在C++侧缓存其指针(作为
static变量或通过上下文参数传递),并提供一个GML可调用的“句柄”(一个代表该资源的唯一ID或索引),而不是每次调用都重新创建。
4.2 调试技巧(地狱级难度降低指南)
调试C++扩展是痛苦的,尤其是当崩溃发生在原生代码中时,GameMaker的调试器几乎帮不上忙。
- 桌面平台(Windows/Linux/macOS):
- 附加调试器:在IDE(如Visual Studio、CLion、Xcode)中打开你的C++项目,编译为Debug版本。运行GameMaker导出的独立可执行文件(Runner),然后从IDE的调试菜单选择“附加到进程”,找到并附加到Runner进程上。设置断点,当GML调用你的函数时,调试器就会中断。
- 日志输出:这是最原始但最有效的方法。在C++代码中使用平台特定的日志函数,如Windows的
OutputDebugStringA,Linux/macOS的syslog或直接写入文件。在GameMaker中,你可以通过show_debug_message查看这些输出(如果Runner配置了捕获标准输出)。
- Android:
- 使用
__android_log_print:在Android NDK中,包含<android/log.h>,使用__android_log_print(ANDROID_LOG_INFO, “MyExtension”, “Message: %d”, value);打印日志。然后通过Android Studio的Logcat查看。 - 使用GameMaker的
show_debug_message:通过JNI桥接,将C++日志信息传回Java层,再通过GameMaker的RunnerJNILib调用show_debug_message。这需要一些额外的桥接代码。
- 使用
- iOS:
- 使用
os_log或printf:在iOS C++代码中,使用os_log或简单的printf。当通过Xcode运行游戏时,日志会输出到Xcode的控制台。对于真机调试,需要配置设备的日志查看。
- 使用
4.3 健壮的错误处理与异常安全
C++异常绝不能跨越DLL边界传播到GameMaker的虚拟机。必须将所有异常捕获在C++函数内部。
YYR_API double YYR_SafeFunction(const char* input) { try { // 可能抛出异常的操作 std::string str(input); // … 处理逻辑 return 0.0; // 成功 } catch (const std::exception& e) { // 记录错误日志到文件或系统 // 返回一个预定义的错误码 return -1001.0; } catch (...) { // 捕获所有未知异常 return -1002.0; } }对于缓冲区操作,边界检查是必须的:
YYR_API double YYR_ProcessBuffer(const char* addrStr, double bufferSize) { void* buffer = GetBufferPointer(addrStr); int size = static_cast<int>(bufferSize); if (!buffer || size <= 0) { return -1.0; // 无效参数 } // 假设我们要写入一个int if (size < sizeof(int)) { return -2.0; // 缓冲区太小 } int* data = static_cast<int*>(buffer); *data = 42; return 0.0; // 成功 }在GML侧,你需要检查这些错误码,并给出友好的提示。
5. 常见问题排查与实战避坑指南
这里记录了我踩过的一些坑和解决方案,希望能帮你节省数小时的调试时间。
5.1 编译与链接问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
Windows: 链接错误LNK2001: 无法解析的外部符号 | 1. 导出宏YYR_EXPORTING未在构建扩展时定义。2. C++函数名被编译器进行了名称修饰(mangling)。 | 1. 在CMake中为target_compile_definitions添加YYR_EXPORTING。2. 确保函数声明在 extern “C”块中,或使用了YYR_API宏(其已包含extern “C”)。 |
Linux/macOS: 运行时找不到符号undefined symbol | 动态库没有正确导出函数。 | 确保编译时使用了-fvisibility=default或__attribute__((visibility(“default”)))。在CMake中,可以设置set(CMAKE_CXX_VISIBILITY_INLINES_HIDDEN ON)和set(CMAKE_CXX_VISIBILITY_PRESET default)。 |
Android:System.loadLibrary崩溃 | 1..so文件未被打包进APK。2. JNI函数签名错误。 3. C++运行时库不匹配(如使用了 c++_shared但未正确打包)。 | 1. 检查GameMaker扩展的Android设置,确保JAR/AAR包含正确ABI的.so,且路径正确(jniLibs/ABI/*.so)。2. 使用 javah或javac -h生成正确的JNI头文件进行比对。3. 在CMake中统一使用 ANDROID_STL=c++_shared,并确保APK中包含对应的.so。 |
| iOS: 构建成功,但调用时无反应或崩溃 | 1. 函数未正确暴露给Objective-C运行时。 2. 使用了iOS禁止的API或特性。 3. 静态库未正确链接到最终Runner。 | 1. 确保函数用extern “C”修饰,并在GameMaker的iOS扩展配置中正确声明。2. 避免使用C++异常、RTTI。编译时添加 -fno-exceptions -fno-rtti。3. 检查GameMaker项目的iOS设置,确保你的 .a文件在“原生框架”列表中。 |
5.2 运行时崩溃问题
- 访问违例 (Access Violation):十有八九是缓冲区越界或使用了悬空指针。仔细检查所有通过
GetBufferPointer获得的指针,确保你访问的内存范围没有超出GameMaker缓冲区的大小。在Debug构建中,可以在指针使用前后加入边界断言。 - 内存泄漏:如果你在C++中手动分配了内存(
new,malloc)并返回给GameMaker,必须提供一个对应的释放函数,并在GML对象销毁或适当的时候调用它。更好的做法是始终使用YYRunnerInterface假设的YYR_CreateString这类函数来分配返回给GameMaker的内存。 - 字符串乱码或崩溃:传入的
char*是UTF-8编码吗?GameMaker字符串内部可能是UTF-16或UTF-8,这取决于版本和平台。最安全的做法是,如果你的扩展处理的是文本,明确约定使用UTF-8。对于Windows API调用,可能需要进行宽字符(wchar_t)转换。
5.3 设计建议与最佳实践
- 薄封装层:你的C++扩展核心逻辑应该尽可能纯粹,与GameMaker接口分离。将平台相关的导出代码(JNI桥接、导出宏)放在单独的源文件中。核心逻辑只依赖于标准C++和你的业务库。
- 版本化接口:考虑在你的扩展中定义一个版本号常量,并在初始化函数中返回。这样,当未来更新扩展接口时,GML脚本可以检测到版本不匹配,给出清晰的错误提示,而不是神秘崩溃。
- 提供详尽的GML包装脚本:不要让你的用户直接调用
external_define定义的函数。为他们编写完整的、带有JSDoc注释的GML脚本函数。这些脚本负责参数检查、错误码转换、资源清理(如删除缓冲区),并提供清晰的用法示例。 - 单元测试:为你的C++核心逻辑编写独立的单元测试(使用Google Test等框架)。这能确保在修改代码或适配新平台时,核心功能依然正确。跨平台适配本身已经足够复杂,不要再为算法bug买单。
最后,拥抱YYRunnerInterface这类范式,本质上是将平台复杂性封装起来。虽然初始学习曲线较陡,但一旦建立起这套跨平台构建和接口体系,后续为GameMaker添加任何原生功能都将变得事半功倍。它让你能更专注于功能实现本身,而不是无休止地与编译器、链接器和平台SDK搏斗。当你看到自己编写的同一份C++代码,流畅运行在从PC到手机的不同设备上,并为你的GameMaker游戏带来强大能力时,这一切的折腾都是值得的。