一个良好的跨平台游戏引擎必须实现平台解耦,通过实现平台抽象层,将游戏引擎与具体平台隔离。 平台独立层可以划分为以下几个方面:
- 平台检测
- 原子数据类型
- 时间管理
- 文件系统
- 窗口管理
- 网络
- 线程操作
- 设备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.h
和io.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
,即手动管理生命周期。