由于历史原因,OSX 没能像 Windows 一样成为游戏玩家的首选。这也导致了大部分的游戏开发的书籍关注的是 Visual C++ 上关于 Windows
的游戏编程。由于我日常用的是 Macbook Pro,最近也想复习一下 C++ 编程,想干一些有趣的事情。

<>准备工作

在 OSX 进行 C++ 开发,首选的 IDE 自然是 Apple 自家的 Xcode。为了创建游戏窗体,使用的是一个跨平台的第三方库,Simple
DirectMedia Layer(SDL <http://www.libsdl.org>),它负责处理创建窗体、2D 图形、音频输出、键盘输入等过程。

<>安装 SDL

我们直接使用 brew install SDL2 命令,就可以一键式的安装好 SDL。

<>创建 C++ 项目

* Create a new Xcode Project
* 接着选择 macOS 中的 Command Line Tool
* 填入项目名称等信息
* 创建好项目后,在 Build Phases 中的 Link Binary With Libraries 添加动态库
* 在 Build Settings 中设置 SDL 头文件的路径


brew 安装的默认路径(似乎是)/usr/local/Cellar



好了,这样一来,我们所需游戏环境就搭好了。

<>游戏循环

游戏需要在运行时每秒钟进行多次刷新
,这可能是游戏和其他程序最大的区别了。如果游戏以每秒60帧(FPS)运行,这就意味着游戏循环每秒完成60次迭代。游戏每秒运行这么多次的迭代,就造成了连续运动的错觉,实际上它只是定时在刷新。

游戏的每次循环迭代,都可以视为一个框架,基本上可以在宏观上划分成三个过程:

* 处理进程输入(例如键盘、鼠标,甚至是 GPS 或多人游戏时的网络用户数据等);
* 更新游戏的世界(游戏中的各个变量,决定游戏如何进一步发展);
* 生成输出(输出新的画面,音效等等)。
<>实现游戏骨架

有了上述的简单介绍,就可以开始开发一个基本的游戏骨架了。我们需要引入 SDL 库的头文件。我们新建一个 Game.cpp,Xcode 会默认生成
Game.hpp。

我们在 Game.hpp 中导入 SDL 的头文件:<SDL.h>:
#ifndef Game_hpp #define Game_hpp #include <SDL2/SDL.h> #endif /* Game_hpp */
<>游戏主体设计

要利用 SDL 创建一个窗体程序,我们需要引用到 SDL_Window 的指针,作为一个游戏基本过程,可以分成初始化,运行时的游戏循环,关闭游戏
这三个阶段。简而言之,创建、运行和关闭。

进一步地,根据上面对游戏循环过程的简单介绍。我们可以将游戏循环的过程划分成三个大致步骤:ProcessInput、UpdateGame、
GenerateOutput。为了判断游戏是否应该继续运行,不妨加上一个 bool 变量 mIsRunning。

C++ 作为一个支持面向对象的语言,我们可以创建一个 Game 类来完成上述设计。设计和实现可以分离出来,我们先在 Game.hpp 头文件中声明 Game
类。
// Game class class Game { public: Game(); // 初始化游戏 bool Initialize(); //
运行游戏循环直到游戏结束 void RunLoop(); // 关闭游戏 void Shutdown(); private: // 处理进程输入 void
ProcessInput(); // 更新游戏 void UpdateGame(); // 生成输出 void GenerateOutput(); // 通过
SDL 创建窗体 SDL_Window* mWindow; // 继续运行 bool mIsRunning; };
设计阶段就这样结束了,我们可以在 Game.cpp 中开始编写具体实现了。

<>骨架具体实现

Game() 作为一个简单的构造函数,只需要把 mWindow 初始化为 nullptr,mIsRunning 初始化为 true。
Game::Game() :mWindow(nullptr) ,mIsRunning(true) { }
<>初始化

Initialize 函数如果返回 true 则代表初始化成功,false 则代表创建失败。初始化时,需要先初始化 SDL 库,需要调用 SDL 库提供的
SDL_Init 函数:
// 初始化 SDL 库 int sdlResult = SDL_Init(SDL_INIT_VIDEO); if (sdlResult != 0) {
SDL_Log("不能初始化 SDL: %s", SDL_GetError()); return false; }
SDL_Init 接受一个 UInt32 的标志位子系统,需要多个子系统,只需要使用 |(OR)运算联系起来,具体的子系统列表见API 文档
<https://wiki.libsdl.org/SDL_Init#Remarks>。这是初始化的是视频子系统。SDL_Init 初始化成功时返回 0
,失败时返回负错误代码。出错时可以调用SDL_GetError() 来获取更多的信息。SDL_Log 和 printf 类似,把信息输出到控制台。

如果 SDL 库初始化成功,接下来就是用 SDL_CreateWindow 函数来创建窗体。
// 创建 SDL 窗体 mWindow = SDL_CreateWindow( "游戏环境的搭建", // 标题 100, // 窗体左上角的 x 坐标
100, // 窗体左上角的 y 坐标 1024, // 窗体宽度 768, // 窗体高度 0 // 标志位 ); if (!mWindow) {
SDL_Log("创建窗体失败: %s", SDL_GetError()); return false; }
最后的标志位可以用来控制窗体是否全屏,例如将 0 改成预置常量:SDL_WINDOW_FULLSCREEN,可以获得全屏的效果。窗体初始化的时候返回的是
SDL_Window*,因此创建失败时将是 nullptr,检查的手法和 SDL_Init 是一样的。

到了这里,初始化 SDL 和窗体都是成功的话,那么可以返回 true。
return true;
<>关闭游戏

Shutdown 和 Initialize 正好相反。由于窗体是指针类型,我们需要手动销毁:
void Game::Shutdown() { SDL_DestroyWindow(mWindow); SDL_Quit(); }
<>游戏循环

游戏循环一直迭代,直到 mIsRunning 变成 false。根据前面所说,游戏循环的过程划分成三个部分:
void Game::RunLoop() { while (mIsRunning) { ProcessInput(); UpdateGame();
GenerateOutput(); } }
<>进程输入

在任何一个桌面操作系统中,用户都可以在窗体上执行某些操作,例如移动窗体、最小化或最大化窗体(P.S.
创建窗体的时候可以通过标志位设置),还有像关闭窗体等等。这些不同的选择用不同的事件
(events)表示。用户执行不同的操作时,程序从操作系统接收到事件,并根据事件作出不同的响应。

事件可能不止一个,SDL
维护了一个内部队列来接收保存操作系统传入的事件。当然,并不是我们需要处理所有的事件,对于用户一些无理请求,我们可以选择直接忽略。如果这些输入的代码将实现在
ProcessInput()中。

SDL_PollEvent 将在访问 SDL 内部事件队列,如果队列存在事件,将返回 true。因此,一个基本的实现就是一直调用 SDL_PollEvent
,只要它返回true:
void Game::ProcessInput() { SDL_Event event; // 有 event 在队列就一直循环 while (
SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: mIsRunning = false
; break; default: break; } } }
SDL 内部队列的元素类型是 SDL_Event,由于函数需要修改 event,所以需要传递元素指针(SDL 是用 C语言 编写的,因此叫指针,不叫引用了)。
SDL_Event 的类型实际上是 union,在这里是 Uint32 的类型。

在这里,我们处理了 SDL_QUIT 类型的事件,现在可以实现关闭窗口了。好了,先运行一下,看看效果。

<>main 函数

要运行程序,main 作为入口是必不可少的。实现起来也很简单,初始化,然后进入游戏循环,退出游戏循环后,关闭游戏。
#include "Game.hpp" int main(int argc, const char * argv[]) { Game game; bool
success= game.Initialize(); if (success) { game.RunLoop(); } game.Shutdown();
return 0; }
编译运行(另外两个函数先定义,但留空就行),效果是这样的:

现在点击左上角的叉,可以关闭这个窗口了。

<>进程输入(续)

用户如果希望按 ESC 键退出窗体,实现的原理也是一样的。键盘的输入同样是事件,要获取键盘的状态用 SDL_GetKeyboardState
,它将返回一个包含当前键盘状态的数组指针。可以通过检查SDL_SCANCODE 的值来获取该键的状态,ESC 键就是 SDL_SCANCODE_ESCAPE。
void Game::ProcessInput() { SDL_Event event; // 有 event 在队列就一直循环 while (
SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: mIsRunning = false
; break; default: break; } } // 获取键盘的状态 const Uint8* state =
SDL_GetKeyboardState(NULL); // 如果按了 Esc,结束循环 if (state[SDL_SCANCODE_ESCAPE]) {
mIsRunning= false; } }
可以编译一下试试。至于后面两个函数,就暂时先搁置一下,留空处理。在编写这两个函数之前,先了解一下 2D 图形如何在游戏上工作。好了,下次继续。

<>最终

虽然很简单,但是记录一下:


下一篇:Xcode与C++之游戏开发:2D图形
<https://blog.csdn.net/guyu2019/article/details/87474735>

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信