GameMaker跨平台C++扩展开发:YYRunnerInterface接口与实战指南

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_GetRealYYR_SetReal是接口提供的辅助函数。这比直接使用裸的double func(double)要复杂,但功能强大得多,可以处理可变参数、不同类型参数和复杂的错误处理。

2.2 内存管理与生命周期陷阱

这是C++扩展开发中最容易崩溃的地方,YYRunnerInterface如果设计得好,必须提供清晰的指引。

  • 字符串内存:如前所述,传入的char*不要释放。传出的char*,如果你是自己newmalloc的,接口需要提供如YYR_CreateString这样的函数,它会复制字符串内容并登记内存,最后由GameMaker统一清理。绝对避免将局部变量的地址(栈内存)作为字符串返回,函数结束栈帧销毁后,指针就悬空了。
  • 缓冲区操作:通过地址字符串解析得到的指针,指向的是GameMaker缓冲区对象内部的内存。你可以安全地读取和写入(在缓冲区大小范围内),但不能reallocfree。任何越界写入都会导致不可预知的崩溃,这种崩溃在GameMaker调试器中可能难以定位。
  • 全局状态与静态变量:如果你的扩展需要维护全局状态(如连接句柄、配置缓存),要非常小心。GameMaker可能在多个房间(Room)间切换,触发多次扩展的加载和卸载(取决于平台和导出设置)。静态变量在DLL/SO的生命周期内是持久的,但这可能不符合预期。好的实践是提供显式的InitializeCleanup函数,并在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):

  1. 使用上述CMake配置,选择windows-x64-release预设。
  2. 构建后得到MyExtension.dll
  3. 在GameMaker中创建扩展,添加此DLL作为“Windows”平台的代理文件。
  4. 在扩展中定义外部函数,External Name必须与C++函数名完全一致(如YYR_Fibonacci),并设置正确的参数和返回类型(doublestring)。

Linux (Ubuntu):

  1. 需要在Linux环境(物理机、虚拟机或WSL2)中构建。确保安装了g++、cmake、ninja。
  2. 构建得到libMyExtension.so
  3. 在GameMaker扩展中添加此.so文件,并勾选“Ubuntu (Linux)”平台。

Android (通过JNI):这是最复杂的部分,YYRunnerInterface的理想形态是自动生成大部分胶水代码。手动操作步骤如下:

  1. 修改CMakeLists.txt:在platforms/android/CMakeLists.txt中,你需要链接Android NDK的log库,并设置正确的编译标志。
  2. 创建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; } // … 其他函数
  3. 创建Java包装类:在platforms/android/java/目录下创建MyExtension.java,其包名和函数需要与JNI函数签名匹配。
  4. 生成JAR/AAR包:将编译好的.so文件(针对arm64-v8a,armeabi-v7a,x86_64)按照jniLibs的目录结构放入,并打包成JAR,或更好的方式——制作一个AAR库。
  5. GameMaker集成:在扩展的Android设置中,指定“Class name”为你Java类的全名(如com.yoyogames.runner.MyExtension),并将包含.so的JAR/AAR文件添加到扩展的Android依赖中。

macOS / iOS:

  1. macOS:与Linux类似,构建得到libMyExtension.dylib。在GameMaker中添加并选择“macOS”平台。
  2. 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_logprintf:在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. 使用javahjavac -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 设计建议与最佳实践

  1. 薄封装层:你的C++扩展核心逻辑应该尽可能纯粹,与GameMaker接口分离。将平台相关的导出代码(JNI桥接、导出宏)放在单独的源文件中。核心逻辑只依赖于标准C++和你的业务库。
  2. 版本化接口:考虑在你的扩展中定义一个版本号常量,并在初始化函数中返回。这样,当未来更新扩展接口时,GML脚本可以检测到版本不匹配,给出清晰的错误提示,而不是神秘崩溃。
  3. 提供详尽的GML包装脚本:不要让你的用户直接调用external_define定义的函数。为他们编写完整的、带有JSDoc注释的GML脚本函数。这些脚本负责参数检查、错误码转换、资源清理(如删除缓冲区),并提供清晰的用法示例。
  4. 单元测试:为你的C++核心逻辑编写独立的单元测试(使用Google Test等框架)。这能确保在修改代码或适配新平台时,核心功能依然正确。跨平台适配本身已经足够复杂,不要再为算法bug买单。

最后,拥抱YYRunnerInterface这类范式,本质上是将平台复杂性封装起来。虽然初始学习曲线较陡,但一旦建立起这套跨平台构建和接口体系,后续为GameMaker添加任何原生功能都将变得事半功倍。它让你能更专注于功能实现本身,而不是无休止地与编译器、链接器和平台SDK搏斗。当你看到自己编写的同一份C++代码,流畅运行在从PC到手机的不同设备上,并为你的GameMaker游戏带来强大能力时,这一切的折腾都是值得的。