Xcode与C++之游戏开发:创建环境

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

准备工作

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

安装 SDL

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

创建 C++ 项目

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

Xcode与C++之游戏开发:创建环境_第1张图片

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

Xcode与C++之游戏开发:创建环境_第2张图片

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

游戏循环

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

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

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

实现游戏骨架

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

我们在 Game.hpp 中导入 SDL 的头文件:

#ifndef Game_hpp
#define Game_hpp

#include 

#endif /* Game_hpp */

游戏主体设计

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

进一步地,根据上面对游戏循环过程的简单介绍。我们可以将游戏循环的过程划分成三个大致步骤:ProcessInputUpdateGameGenerateOutput。为了判断游戏是否应该继续运行,不妨加上一个 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 初始化为 nullptrmIsRunning 初始化为 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 文档。这是初始化的是视频子系统。SDL_Init 初始化成功时返回 0 ,失败时返回负错误代码。出错时可以调用 SDL_GetError() 来获取更多的信息。SDL_Logprintf 类似,把信息输出到控制台。

如果 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;

关闭游戏

ShutdownInitialize 正好相反。由于窗体是指针类型,我们需要手动销毁:

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;
}

编译运行(另外两个函数先定义,但留空就行),效果是这样的:
Xcode与C++之游戏开发:创建环境_第3张图片
现在点击左上角的叉,可以关闭这个窗口了。

进程输入(续)

用户如果希望按 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++之游戏开发:创建环境_第4张图片

下一篇:Xcode与C++之游戏开发:2D图形

你可能感兴趣的:(游戏开发,SDL,Xcode,C++)