一个良好的跨平台游戏引擎必须实现平台解耦,通过实现平台抽象层,将游戏引擎与具体平台隔离。 平台独立层可以划分为以下几个方面:

  • 平台检测
  • 原子数据类型
  • 时间管理
  • 文件系统
  • 窗口管理
  • 网络
  • 线程操作
  • 设备IO
  • 实现细节

平台检测

个人认为平台检测主要是通过系统宏定义进行预处理

Win32

#if defined(WIN32)

// Win32 Platform
#include "Win32Window.h"
#include "Win32FileSystem.h"
#include "Win32Timer.h"

Linux

#if defined(__linux)

// Linux Platform

Mac OS X

#if defined(__MACOSX__)

// Mac OS X

项目构建工具也有区分平台的功能,如CMake本身也定义了一些宏用于表明当前要构建的平台。 从Qt的宏定义文件中,我们可以得到一些平台的系统宏定义。

#ifndef QGLOBAL_H
#define QGLOBAL_H

/*
   The operating system, must be one of: (Q_OS_x)

     MACX	- Mac OS X
     MAC9	- Mac OS 9
     MSDOS	- MS-DOS and Windows
     OS2	- OS/2
     OS2EMX	- XFree86 on OS/2 (not PM)
     WIN32	- Win32 (Windows 95/98/ME and Windows NT/2000/XP)
     CYGWIN	- Cygwin
     SOLARIS	- Sun Solaris
     HPUX	- HP-UX
     ULTRIX	- DEC Ultrix
     LINUX	- Linux
     FREEBSD	- FreeBSD
     NETBSD	- NetBSD
     OPENBSD	- OpenBSD
     BSDI	- BSD/OS
     IRIX	- SGI Irix
     OSF	- HP Tru64 UNIX
     SCO	- SCO OpenServer 5
     UNIXWARE	- UnixWare 7, Open UNIX 8
     AIX	- AIX
     HURD	- GNU Hurd
     DGUX	- DG/UX
     RELIANT	- Reliant UNIX
     DYNIX	- DYNIX/ptx
     QNX	- QNX
     QNX6	- QNX RTP 6.1
     LYNX	- LynxOS
     BSD4	- Any BSD 4.4 system
     UNIX	- Any UNIX BSD/SYSV system
*/

#if defined(__APPLE__) && defined(__GNUC__)
#  define Q_OS_MACX
#elif defined(__MACOSX__)
#  define Q_OS_MACX
#elif defined(macintosh)
#  define Q_OS_MAC9
#elif defined(__CYGWIN__)
#  define Q_OS_CYGWIN
#elif defined(MSDOS) || defined(_MSDOS)
#  define Q_OS_MSDOS
#elif defined(__OS2__)
#  if defined(__EMX__)
#    define Q_OS_OS2EMX
#  else
#    define Q_OS_OS2
#  endif
#elif !defined(SAG_COM) && (defined(WIN64) || defined(_WIN64) || defined(__WIN64__))
#  define Q_OS_WIN32
#  define Q_OS_WIN64
#elif !defined(SAG_COM) && (defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__))
#  define Q_OS_WIN32
#elif defined(__MWERKS__) && defined(__INTEL__)
#  define Q_OS_WIN32
#elif defined(__sun) || defined(sun)
#  define Q_OS_SOLARIS
#elif defined(hpux) || defined(__hpux)
#  define Q_OS_HPUX
#elif defined(__ultrix) || defined(ultrix)
#  define Q_OS_ULTRIX
#elif defined(sinix)
#  define Q_OS_RELIANT
#elif defined(__linux__) || defined(__linux)
#  define Q_OS_LINUX
#elif defined(__FreeBSD__)
#  define Q_OS_FREEBSD
#  define Q_OS_BSD4
#elif defined(__NetBSD__)
#  define Q_OS_NETBSD
#  define Q_OS_BSD4
#elif defined(__OpenBSD__)
#  define Q_OS_OPENBSD
#  define Q_OS_BSD4
#elif defined(__bsdi__)
#  define Q_OS_BSDI
#  define Q_OS_BSD4
#elif defined(__sgi)
#  define Q_OS_IRIX
#elif defined(__osf__)
#  define Q_OS_OSF
#elif defined(_AIX)
#  define Q_OS_AIX
#elif defined(__Lynx__)
#  define Q_OS_LYNX
#elif defined(__GNU_HURD__)
#  define Q_OS_HURD
#elif defined(__DGUX__)
#  define Q_OS_DGUX
#elif defined(__QNXNTO__)
#  define Q_OS_QNX6
#elif defined(__QNX__)
#  define Q_OS_QNX
#elif defined(_SEQUENT_)
#  define Q_OS_DYNIX
#elif defined(_SCO_DS)                   /* SCO OpenServer 5 + GCC */
#  define Q_OS_SCO
#elif defined(__USLC__)                  /* all SCO platforms + UDK or OUDK */
#  define Q_OS_UNIXWARE
#  define Q_OS_UNIXWARE7
#elif defined(__svr4__) && defined(i386) /* Open UNIX 8 + GCC */
#  define Q_OS_UNIXWARE
#  define Q_OS_UNIXWARE7
#else
#  error "Qt has not been ported to this OS - talk to qt-bugs@trolltech.com"
#endif

#if defined(Q_OS_MAC9) || defined(Q_OS_MACX)
#  define Q_OS_MAC
#endif

#if defined(Q_OS_MAC9) || defined(Q_OS_MSDOS) || defined(Q_OS_OS2) || defined(Q_OS_WIN32) || defined(Q_OS_WIN64)
#  undef Q_OS_UNIX
#elif !defined(Q_OS_UNIX)
#  define Q_OS_UNIX
#endif

原子数据类型

数据类型和平台、以及编译器相关,但是基本上不会涉及到平台相关的函数,对原子数据类型的确定,主要是通过对平台特定的数据类型使用typedef进行类型定义

typedef unsigned int uint32;	// 4B
typedef unsigned short uint16;	// 2B
typedef unsigned char uint8;	// 1B
typedef int int32;		// 4B
typedef short int16;		// 2B
typedef char int8;		// 1B

上面这些数据类型,针对大多数平台来说都是确定字节的。针对8个字节的整型,各个平台有些区别。

MSVC

typedef unsigned __int64 uint64;	// 8B
typedef __int64 int64;			// 8B

其它编译器

typedef unsigned long long uint64;	// 8B
typedef long long int64;		// 8B

与数据相关的问题还包括大小端: 大小端一般与处理器采用的架构相关。 Intel x86, MOS Technology 6502, Z80, VAX, PDP-11都是小端模式(Little Endian)。 Motorola 6800, Motorola 68000, PowerPC 970, System/370, SPARC(除V9外)为大端模式(Big Endian)。 ARM, PowerPC(除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC, IA64的字节序是可配置的。

高分辨率时钟

时钟作为一个游戏引擎最基本的模块,在很多方面都会使用到,如FPS统计,游戏时间线,物理系统等。 时钟模块的主要作用是获取当前时间。 C语言的time.h库提供了一些基本的时间获取函数,如下。

// 从 1970-01-01 00:00:00 GMT 以来消逝的秒数
time_t seconds = time(NULL);

// 获取时分秒结构
struct tm* Current = localtime(&seconds);

// 从程序启动到 clock() 调用,所消耗的CPU时间
clock_t ticks = clock();

// 转换成秒
long ElapsedSecond = ticks / CLOCKS_PER_SEC;

上面这些函数只能提供秒级精度,对于一些对时间要求不高程序,可以直接使用这些函数。 对游戏引擎而言,秒级精度是不够的,最少需要毫秒级精度。因此就需要使用到与平台相关的一些函数。

Win32

// 包含 windows.h
static LARGE_INTEGER m_StartTime;
static LONGLONG m_LastTime;
static DWORD m_StartTick;

void init()
{
	QueryPerformanceFrequency(&m_StartTime);
	m_StartTick = GetTickCount();
	m_LastTime = 0;
}
// 参考 OGRE getMilliseconds, 获取毫秒
unsigned long getMilliseconds()
{
	LARGE_INTEGER frequency;
	QueryPerformanceFrequency(&frequency);

	LARGE_INTEGER endTime;
	QueryPerformanceCounter(&endTime);

	LONGLONG TimeOffset = endTime.QuadPart - m_StartTime.QuadPart;

	// 毫秒:* 1000, 微秒:* 1000000
	unsigned long Ticks = (unsigned long)(1000 * TimeOffset / frequency.QuadPart);

	unsigned long check = GetTickCount() - m_StartTick;
	signed long msecOff = (signed long)(Ticks - check);
	if (msecOff < -100 || msecOff > 100)
	{
		LONGLONG adjust = (std::min)(msecOff * frequency.QuadPart / 1000, TimeOffset - m_LastTime);
		m_StartTime.QuadPart += adjust;
		TimeOffset -= adjust;

		Ticks = (unsigned long)(1000 * TimeOffset / frequency.QuadPart);
	}

	m_LastTime = TimeOffset;

	return Ticks;
}

Unix/Linux

// 包含 sys/time.h
static struct timeval m_StartTime;

void init()
{
	gettimeofday(&m_StartTime, NULL);
}

unsigned long getMilliseconds()
{
	struct timeval endTime;
	gettimeofday(&endTime, NULL);

	// timeval 由 tv_sec(秒), tv_usec(微秒) 共同组成
	unsigned long elapsedTime = (endTime.tv_sec - m_StartTime.tv_sec) * 1000;
	elapsedTime += (endTime.tv_usec - m_StartTime.tv_usec) / 1000;

	return elapsedTime;
}

文件系统

游戏引擎的一个重要的功能就是资源管理,而文件系统则是资源管理的基石。 文件系统的主要作用是管理文件、文件夹,必须实现文件的存取,目录的创建、删除、读取等。

文件

关于文件的存取,既可以使用C中的FILE相关操作函数,也可以使用C++中的文件流对象。 当然各个操作系统也都提供了对应的用于文件操作的API。

Win32

// #include<windows.h>
HANDLE WINAPI CreateFile(
  _In_      LPCTSTR lpFileName,
  _In_      DWORD dwDesiredAccess,
  _In_      DWORD dwShareMode,
  _In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  _In_      DWORD dwCreationDisposition,
  _In_      DWORD dwFlagsAndAttributes,
  _In_opt_  HANDLE hTemplateFile
);

Unix/Linux

//#include <fcntl.h>
int open(const char *path, int flags, mode_t mode);

目录(文件夹)

Linux和Windows中都有dirent.h这个头文件,但是里面定义的函数却是不相同的。 Linux中的dirent.h提供的函数能打开目录,关闭目录,遍历目录文件。 而Windows中的dirent.h只提供了创建目录、删除目录、进入目录,已经获取当前路径等功能,并没有提供遍历目录文件的功能。 Windows中遍历目录文件的函数在io.h中。

Win32

// dirent.h
int chdir(const char* path);
int mkdir(const char* path);
int rmdir(const char* path);
char* getcwd(char* buf, int buffsize);

// io.h
intptr_t _findfirst(const char *pattern, struct _finddata_t *data);
int _findnext(intptr_t id, struct _finddata_t *data);
int _findclose(intptr_t id);

fileapi.h文件中,定义了Windows关于文件和目录操作的API。

Unix/Linux

// dirent.h
DIR * opendir(const char *filename);
struct dirent * readdir(DIR *dirp);
int closedir(DIR *dirp);

OGRE的文件系统 在OGRE中,对于Win32平台,使用的是dirent.hio.h中提供的函数_findfirst、_findnext、_findclose。 对于Unix/Linux平台,则通过dirent.h中的opendir、readdir、closedir实现了上面3个函数。

窗口系统

应用程序对于窗口的操作,主要集中在创建、删除窗口,查询、设置属性等。除此之外,还要管理绘图上下文。 与窗口相关的还有窗口的事件处理,如何让使用者也能接收到事件也是必须要考虑的问题。

Win32

// windows.h

// 注册窗口类
WNDCLASS wndClass;
RegisterClass(&wndclass);

//创建窗口
HWND hwnd = CreateWindow("WndClassName", "WindowName", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

// 显示(隐藏)窗口
ShowWindow(hwnd, SW_SHOW); // ShowWindow(hwnd, SW_HIDE);

// 获取窗口尺寸
GetWindowRect(hwnd, &rect);

// 获取客户区域尺寸
GetClientRect(hwnd, &rect);

Linux 关于Linux上的窗口系统,可以参考《关于X11》这篇文章。

设备IO

对于大部分应用程序而言,其输出设备主要是显示器、音箱(或耳机),输入设备则种类较多:键盘、鼠标、游戏杆、游戏手柄、摄像头、麦克风等。一个典型的跨平台的人机接口函数库如OIS(Object Oriented Input System,面向对象输入系统)。

实现细节

在使用特定语言具体实现一个平台独立层,有很多细节部分需要考虑。下面将记录本人遇到的一些问题。

函数返回值

参考Win32的API设计,可以发现大部分函数都用HRESULT作为返回值类型,并且定义了若干个宏来表示函数运行状态。如S_OK代表运行正常,E_FAIL代表未知错误,E_OUTOFMEMORY代表内存不足,E_INVALIDARG代表非法参数。 在winerror.h中有HRESULT与相关错误代码的定义,HRESULT本质上是一个4字节值,所以我们在非Win32平台下,可以定义自己的HRESULT类型。

#if !defined(_WIN32) && !defined(PLATFORM_HRESULT_DEFINE) \
    && !defined(_HRESULT_DEFINED) && !defined(__midl)
#define PLATFORM_HRESULT_DEFINE

// 定义4字节整形
typedef long int32;

// 定义 HRESULT 类型
typedef long HRESULT;

/*
  还可以参照winerror.h定义一些工具宏,以及常用错误代码
  如  MAKE_HRESULT
      SUCCEEDED
      FAILED
*/

#endif

指针的处理

当一个封装好的库需要向使用者提供指针的时候,必须得考虑指针所指对象的生命周期的管理。 一个简单的办法是使用智能指针,在最新的C++11中,已包括了智能指针,其他也有很多库都提供了智能指针的实现,比如Boost。 另一个办法是定义一套使用规则,保证通过Create返回的指针,使用完之后,必须调用Release,即手动管理生命周期。