本帖最后由 weyl 于 2013-5-23 04:23 编辑
1. 编译环境的配置
SDL是一个游戏开发库,具体的东西就不多讲了。直接进入正题。
(1)Windows下的配置
在Windows下面配置是最简单的,下载Lazarus最新版,安装,然后将SDL的系列DLL文件放到你的工程目录下面,配置结束。
注意,实际上fpc的源码中已经包含了SDL以及插件的全部pas头文件,所以配置起来才会如此简单。如果使用Delphi,可以下载JEDI提供的一个安装文件来自动安装。
无论是使用fpc还是Delphi,都建议把JDEI提供的一个Object Pascal帮助文件作为最基本的参考资料。本文中对一些很基本的问题也并不会具体讨论,这些问题还是请参考这个帮助文件。
SDL(包含各种插件)的系列dll文件,可能需要到几个地方分别下载。
(2)Linux下的配置
事实上,SDL库本身的安装并不复杂,但是SDL的几个插件,例如ttf和Mixer,如果手动逐个编译的话相当麻烦。如果能够自动安装,应该尽量利用。
在Ubuntu里面,用apt-get命令,安装SDL的开发库。
在Redhat以及CentOS中,用yum命令安装。
(3)MacOSX下的配置
在MacOSX中,安装Lazarus需要按照编译器-源代码-IDE的顺序安装。同时必须安装XCode和其中的命令行工具。
关于SDL的开发库,建议使用Fink来安装,lazarus的Mac版已经将Fink的库文件安装位置(/sw)加进了编译器的参数中。
同时,如果使用了SDL的插件,在lpr文件的开头部分,应当有类似的预编译处理,否则可能会找不到库文件。
- {$LINKLIB SDL_ttf}
- {$LINKLIB SDL_image}
- {$linklib SDL_gfx}
复制代码 2. SDL的表面和像素的处理
在开始之前,先建立一个SDL的基本程序。
在工程里面,创建一个lazarus的基本程序,修改一下代码,成为这个样子:
- program sdltest;
- {$mode Delphi}{$H+}
- uses
- {$IFDEF UNIX}{$IFDEF UseCThreads}
- cthreads,
- {$ENDIF}{$ENDIF}
- Classes
- { you can add units after this },
- SDL;
- var
- scr: PSDL_Surface;
- begin
- SDL_Init(SDL_INIT_VIDEO);
- scr := SDL_SetVideoMode(640, 480, 32, 0);
- SDL_Delay(1000);
- SDL_Quit();
- end.
复制代码 编译选项原本是objfpc,这里改成了delphi,这是为了让语法与Delphi一致,否则调用PSDL_Surface的相关参量的时候,需要用到^运算符(例如屏幕的宽度在两种模式下写成scr.w或者scr^.w),而JEDI给出的例程中并没有这样做。当然,如果你钟爱objfpc的模式,也可以从一开始就使用这个运算符。
在SDL中,基本的绘图单位是表面(Surface)。在SDL游戏的大部分显示过程中,就是计算表面应该放的位置,以及将一大堆的表面互相贴来贴去。
需要注意,其中有一个表面是特殊的,也就是实际显示的屏幕所对应的表面,如果没有这个表面,大部分游戏可能是没法玩。而显示部分最后应该做的就是将所有的东西正确地画到这个表面上去。比如我用另一个叫做tempscr的PSDL_Surface来记录一张主角的图片,那么我经常做的就是用SDL_BlitSurface这个函数来合并scr和tempscr。
这个特殊表面由SDL_SetVideoMode来创建,该函数包含4个参数,即屏幕的长,宽,色深和显示标识。这里我只列举几个重要的标识:
SDL_SWSURFACE 在内存中创建
SDL_HWSURFACE 在显存中创建
SDL_DOUBLEBUF 使用双缓冲
SDL_FULLSCREEN 全屏
SDL_OPENGL 创建OpenGL的窗口
SDL_RESIZABLE 创建的窗口可以被改变大小
这里面全屏和可改变大小是必须注意的。如果改变了窗口大小,那么必须重新调用SDL_SetVideoMode来适应新的窗口,否则就只是窗口变了,但是画布还没变。
子程片段如下:
- procedure ResizeWindow(w, h: integer);
- begin
- Screen := SDL_SetVideoMode(w, h, 32, ScreenFlag);
- event.type_ := 0;
- SDL_UpdateRect(Screen, 0, 0, screen.w, screen.h);
- end;
- procedure SwitchFullscreen;
- begin
- fullscreen := 1 - fullscreen;
- if fullscreen = 0 then
- begin
- Screen := SDL_SetVideoMode(RESOLUTIONX, RESOLUTIONY, 32, ScreenFlag);
- end
- else
- begin
- Screen := SDL_SetVideoMode(Center_X * 2, Center_Y * 2, 32, ScreenFlag or SDL_FULLSCREEN);
- end;
- end;
复制代码 而全屏模式下需要注意的是,颜色的格式常常是不同的。在Mac系统中,如果使用位运算计算色值的话,在全屏和窗口下经常会得到不同的结果。同时,在SDL的某些版本中,对像素的直接操作在全屏模式下可能会出现偏差。这可能是SDL中的bug,也可能是因为其他的情况。
一个表面包含的像素数目是长乘以宽,在JEDI-SDL中附带的帮助文件中,给出了读和写像素的子程,截取片段如下:
- bpp := screen_.format.BytesPerPixel;
- X := screen_.w div 2;
- Y := screen_.h div 2;
- bits := ( PUint8( screen_.pixels ) ) + Y * screen_.pitch + X * bpp;
- // Set the pixel
- case bpp of
- 1:
- begin
- PUint8( bits )^ := Uint8( pixel );
- end;
- 2:
- begin
- PUint16(bits)^ := Uint16( pixel );
- end;
- 3:
- begin // Format/endian independent
- r := ( pixel shr screen_.format.Rshift ) and $FF;
- g := ( pixel shr screen_.format.Gshift ) and $FF;
- b := ( pixel shr screen_.format.Bshift ) and $FF;
- bits^ + (screen_.format.Rshift div 8) := r;
- bits^ + (screen_.format.Gshift div 8) := g;
- bits^ + (screen_.format.Bshift div 8) := b;
- end;
- 4:
- begin
- PUInt32(bits)^ := Uint32( pixel );
- end;
- end;
复制代码 因此我们大概能知道,SDL进行像素级别的操作,是比较麻烦的。所以一般情况下,尽量不要这样做。
此外,当不得不这样做时,应当注意使用变量的数据类型。单独的r,g,b,a四个值,应当尽量使用Byte类型的变量。我曾经试过用Uint32类型来表示,发现编译出的程序效率远远低于Delphi的结果。这应当是归于fpc并未针对位操作进行优化的原因。后来我改用Byte类型来表示它们,得到的程序效率就追得上Delphi了。
在网络上的讨论中,一般认为除了上面提到的特殊表面之外,其他的表面均应该手动将其释放。虽然一般来说系统会释放掉程序占用的全部内存,但是为防万一,手动释放还是必要的。
3. 中文的显示
在网上查找SDL显示文字的教程,可以找到很多,而显示中文的教程就少得多了。
特别地,fpc与Delphi的字串是不同的,字串间的赋值也是不同的,这导致显示中文的问题变得更加复杂。
显示中文时,一般需要SDL_ttf这个库,这个库里面包含了两种显示方式,即Unicode和UTF8。那么我们可以利用两个途径来显示中文。
在fpc中,字串的默认编码就是UTF8,显然,选择UTF8方式是较合适的。
在Delphi中,字串的编码是Ansi,但是widestring的编码是Unicode,因此用Unicode方式更好。
无论哪种方式,在实际操作中,都会有微妙的变化。在这里,我们必须做出一项约定,就是源码文件的编码必须是UTF8,而且最好没有BOM!否则fpc很可能会给你意外的结果!
根据上面的讨论,我们在fpc中写两个函数,用来试验:
- procedure drawutf8text(str: string; x, y: integer);
- var
- text: PSdl_Surface;
- font: PTTF_Font;
- dest: TSDL_Rect;
- tempcolor: TSDL_Color;
- begin
- tempcolor.r := $FF;
- tempcolor.g := $FF;
- tempcolor.b := $FF;
- font := TTF_OpenFont('MSJHBD.TTC', 20);
- text := TTF_RenderUTF8_blended(font, pchar(str), tempcolor);
- dest.x := x;
- dest.y := y;
- SDL_BlitSurface(text, nil, scr, @dest);
- SDL_FreeSurface(text);
- TTF_CloseFont(font);
- end;
- procedure drawunicodetext(str: widestring; x, y: integer);
- var
- text: PSdl_Surface;
- font: PTTF_Font;
- dest: TSDL_Rect;
- tempcolor: TSDL_Color;
- begin
- tempcolor.r := $FF;
- tempcolor.g := $FF;
- tempcolor.b := $FF;
- font := TTF_OpenFont('MSJHBD.TTC', 20);
- text := TTF_RenderUnicode_blended(font, puint16(str), tempcolor);
- dest.x := x;
- dest.y := y;
- SDL_BlitSurface(text, nil, scr, @dest);
- SDL_FreeSurface(text);
- TTF_CloseFont(font);
- end;
复制代码 这两个函数的区别只是用UTF8和Unicode编码的不同,当然,对应的指针也略有区别,请自行查看。另外,我们也可以发现ttf的渲染颜色是通过TSDL_Color来指定的。这里我们为了简单起见,颜色的3个分量均指定为$FF,也就是白色。
这里的字体文件是“微软正黑粗体”,需要先将这个字体文件拷贝到程序目录里面。
在主函数中,这样使用:
- drawutf8text('中文', 0, 0);
- drawunicodetext(UTF8decode('文中'), 0, 30);
复制代码 结果是这样的,看起来非常圆满。当然,用这两个编码,显示简体或繁体是都没有问题的。
下面,你可以自己试验将源码的编码变化,并试着加上AnsiToUTF8这个函数来对字符串进行处理,来看看输出结果。可见,fpc对于字串的编码,是与源码的编码相关的。但是delphi的字串则并不是这样,因为delphi在字串赋值时包含了隐式转换。
不过事情到这里还没有结束,我们试一试另外一个字体“标楷体”,这个字体在轩辕剑5的两个外传中曾被使用过,文件名为kaiu.ttf,非繁体系统中识别为DF-Kai。
这个结果就有点匪夷所思了,为什么x方向的坐标会不同?
实际上,在我之前试着编写一个游戏时,最开始用的就是标楷体,否则可能我并不会注意到这个问题。经过我用不同的字符试验,得到的结论是:不同的字体,甚至同一字体中的不同字符,有可能会存在一个初始的x偏移,这个偏移值是怎么来的我也不清楚,自然我是没办法估计的。
但是后来我想到了一个绕过它的办法,也就是在所有字串前面加上一个空格,并且调整x方向的偏移,这样字体的输出就会比较圆满了。
- drawutf8text(' 文中', -20, 0);
- drawunicodetext(UTF8decode(' 中文'), -20, 30);
复制代码
当然像我这么处理,在正式编写时就不合适了,在输出字串函数的内部处理会更好。
问题到这里还并不算圆满解决。我们知道,中文字体的英文和数字部分往往是不怎么好看的。因此,也许我们有必要引入一个英文字体,那样的话,也许我们需要对字串中每一个字符分别输出。
这里我就不再写出具体的函数了,大家可以自己试试。
特别地,因为fpc与delphi对字串的处理不同,因此如果代码想在delphi中也能使用的话,这里的条件编译处理可能会非常麻烦。
4. 事件的响应
基本上,在SDL的说明中,事件统一使用while循环加上SDL_PollEvent或者SDL_WaitEvent来控制,实际上如果不需要死循环的话,用for、if甚至不作任何判断都是可行的。有几点需要注意:
首先,就是退出的常数是SDL_QUITEV,而并非SDL_QUIT,这是因为pascal是一种不区分大小写的语言,而SDL_Quit另有作用,因此JEDI将这个常数改了名字。
其次,如果所需要的循环不止一个,所有基本事件的处理(例如右上角的关闭按钮,窗口尺寸变化,全屏切换等)都需要重写一遍。因此最好将这些基本处理先写在一起成为一个子程。
如果需要获得键盘(或者其他设备)的状态,例如判断是否有方向键被按下等,应该使用如下(或者类似的)代码:
- keystate: PChar;
- keystate := PChar(SDL_GetKeyState(nil));
复制代码 说明书中认为这个函数调用后没有必要释放PChar,可以认为实际上该指针一直指向的是内存的同一位置。
SDL并没有鼠标双击这个设定,说明中建议利用两次按键之间的时间间隔来处理,但是我尝试过觉得效果不是很好,因此不推荐。
此外,SDL对于修饰键(Alt,Shift,Ctrl)的响应好像也不是特别完善,所以建议使用别的方式来代替。
5. 声音
在SDL的插件中,SDL_Mixer是一个应用很广泛的库,而且用法也不难。在游戏中使用它无非是随着游戏的进行,在适当的时候播放适当的音乐和音效。
音乐和音效基本上是MIDI,WAV,OGG,MP3这几种主流格式。
但是我之所以会写这一部分,是建议你不要使用这个库。经过我的试用之后,发现这个库至今为止仍然在切换,载入的时候,有使整个程序崩溃的可能。在个人开发中,BASS声音库比Mixer稳定得多,不过这个库是闭源,非商业应用免费,而商业应用需要收费。FMOD的功能略强一些,也是非商业应用免费。Mixer虽然是完全免费的,但是因为其稳定性存在问题,所以在免费软件开发中,并不推荐这个库。但是在完全免费的声音库中,Mixer还算是首选(或者谁知道其他的?)。
这里我推荐使用的是BASS这个声音库。这个库的体积很小,而且提供各种平台,还有各种语言的头文件,当然少不了pascal。BASS的基本库可以播放OGG,MP3,WAV等,如果需要midi音效,可以用插件bassmidi。但是同时,bassmidi要一个sf2格式的音色库才能出声音,操作系统里面通常会自带一个音色库,但是并不是sf2格式的,实际上其他不需要音色库即可播放midi的声音库使用的都是调用了这个音色库,它的电子味很浓。
BASS提供的pascal头文件不需要任何修改即可在fpc中使用。但是在Mac系统中,预编译符号应该是darwin而不是macosx。
6. 图片资源
(1)载入图片为表面
SDL_image插件可以将多种格式的图片载入为表面。同样如果这个图片的内容被读进了内存中,也可以从内存中加载。
具体的代码如下:
- function LoadSurfaceFromFile(filename: string): PSDL_Surface;
- var
- tempscr: PSDL_Surface;
- begin
- Result := nil;
- if fileexistsUTF8(filename) then
- begin
- tempscr := IMG_Load(PChar(filename));
- Result := SDL_DisplayFormatAlpha(tempscr);
- SDL_FreeSurface(tempscr);
- end;
- end;
- function LoadSurfaceFromMem(p: pchar; len: integer): PSDL_Surface;
- var
- tempscr: PSDL_Surface;
- tempRWops: PSDL_RWops;
- begin
- Result := nil;
- tempRWops := SDL_RWFromMem(p, len);
- tempscr := IMG_LoadPNG_RW(tempRWops);
- Result := SDL_DisplayFormatAlpha(tempscr);
- SDL_FreeSurface(tempscr);
- SDL_FreeRW(tempRWops);
- end;
复制代码 以上两个例子,从文件中载入是依据扩展名判断的,这是IMG_Load这个函数的一部分,如果确定了图片格式,就可以改成这个格式专用的函数。后面从内存中载入的,则是以PNG格式为例。
SDL_DisplayFormatAlpha这个函数的目的是将载入的格式转为最适合快速贴图的格式。如果图片本身没经过针对性处理,那么这个转换函数可以帮你处理,处理之后的表面的贴图速度可以加快很多。
同样如果不需要Alpha通道,那么用SDL_DisplayFormat来处理刚刚载入的表面。
(2)混合色和半透明
这里我只讨论两种表面,即32位色的RGBA和RGB两种。
首先,两种表面的Alpha值含义一样,0是全透明,255是不透明,以此类推。
这两种表面中,每个像素均用32位(4字节)的无符号整数表示。其中当然有3个字节记录了RGB的值,顺序是与操作系统有关的。
在RGB表面中,透明数据是用ColorKey和Alpha两个值处理的。凡是某个像素的值与ColorKey相同,则视为这个像素是透明的,或者更严格的说法是被忽略的。如果Alpha的值不为255,那么整个表面上不被忽略的像素,均按照这个值进行半透明处理。
在RGBA表面中,一般来说表面自己的Alpha值是无用的,同时每个像素都有自己的Alpha值,所以有的表面每个点都可以是不同的透明度。而ColorKey仍然是标记被忽略像素的。
标准的PNG格式载入到表面上的时候,如果不作任何处理,使用的是RGBA格式。对这两种格式来说,进行混合色和半透明处理,应当使用不同的方法。
a 半透明
RGB格式的半透明,只需设好ColorKey和Alpha两个值就可以了。RGBA格式的半透明因为每个像素的值不同,如果遍历修改的话,速度会比较慢,直接转的话,可能原本透明的像素的RGB值是一些未处理的混乱数据。整体处理的方法是将所需的主屏提取出一部分作为背景,然后将源图画到背景上。这时需要贴上去的图变成了图片+主屏的一部分,再设置这个贴图的透明值,贴到主屏上就可以得到更透明的效果了。
b 混合色
如果源图是RGB,那么只需设置源的Alpha,将其画到一个单色的RGB表面上,设置结果的ColorKey即可。如果源图是RGBA,那么首先设置一个单色的RGBA表面,填充为需混合的颜色,但是颜色值的A位设置为混合用的Alpha值,再将这个单色RGBA表面画到源图上去,将结果转为RGB,再设置透明色。
上面的方法避免了单独遍历全部像素,效率应该会高一点。应该还会有更好的方法。
(3)放缩
表面的放缩,可以用SDL_gfx这个插件库来处理。特别地,JEDI制作的针对Delphi的安装文件中,并没有把这个库加到编译器的路径中,需要自己加进去。fpc已经加进去了。
gfx的放缩主要依赖rotozoomSurfaceXY这个函数,如果使用平滑的时候,效率比较低。
同时,gfx还有一些其他的功能,例如绘制一些几何图形,重设置RGBA的透明度等(查看源码好像是遍历像素来实现的),需要的时候可以利用。
SPriG是另一个完成类似功能的扩展库,但是只有c语言版本,必要的时候可以直接调用dll文件。所用算法的速度比gfx要快,但是不太准确。
好像翻转和扭曲都没有对应的库。
7. OpenGL
为什么要用到OpenGL呢?其实这个问题我也不太明白,因为我只做过一个2D游戏,虽然这个游戏还是比较复杂的,但是绝对用不到任何3D的东西。
因为不断有人跟我说,或者通过别人跟我说画面太小(其实也有少数人嫌画面大的……),所以我考虑过画面的拉伸。我所用的解决办法就是利用gfx这个库,将最终画好的表面拉伸一下,再输出到真实的屏幕上去。这时如果不使用平滑,当拉伸的比例不是一些特殊的数字的时候,整个画面就像一块破布,非常难看。如果使用了平滑,游戏的帧数下降有点让人不能容忍。
简单地说,用CPU来计算平滑效果是一个错误的选择,所以更好的方法自然是利用显卡来进行平滑拉伸,OpenGL的纹理贴图正是一个合适的方案。
另外SDL_Image(我想ttf也可以吧)与OpenGL配合来载入一些图片作为纹理,也是很常见的情况。
sdl的表面转为OpenGL的纹理,可以使用以下的代码(uses中需要添加gl和glext):
- TextureID: GLUint;
- ..............
- glGenTextures(1, @TextureID);
- glBindTexture(GL_TEXTURE_2D, TextureID);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, scr.w, scr.h, 0, GL_BGRA, GL_UNSIGNED_BYTE, scr.pixels);
-
-
- if SMOOTH = 1 then
- begin
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- end
- else
- begin
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- end;
-
-
复制代码 这里针对是否使用平滑的情况设置了放大和缩小时使用的方式,LINEAR是平滑,NEAREST是不平滑。如果把这个纹理贴到整个屏幕上,代码是这样的:
- glENABLE(GL_TEXTURE_2D);
- glBEGIN(GL_QUADS);
- glTexCoord2f(0.0, 0.0);
- glVertex3f(-1.0, 1.0, 0.0);
- glTexCoord2f(1.0, 0.0);
- glVertex3f(1.0, 1.0, 0.0);
- glTexCoord2f(1.0, 1.0);
- glVertex3f(1.0, -1.0, 0.0);
- glTexCoord2f(0.0, 1.0);
- glVertex3f(-1.0, -1.0, 0.0);
- glEND;
- glDISABLE(GL_TEXTURE_2D);
- SDL_GL_SwapBuffers;
- glDeleteTextures(1, @TextureID);
-
复制代码 最后一句视情况是否删除这个纹理。因为OpenGL是一个3D引擎,所以4个顶点是可以任意设置的,这就可以非常简单地实现任何的扭曲,旋转,拉伸效果,而且因为利用的是显卡来计算,基本上不会拖慢游戏速度。
8. 多线程
有两种情况应该需要用到多线程,第一就是有多个元素需要处理,这种情况比较简单,只要注意几个元素的相互作用就可以了。
比较麻烦的是另一种情况:有时候一些计算量很大的东西不得不算,但是放到主线程会导致游戏卡得很厉害,这时创建一个副线程可能是一个办法。
在副线程中,有几件事是不能做的:
不能进行屏幕的刷新,否则会出现无法理解的情况!
不能使用可变长数据结构作为变量,即使函数体是空的,这会导致进程非常不稳定!
附录
SDL_UpdateRect函数,在darwin(即macos和ios)系统中,无论范围如何指定,均刷新全部屏幕范围。针对于此,可以手动进行双缓冲。
如果循环出现卡进程的现象,可以在循环内部用SDL_PollEvent来接收事件,即可避免卡死。 |