游戏编程文章搜集资料

更新时间:2024-06-07 17:20:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

游戏编程文章搜集资料

游戏开发—图形图像篇

游戏开发--开篇

记得我第一次玩的PC game 是KKND(绝地风暴),当时的游戏平台是DOS,我只是觉得很好玩,经常和几个小学同学一起厮杀到12点。可是现在回忆起来,KKND无论是从智能设计还是在游戏画面与操作上都是非常出色的。他的音效同样是很棒的,而且全中文配音让当时很多不懂英语的玩家找到了许多的亲切感。这真是一款很棒的游戏。It is great!

我想,这样的感觉是许多游戏开发者或者梦想成为游戏开发者的人们所共同经历的感受。一款好游戏会带给人们很多东西,会带给社会很多东西。 在人的层面上,无论是\大富翁\让我们娱乐,还是\文明\让我们感受科技发展带来的成果与思考,在我们玩一个游戏的时候,我们读到的故事,经历的坎坷,解决的问题,无时无刻不在让我们感受着游戏之外的很多东西。 在社会的层面上,游戏产业就本身而言,市场不断扩大,需求不断增多;更重要的是,他带动了出版、通信等相关产业的迅猛增长。游戏与相关产业增长的比值有时甚至是1:100。现在看看韩国政府对于游戏产业的支持,就有\醉翁之意不在酒\的感觉了。

中国的游戏产业将在2005年左右达到10美元的市场,我们国人绝不应放弃这个发展的大好机会。其实,我们可以看到现在有很多的游戏公司,看起来像是一片欣欣向荣、蓬勃发展的场面,但是这里面又有多少是搞游戏开发的呢?是少之又少。很多公司都是代理国外的游戏,这样一来,国外很多的游戏开发厂商获得了大量的利润,而我们在为小蛋糕争得你死我活时,别人已经各自享用着自己的大餐了。资金不足无疑是各个公司搞游戏代理的最主要理由。但是我们可以看到,像CS这样成功的Garage Game,我们为什么做不出来?答案很多,问题很多,我们要做的事情很多。 于是,本着从我做起的原则,还是应该不断以提高自身的能力为主。毕竟我们和外国走的路不同,我们要找到适合自己的发展道路,走出一条具有中国特色的产业发展路线。当然,在路上我们还要不断的学习。

所以,在我学习游戏开发的过程中,看到了我们中国学生的很多文章,有很多都是很有水平的。于是自己也提起笔来,把自己学习的经验和心得写下来,和大家一起讨论和提高。 这一系列的文章主要是面向想学习游戏开发的朋友,文章是从实际开发源代码的角度去介绍游戏开发的各个方面(文章中的示例代码用C++编写),但是瓦文章并不是完全地教学,更像是一个游戏开发的学习索引。它告诉初学者,做什么游戏需要什么技术,指引他们去学习某项技术,从而真正回归到自己学习的乐趣中来,为最终游戏的设计和实现奠定基础。

游戏开发--图形图像篇(一)

一个游戏的好坏谁说了算? 玩家。

玩家如何看一个游戏?

用眼睛。

那么这款游戏怎么样? \画面真棒!\ … …

你的游戏做得再好,如果没有一个很好的图形效果,那展现在玩家眼前的东西将大打折扣。而且,你的游戏也不会得到很好的支持。为什么呢?因为你的游戏无需使用很好的显示设备和图形加速设备,硬件厂商不会理睬你的游戏。这也就和开篇所说的\游戏促进相关产业的发展\的职责相悖了。所以,在写一个游戏之前,想一想开发这个游戏需要什么技能,\磨刀不误砍柴工\。让我们开始吧,先学一学游戏的图形开发。

一. Windows 绘图接口

1.GDI/GDI+

在开始前先让我们看一个具体的例子。

这个小程序的图形显示部分就是用GDI编写的。

我们在编写Windows应用程序的时候,经常会用到GDI/GDI+(Graghic Deice Interface,图形设备接口)。GDI是Windows早期版本中包括的图形设备接口,而GDI+是GDI的改进版本,对GDI的现存性能进行了优化,而且增加了很多新性能。

GDI/GDI+的结构如下图:

GDI+的C++接口中包括大约40个类、50个枚举和6个结构。听起来虽然不多,但是这也足够胜任大多数应用了。

GDI编写程序的优点是他基本上兼容所有的显示设备,但是这是以损失效率为代价的。通常,编写应用程序时,大多数桌面应用程序用到的只是相对稳定的图形图像显示,所以在这种情况下,使用GDI是很实用的。

可是,在开发对图形图像显示速度要求非常高的游戏程序时,GDI就显得不够强劲了。所以,我们把开发游戏的图形图像显示部分的重担交给了下面就要介绍的DirectX。

GDI学习参考:《Windows Program With MFC》中的GDI部分。

2.DirectX

谈到DirectX或许你不会陌生,因为像很多大名鼎鼎的FPS游戏都是用它来开发的。这其中有Valve小组的Half Life及其衍生作品--CS。所以,DirectX已经成为了游戏开发所不可或缺的重要组成部分。

还是先让我们看一个小程序。

[program MovePicture v0.3]

DirectDraw 与 DirectInput 的游戏编程体验

我想关于这个主题的文章,不算少,但也不算太多。但大多是分别介绍

DirectDraw 与 DirectInput,而并没有将其结合起来,也许你会问:“分开与合并起来并没有本质区别啊!”。其实的确没有本质区别,但那样使那些最初对游戏编程报有极大热情的爱好者感到非常失望,因为这其中的一个并不能完全满足他们的要求,并且使其感到巨大的阻力,从而失去信心。所以本文将

DirectDraw 与 DirectInput结合起来去讲一个主题就是“游戏编程”,请注意是“游戏编程”,当然这只是一个简单的桌面游戏,但这已经与先前有很大的不同了,这已不是简单的 DirectDraw或 DirectInput编程。我想你现在应该能够体会出其中的区别了。

声明:在这之前需要你具有一定的 WIN32 API 函数的知识,并且可以熟练使用。和 DirectDraw的知识,关于DirectDraw可以参见 www.frontfree.net 中的 <<动画程序编写——DirectDraw之旅>> 1-3),或其它文章。最后是 c++ 语言,当然也要包括面向对象的那部分。在 Visual C++ .NET 编译环境下进行开发的。

首先 ,我们还是先简要复习一下DirectDraw的概念吧!

DirectDraw本质上是显存管理程序。它最重要的性能是允许程序员直接在显存里存储和操纵位图。它使你能够利用视频硬件bliter(位块传输器)在显存内部进行位图的blit(位块传输)。用视频硬件的blitter从显存向显存进行blit比从内存向显存更快。这在64位显卡向显存提供64位数据路径的今天显得尤其重要,硬件独立于促CPU进行位块传输操作,使得CPU得以继续工作。另外DirectDraw支持显卡的其他硬件加速特性,例如对精灵和z -buffering的硬件支持。

DirectDraw的工作原理

我们这里还是用图表方式展现给大家吧!

细心的朋友可以很明显地注意到图示中的右上角的图解中说明,表面对象有两个宽度,一个是WIDTH,一个是PITCH。WIDTH就是创建表面时所给出的那个宽度,而PITCH是表面的实际宽度,是按字节算的。在许多显卡上,PITCH和WIDTH是相等的,比如在640x480的高彩模式下,PITCH为1280。而在某些显卡上,PITCH比WIDTH要大。比如在640x480的256色模式下,当WIDTH是640时,PITCH为1024而不是640,这些显卡这样做是为了更好地进行数据对齐来提高性能或达到其它目的。所以,我们在实际编程时,为了保证程序的兼容性,必须按PITCH处理。 但这些硬件的底层问题,我们不用太关心,只要稍有了解就可以了。 下面我们再简要叙述一下,如何使用 DirectX 9.0 中提供的 DirectDraw 类库来创建对象并使用操作对象。

宏定义在先,定义删除指针和释放对象的宏

#define SAFE_DELETE(p) { if(p) { delete (p); (p)=NULL; } } #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } } 先创建一个 CDisplay 的全局对象 CDisplay就是ddutil.h中定义的类,用于处理表面之间的拷贝翻页等操作的类,再次定义一个全局变量,用于以后对指向的表面之间进行操作

CDisplay* g_pDisplay = NULL;

然后创建表面,当然可以创建很多的表面,这些表面都是离屏表面,在更新画面时,都可以用 CDisplay 类的对象中的方法,将其拷贝到后备缓冲区表面上。只要创建离屏表面,就要用到 CSurface 类。CSurface也是ddutil.h头文件中定义的类,用于对表面本身进行操作,如设置色彩键码,在此定义的图画指针。

CSurface* g_pBackSurface = NULL;

DirectX 中就一共用这两个类封装了 DirectDraw 对象的大部分操作,如果你觉得这还不能满足要求,那么你也可以在程序中用 DirectDraw API 函数编写程序,不过在本文中不再介绍。

这之后,我们会用到 InitDirectDraw 函数。这个函数是我们自己创建的。在此函数中作所有的 DirectDraw 的对象初始化工作。

HRESULT InitDirectDraw( HWND hWnd ) {

HRESULT hr; //接受返回值,其实是long型变量

LPDIRECTDRAWPALETTE pDDPal = NULL; //定义程序中的调色板 int iSprite; //定义与sprite个数有关的计数器

g_pDisplay = new CDisplay(); //动态开辟一个CDisplay类

if( FAILED( hr = g_pDisplay->CreateFullScreenDisplay( hWnd, SCREEN_WIDTH,

SCREEN_HEIGHT, SCREEN_BPP ) ) ) /*设置程序为全屏,并且 g_pDisplay 就是动态开辟一个CDisplay类的指针,而在这个类的域中,有一个DirectDraw主表面指针,和一个后备缓冲区表面的指针。在从我建议你可以先去阅读一下 ddutil.h 和 ddutil.cpp 文件。*/ {

MessageBox( hWnd, TEXT(\ TEXT(\ return hr; }

if( FAILED( hr = g_pDisplay->CreatePaletteFromBitmap( &pDDPal, MAKEINTRESOURCE( IDB_DIRECTX ) ) ) ) //顾名思义,就是从bmp图片中获得调色板值,并赋值在pDDPal结构指针所指向的结构体中。 return hr;

if( FAILED( hr = g_pDisplay->SetPalette( pDDPal ) ) ) //用刚才从IDB_DIRECTX中获得的调色板制来设置程序调色板

return hr;

SAFE_RELEASE( pDDPal );//释放指针,在用过后,一定要释放,这是良好的编程习惯 // 用IDB_WINXP图片创建一个表面,并用g_pBackSurface指向这个表面 if( FAILED(

hr

=

g_pDisplay->CreateSurfaceFromBitmap(

&g_pBackSurface,

MAKEINTRESOURCE( IDB_WINXP ),

SCREEN_WIDTH, SCREEN_HEIGHT ) ) )

return hr;//设置色彩键码为黑色,0代表黑色,这样在表面的拷贝过程中黑色像素的点将不会被拷贝,这样可以产生镂空效果。当然你可以任意设置关键颜色,而颜色的表示法可以用 RGB 宏定义。例如 红色:RGB( 255,0,0 ), 黑色 RGB( 255,255,255 ) if( FAILED( hr = g_pBackSurface->SetColorKey( RGB( 255,255,255 ) ) ) ) return hr; return S_OK; }

下面的函数是用于更新画面的。

HRESULT DisplayFrame() {

HRESULT hr;

g_pDisplay->Clear( 0 ); //清空后备缓冲区表面

//将g_pBackSurface所指向的图片拷贝到后备缓冲区表面

g_pDisplay->Blt( 0, 0, g_pBackSurface, NULL );//最关键的地方在这里,请看下面的语句,只要我们一执行翻页操作,就可以将改动了的图像了显示在屏幕上了 if( FAILED( hr = g_pDisplay->Present() /*翻页操作*/) ) return hr; return S_OK;

}

下面的函数是用于在程序失去焦点时调用的。

HRESULT RestoreSurfaces() {

HRESULT hr;

LPDIRECTDRAWPALETTE pDDPal = NULL; /*当程序失去焦点,要保存当前的画面,请注意这里,g_pDisplay->GetDirectDraw()函数返回的才是真正的 DirectDraw 对象 */ if( FAILED( hr = g_pDisplay->GetDirectDraw()->RestoreAllSurfaces() ) ) return hr;//在此我们还要重新创建调色板

if( FAILED( hr = g_pDisplay->CreatePaletteFromBitmap( MAKEINTRESOURCE( IDB_DIRECTX ) ) ) ) return hr;//重新设置调色板

if( FAILED( hr = g_pDisplay->SetPalette( pDDPal ) ) ) return hr;

SAFE_RELEASE( pDDPal );//重新画出图画

if( FAILED( hr = g_pLogoSurface->DrawBitmap( MAKEINTRESOURCE( IDB_WINXP ), SPRITE_DIAMETER, SPRITE_DIAMETER ) ) ) return hr; return S_OK; }

&pDDPal,

下面这个函数是释放表面指针所用的。

VOID FreeDirectDraw() {

SAFE_DELETE( g_pBackSurface ); SAFE_DELETE( g_pDisplay ); }

我们的回顾到此结束,下面我们开始本文要介绍的一个关键技术,DirectInput 的使用。

游戏编程可不仅仅是图形程序的开发工作,实际上包含了许多方面,本文所要讲述的就是关于如何使用 DirectInput 来对键盘编程的问题。

而我们为什么要选择用 DirectInput 来处理游戏中的输入问题呢?其实用 Win32 API 函数也完全可以处理这些工作,例如其中,有一个

GetAsyncKeyState() 的函数可以返回一个指定键的当前状态是按下还是松开。这个函数还能返回该指定键在上次调用 GetAsyncKeyState() 函数以后,是否被按下过。虽然这个函数听上去很不错,但需要我们自己轮换查询每个键盘的状态。而在 DirectInput 中我们已经可以脱离这些烦琐的工作,只因它的功能更强大。

由于本文重点在二者的结合,故在此只介绍 DirectInput 中比较简单的,而且最容易上手的立即模式的工作方式。

而这里我们要用到 DirectInput 的 API 函数。有人会问,为什么在 DirectDraw 中用 DirectX 提供的类库编程,而对于 DirectInput 却直接使用要用其 API 函数呢,是因为没有提供 DirectInput 的类库吗?不是!而是因为使用类库并不很方便而且不灵活。

OK,让我们开始我们游戏编程的第二部——DirectInput编程。

前面讲 DirectDraw 时,并没有提到,微软是按 COM 来设计DirectX的,所以就有了一个 DIRECTINPUT 对象来表示输入设备,而某个具体的设备由

DIRECTINPUTDEVICE 对象来表示。也许会感到很无奈,怎么游戏编程需要这么多的知识啊,其实您也无需烦恼,只要知道一下就可以了,其实这并不;影响您的设计,而且就算您不知道,也同样可以驾驭DIRECTINPUT。

实际的建立过程是先创建一个 DIRECTINPUT 对象,然后在通过此对象的 CreateDevice 方法来创建 DIRECTINPUTDEVICE 对象。

#include

#define DINPUT_BUFFERSIZE 16

LPDIRECTINPUT lpDirectInput; // DirectInput 对象实际上是一个com对象 LPDIRECTINPUTDEVICE lpKeyboard; // DirectInput 设备 BOOL InitDInput(HWND hWnd)

{

HRESULT hr;// 创建一个 DIRECTINPUT 对象

if( FAILED( hr = DirectInputCreate(hInstanceCopy, DIRECTINPUT_VERSION, &lpDirectInput, NULL))) {// 失败提示或处理 return hr;

}// 创建一个 DIRECTINPUTDEVICE 界面

//参数 GUID_SysKeyboard 指明了建立的是键盘对象

if( FAILED( hr = lpDirectInput->CreateDevice(GUID_SysKeyboard, &lpKeyboard, NULL))) {

// 失败提示或处理 return hr;

}// 设定为通过一个 256 字节的数组返回查询状态值

if( FAILED(hr = lpKeyboard->SetDataFormat(&c_dfDIKeyboard))) {

// 失败提示或处理 return hr;

}// 设定协作模式为独占模式和前台模式,独占模式表面本程序在运行中占有所有键盘资源,而前台模式指出当程序具有焦点时才可以占有键盘资源

if( FAILED( hr = lpKeyboard->SetCooperativeLevel(hWnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND)))

{

// 失败提示或处理 return hr; }

// 设定缓冲区大小

// 如果不设定,缓冲区大小默认值为 0,程序就只能按立即模式工作 // 如果要用缓冲模式工作,必须使缓冲区大小超过 0

// 在此,我们没有必要设定,因为我们就用立即模式工作(还有一种缓冲模式),所有我们将其注调了

/* DIPROPDWORD property;

property.diph.dwSize = sizeof(DIPROPDWORD);

property.diph.dwHeaderSize = sizeof(DIPROPHEADER); property.diph.dwObj = 0;

property.diph.dwHow = DIPH_DEVICE; property.dwData = DINPUT_BUFFERSIZE;

if( FAILED(hr = lpKeyboard->SetProperty(DIPROP_BUFFERSIZE, &property.diph))) {

// 失败

return FALSE; } */

//此处是关键,我们要通过这个函数来锁定键盘,记住,所有的DirectInput资源在使用前都要锁定,在此即获得键盘资源,在知识我们刚才设定的键盘模式才能起作用 hr = lpKeyboard->Acquire(); if FAILED(hr) {

// 失败

return FALSE; }

return TRUE; }

在这段代码中,我们首先定义了 lpDirectInput 和 lpKeyboard 两个指针,前者指向 DIRECTINPUT 对象,后者指向一个

DIRECTINPUTDEVICE 界面。其顺序就是这样的。这和其它COM对象的使用方法都一样,即先创建 COM 对象,然后创建界面,然后再获得 硬件资源,然后使用资源,然后释放。

通过 DirectInputCreate(), 我们为 lpDirectInput 创建了一个 DIRECTINPUT 对象。然后我们调用 CreateDevice 来建立一个DIRECTINPUTDEVICE 界面。

完成这些工作以后,我们便调用 DIRECTINPUTDEVICE 对象的 Acquire 方法来激活对设备的访问权限。在此要特别说明一点,任何一个

DIRECTINPUT 设备,如果未经 Acquire,是无法进行访问的。还有,当系统切换到别的进程时,必须用 Unacquire 方法来释放访问权限,在系统切换回本进程时再调用 Acquire 来重新获得访问权限。 立即模式的数据查询

HRESULT ReadImmediateData( HWND hWnd ) {

HRESULT hr;

BYTE diks[256]; // 创建键盘状态数据缓冲区存取键盘信息 int i; // 计数器

if( NULL == g_pKeyboard )

return S_OK;// 键盘状态数据缓冲区清0

ZeroMemory( &diks, sizeof(diks) );// 获得键盘所有键的信息,这只是检查一次 hr = g_pKeyboard->GetDeviceState( sizeof(diks), &diks ); if( FAILED(hr) )

{

// 如果键盘资源丢失,我们要重新获得 hr = g_pKeyboard->Acquire(); while( hr == DIERR_INPUTLOST ) hr = g_pKeyboard->Acquire(); return S_OK;

}// 进行一下轮循,处理键盘信息。 for( i = 0; i < 256; i++ )

{

if( diks[i] & 0x80 ) //记录此键的状态,低字节最高位是 1 表示按下,0 表示松开,一般用 diks[i]&0x80 来测试 {

switch(i) {

//我们可以通过测试计数器i,来判断是哪个键被按下了。 //我们提供几个数据 UP:200 down:208 left:203 right:205 enter:28 space:57

//其实你可以用DirectX中的Samples\\C++\\DirectInput\\Bin\\Keyboard.exe程序来测试,只不过那是用

//16进制显示的。 case 200: break; case 0xc8: break;

} } }

return S_OK; }

请注意,上面的这段代码只是一个示例,重在使你明白其原理,但并不能满足游戏的需求,因为这其中只查询了一次键盘的全部信息,做了一次轮循,而在游戏中要周期性地查询,并轮循,这就需要你自己用Win32 API函数 SetTimer和 KillTimer 设置初始化 DirectInput 对象函数中在相应的地方设置计计时器,让windows定时向程序发送 WM_TIMER消息,你要通过此消息进行周期性地键盘查询,并在相应的地方解除计时器。

最后一个函数是用于释放指针或DirectInput对象的

void ReleaseDInput(void) {

if (lpDirectInput) {

if(lpKeyboard) {

// Always unacquire the device before calling Release(). lpKeyboard->Unacquire(); lpKeyboard->Release(); lpKeyboard = NULL; }

lpDirectInput->Release(); lpDirectInput = NULL; } }

在这些函数中的注释很明确,关键在于理解其原理,而怎样将他们融入到 Win32 API 程序的基本框架中的,在<<动画程序编写——DirectDraw之旅>> 1-3中的示例代码中已经解释得很明确了,在此不再赘述。不过我们提供其中的代码示例下载。同时你也可以去仔细阅读DirectX 8.0 SDK 包中的 samples\\ multimedia\\ directdraw\\ fullscreenmode 或 \\ samples\\ multimedia\\ directdraw\\

windowedmode 这两个工程中的文件,因为为了我们的示例也是照这两个工程改编过来的,读者可以通过仔细阅读代码和对比我们的更改,而更加了解

DirectDraw的运行运行原理。(请注意:是 DirectX 8.0 SDK 包中的示例,而在 9.0 中 DirectX SDK 已经不提供 DirectDraw的示例代码了) 我们就用这七个函数就已经可以创造出一个小游戏了。

我们下面就要利用<<动画程序编写——DirectDraw之旅>> 1-3 中所用的代码进行进一部的游戏开发。

我们先展示一下 DirectX中\\ samples\\ multimedia\\ directdraw\\ windowedmode 工程中的截图

在这个动画中有黑色背景,并有很多 DirectX 精灵在漂浮。

这是一个全屏的动画程序,而我们在<<动画程序编写——DirectDraw之旅>> 1-3其中做的改动就是为其加了一个背景,改屏幕分辨率 640×480 为 1024×768.注意,因为我们应用的是全屏模式,即可以独占显存资源,所以我们可以更改屏幕的分辨率。这只是做的小小的改动,而我们的目的只在于让大家更加深入了解。且看下面的这副截图:

而我们还要继续深入编程,我们的思路是,先将程序由先前的全屏程序改编成一个windows的窗口程序,然后将其所有的界面翻新,并改编 DirectX精灵为许多小蘑菇在漂浮,还要加入DirectInput 的组建,用键盘控制一个小娃娃。可以上下左右,并可以斜向飞行。 我们先将此动画的截图展现给大家

怎么样,你有什么想法,是想说:“唉,这还不好办,就是又多加了一个!”,但不要光看截图,不要忘记,我们一定让她动起来,并且是可以控制的,这就不是那么简单的事了!

什么?若有人看到这里感到有些迷茫和泄气,不禁想问:“你说了这么多,那么源代码在那里呢!,光给我们几个函数,又能做什么呢?”,如果你这么想,你也不要太急迫。我们还是先分析一下程序框架吧。

不过,还有一件重要的事情,我还是要重申一边。一定要将 DirectX 的头文件价,和lib文件夹加入到 Visual C++.NET 的默认目录中去,这样编译器就可以正确地找到它们了。

如果你不会加入,就请通过工具栏上的 Tool -> Option? 打开Option 对话框,设置如图:

好了,这样我们的准备工作就算已经做好了。 来看看我们的工程文件结构吧,还有工程中的资源。

在工程资源中我们的 ID 号是都用的字符串表示的,笔者认为这样更加方便。 我想对于工程文件中的 ddutil.cpp 和 dxutil.cpp 文件,读者如果了解有些 DirectDraw编程是不会感到陌生的,我们只是将其引入到我们的工程中了。而我们自己实际编程的文件是 outfly.cpp 文件。

我们的程序叙述如下:

首先进行宏定义,结构设置,和全局变量的声明。

后在 WinMain (windows程序的入口点)中首先初始化一切需要初始化的物件(有windows窗口,DirectDraw对象,和 DirectInput对象),在此我们就调用前文讲过的函数,但要有写改动,读者在会在后面看到的。然后进入消息循环,在其中没有消息时,程序会自动更新画面,在有消息时处理消息。 当遇到 WM_QUIT 消息后,结束整个程序。

我们在一些地方有一些小小的改动,我们来看看吧。 1 我们在 HRESULT InitDirectInput( HWND hWnd ) 函数中的开始加入了

KillTimer( hWnd, 0 ); FreeDirectInput();

关掉上一次使用的计时器,并释放 DirectInput 设备。 而在最后加入了

SetTimer( hWnd, 0, 1000 / 100, NULL );

用来重新设置计时器。

2 我们在主窗口的消息处理函数中加入了

case WM_ACTIVATE: //当程序先失去焦点,而现在有重新得到焦点时,要重新锁定键盘资源

if( WA_INACTIVE != wParam && g_pKeyboard ) {

// Make sure the device is acquired, if we are gaining focus. g_pKeyboard->Acquire(); }

break;

case WM_TIMER: //因为设置了计时器所以要处理此消息 if( FAILED( ReadImmediateData( hWnd ) ) ) {

KillTimer( hWnd, 0 );

MessageBox( NULL, _T(\ _T(\

_T(\ }

break;

case WM_DESTROY:// Cleanup and close the app

FreeDirectDraw();

FreeDirectInput(); // 释放资源 PostQuitMessage( 0 ); return 0L;

3 在HRESULT ReadImmediateData( HWND hWnd ) 函数中进行了这样的处理,来时时改变小娃娃的坐标。

for( i = 0; i < 256; i++ ) {

if( diks[i] & 0x80 ) {

switch(i) {

case 200: //上键

if( g_me.fPosY > g_me.fVelY) g_me.fPosY -= g_me.fVelY; else

g_me.fPosY = 0; break;

case 208: //下键

if( g_me.fPosY <= WINDOW_HEIGHT - SPRITE_DIAMETER - g_me.fVelY) g_me.fPosY += g_me.fVelY; else

g_me.fPosY = WINDOW_HEIGHT- SPRITE_DIAMETER; break;

case 203://左键

if( g_me.fPosX > g_me.fVelX) g_me.fPosX -= g_me.fVelX; else

g_me.fPosX = 0; break;

case 205://右键

if( g_me.fPosX <= WINDOW_WIDTH - SPRITE_DIAMETER - g_me.fVelX) g_me.fPosX += g_me.fVelX;

else

g_me.fPosX = WINDOW_WIDTH- SPRITE_DIAMETER; break; } } }

这些只是其中一些比较重要的改动,还有许多改动,读者会在实际的程序中看到的。如果你觉得:“啊!到这里就结束了,可是我还是感到似乎莫不到头绪,就

这样草草收尾了?”,其实文章并没有结束,重头戏还在后面呢,那就不是我的工作了,而是看你有没有耐心去仔细阅读代码了,因为想要把握程序的整体,与其让我将代码放在文章中,还不如读者自己在编译器中自己运行实践一下好,其实我们已经在第二个工程代码中有过详细的解释。但记住一定要按照顺序阅读 工程1,工程2 ,工程3。工程1就是 DirectX中提供的原代码,工程2就是我们改了一个背景的工程,而3就是我们讨论的工程。

斜45度角地图拼接

All Rights Reserved to Lacutis, 2004

有人在新浪网的游戏制作论坛问这个,那我随便说说这个问题的解法,先看看地图元素:

可以看出来是个扁的菱形。这个地图元素的大小是64X32,你可以随意决定元素长宽,在设计程序时,地图元素大小并不重要,只要把尺寸扔进绘图方程,程序就能正确地绘制地图。在这个例子中,我们就先用64X32来演示。

那么这个公式是怎么样的呢?先看看Staggered地图:

<-- Staggered

这个地图有5行,看着这个地图你会想,怎么拼图才能将地图拼出来。再画张图来演示:

从这张图可以看出,拼图时从左到右,从上到下,跟正规的矩形拼图一样,唯一同的是,地图元素与元素之间有重叠,看看第一行和第二行之间,第二行的地图元素会压在第一行的元素上,而第三行的的地图元素则压在第二行的元素上。所以,只要找到正确的公式,你就能正确地设计程序,再来一张图:

图上绿点(是高亮度绿色,不是暗绿色)是每块地图元素的起点,第一行的座标是0,第二行的座标是1,第三行的座标是2,......由这些行位座标决定你的地图元素的起点,从这个规律中看出行位座标0,和行位座标2的横向座标X的起点是一样的是0,行位座标1的起点是向右移半块地图元素。

再从纵向座标找规律,看行位座标0和行位座标2,两块地图元素之间的距离刚好是一块地图元素的高。再看看行位座标0和行位座标1,两块地图元素之间的距离刚好是半块地图元素的高。所以,计算每块地图元素的位置,你的公式刚好是:

void CalculateMapTilePos(int n_map_pos_x, int n_map_pos_y, int & n_scrn_pos_x, int & n_scrn_pos_y) { n_scrn_pos_x = n_map_pos_x * iso_tile_size_x + (n_map_pos_y & 1) * (iso_tile_size_x / 2); n_scrn_pos_y = (n_map_pos_y) * iso_tile_size_y / 2; } 在这个公式中,n_map_pos_x是地图纵横的横向座标,n_map_pos_y是地图纵横的纵向座标,n_scrn_pos_x和n_scrn_pos_y是地图元素在屏幕上的纵横座标。

重要:

首先以上的公式只适用于Staggered斜45度角地图,而slide,和Diamond形地图,这个公式要稍加修改才能用。 Slide:

Diamond:

角色移动的步长、步速与滑步现象

滑步只跟步长有关,就是一轮人物行走(或跑步)实际移动的像素。

当然国产游戏大多做的不好。大多数根本不去认真做人物行走的播放程序,简单一帧帧播放动画,并随意移动小人在地图上的位置了事。

防止滑步又可以随意改变人物移动速度的方法是这样的:

将走路的程序用步长和步速两个量来控制。步长必须定死,按做出来的图片中小人一组动画下来,移动的像素为准。

步速是任意的,可以用游戏每帧或每 1/100 秒,人物移动的像素数来表示(可以是小数,比如每帧刷新移动 1.2 个像素)。

简化讲解,我以横版水平移动走路举例:(斜视角可以类推)

假设步长为 step(pixel),步速为 speed(pixel/frame),人物行走动画帧数为 n(frame)。

注: 其中 step 和 speed 可以是小数; n 是整数。 以下运算全部是浮点运算,真实游戏酌情考虑用定点数。

现在要把人物从坐标 0 移动到 100。

我们可以算出,这个过程是需要 100/speed 帧来完成的。

那么第 i 帧画面,人物在的位置:

pos=i*100/speed

这个时候,需要画第几帧图象呢?

首先我们需要知道第 i 帧画面,也就是 pos 这个位置,人物已经走了t=|pos/step| 步 (t 取整了);

我们的小人正在走在第 t+1 步的 m=(pos-t*step) * 100% 时刻m 是一个 0~1 之间的数字。

f=|n*m| 就是我们在此时刻需要画的行走图片帧了。

加快行走速度,无非是加快了频率,不可能导致滑步的,因为一步迈出的距离根本不会变。

ps. 对于跑步、有腾空,轻微滑步是允许的,所以可以适当加大步长是允许的。

by http://www.codingnow.com

Tile Based Engine 中的墙壁自动拼接处理

基于 Tile 的引擎中重复利用率最高的图素,除了地面就是墙壁了, 大家回想一下恺撒III 中的修筑城墙, 或是 Simcity2000 中拉扯电线, 道路,每种物体都只有十几种基本的图素来拼接,而交叉处理都是程序自动选择正确的图素. 假设我们做一个地图编辑器, 这个功能应该也不能少吧:-)

实际上, 这个问题早在文本模式的程序就已经出现过. 那就是表格线的处理. 文本模式下, 不能绘制直线, 我们需要绘制表格时, 就需要利用 ASCII 字符来拼接出需要的图形. 成功的文本编辑器, 都具有绘制制表线的功能. 其中最需要解决的问题就是表格线交叉时, 如何正确的选择合适的 ASCII 码.

如果采用多层条件判断是很愚蠢的方法. 最简洁的程序是将拼接用的图素按一定的次序 (序号的 2 进制形式可以反映周围的连接状态) 排列. 然后通过简单的公式计算出正确的元素. 具体就是说, 将上下左右的连接用 4 位 2 进制数表示出来. 通过读取当前图素的上下左右的图素是否有连接来生成一个数字, 查表得到正确的图素. :-)

下面附有一个小程序, 可以完成上述的功能, 感兴趣的朋友可以读读 :-) /*

墙壁交叉 Tile 的处理. 类似于文本方式的制表符处理, 我们需要针对制表线的交叉输出正确的 ASCII 码.如果 采用多层条件判断是很愚蠢的方法. 最简洁的程序是将 拼接用的图素按一定的次序 (序号的 2 进制形式可以 反映周围的连接状态) 排列. 然后通过简单的公式计算 出正确的元素.

如下程序演示了这一算法, 其输出结果如下

┏━━┳━━━┳━┓ ┃ ┃ ┃ ┃ ┃ ┃ ┗━┫ ┃ ┃ ┃ ┃ ┏┻━┓ ┃ ┃ ┃ ┃ ┃ ┃ ┗━━╋━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┗━━━━┻━━━┛

程序没有注释, 但应当很容易理解. 如有问题, 可以联系 作者 cloudwu@263.net

Homepage: http://member.netease.com/~cloudwu

云风作于 1999 年 9 月 20 日 */

#include #define MAX 12

char wall[32]=\

char map[MAX][MAX]={ \ \ \ \ \ \ \ \ \ \ \ \};

int main() {

int i,j,id;

for (i=1;i

if (map[i][j]=='.') printf(\ else {

id=(map[i][j-1]=='o')<<3|(map[i+1][j]=='o')<<2|(map[i][j+1]=='o')<<1|(map[i-1][j]=='o');

printf(\ }

printf(\ }

return 0; }

Tile Based Engine的设计 - 遮挡处理

所谓 Isometric, 应该是指等距视角, 和透视相对, 指视野内的物体, 无论远近都用同一大小来表现. 而Tile 就是指的砖块, 我们平常所见的许多 2D 游戏, 都是 Tile 的. 比如新出的决战朝鲜, 还有去年的皇朝霸业;-), 这两个游戏都是典型的 Tile 游戏, 游戏中的元素被分割为一个个的砖块拼装起来. 不同的是, 前者是矩形的 Tile, 而后者是 Isometric 的. 它的Tile 有高度, 底面为一个菱形. Isometric 的外观更接近真实物体, 我个人比较喜欢这样的图形表现形式. 由于 Isometric 的 Tile 相互覆盖, 所以就有了遮挡问题, (传统的矩形Tile是没有遮挡问题的, 但其扩展也需要处理这个,不过比Isometric的简单), 这也许是Isometric Tile 引擎设计的一个难点, 本文

的重点就在于介绍云风的处理方法.

先来看看 Isometric 下的地图,

这里由一个个菱形拼起来,每个菱形就是一个 Tile 的底面, 我将每个 Tile 编了号, 是按游戏世界的坐标编的, 组织数据的时候完全可以按照这个形式在内存开一个2D 数组描述地图. 实际上你也可以按照屏幕的横竖方向编坐标, 处理时注意这时奇数行和偶数行Tile的屏幕坐标相差半个 Tile 宽就可以了, 这样处理也很方便, 实际上两种坐标可以用一个很简单的公式换算, 本文不使用这种坐标,在此不提. 如果你不想为遮挡的问题而烦恼, 最简单的处理方法是每次产生一症图象的时候, 按从左上到右下,从右上到左下(就是游戏世界中的从左到右,从后到前)的顺序把每个Tile画一遍就够了. 但是这对于 Isometric 来说是很大的浪费, 因为 Isometric 中每个Tile 都是等大的,遮挡关系一目了然. 一般我们将Tile 的高度单位设置成 Tile 底面矩形的高,方便处理(如下图). 当游戏世界是单层时,这样一个最小单位的Tile, 它可能被比它的 (x,y) 坐标大 1 的 Tile 共 3 块遮挡. 同样, 如果一个精灵占了 4 个 Tile,可能遮挡它的 Tile 为 5 个. 如果物体有两层, 可能遮挡这个 2x2 物体的 Tile 就是10个, 依此类推.

现在来看一个例子,

上图中, 有两个(固定)物体, 树是 2x2 的, 它占据了 (2,2) (2,3) (3,2) (3,3) 四个Tile, 石块是 1x1 的, 占据了 (1,3) 这个 Tile. 大象(精灵) 也是 2x2 的,目前它在 (0,1) (0,2) (1,1) (1,2) 这4个 Tile 上.

OK, 看看另一张图:

更清楚一些. 一般来说, 绘制 Isometric 的场景分 2 步, 先产生固定的场景,再绘制精灵.精灵由于是运动的, 所以通常每帧图都要重画一次(当然也可以不用全部重画, 让每个精灵的运动频率不同, 动画交错, 提高速度, 这个技术比较复杂, 在我的超越 Isometric Tile Engine 中使用的效果不错:-) 这个例子就是要说明图中的大象是怎么画的: 大象占据的是 (0,1) (0,2) (1,1) (1,2) 4 个Tile, 单层地图中可以遮挡它的 5 个 Tile 分别是 (2,1) (2,2) (2,3) (0,3) (1,3), 这 5 个坐标很容易求得, 最简单的方法是将 大象占据的 4 个 Tile 的坐标都加 1,(从 (x,y) 推出 (x+1,y) (x+1,y+1) (x,y+1) ), 把推出的 12 组坐标中重复的删掉. 当然这不是最好的算法, 这儿只是举个例子. 从地图上可以得到, (2,2) (2,3) (1,3) 上放有物体, 其中 (2,2) (2,3) 是 同一棵树的两部分, 所以只算一个. (1,3) 上是一个石块. 下一步, 我们设置一个剪裁矩形将画大象的区域框起来, 再向上面绘制遮挡它的树和石块. 注意,这里剪裁矩形的设置是不能少的, 否则这个遮挡问题处理完了, 又会引出新的遮挡问题 ;-) 具体步骤如下:

-->

-->

-->

可以看到, 传统的 Isometric Tile 引擎的优点正在于遮挡处理简洁, 速度快. 所以我个人观点就是, 如果要2D 游戏图形引擎要革新, 就全部采用新的复杂的手段, 使其完全摆脱 Tile, 所有的物体都是任意凸多边形, 精灵行走方向任意, 物体高度任意. 否则就采用传统的 Isometric Tile 型, (到目前的经典 2D 游戏几乎都是很正统的Tile Base 的, 比如 Diablo, Starcraft...) 以获得高的处理速度. 从速度和效果两方面看, 界于中间的处理方法,就不是很可取了。

Tile Based Engine的设计 - 坐标变换

Isometric Tile的处理比矩形的稍微复杂一点的地方在于屏幕是矩形的, 而反映出来的游戏世界的坐标轴有些不同. 无论是精灵的移动, 还是处理 Tile 都需要经过坐标变换. 而一个屏幕的区域在游戏世界的地图上却成了一个菱形. 我想,所有第一次设计 Isometric Tile 引擎的程序员都为这个烦躁过 (自己的感受啦;-) 不排除因为这个原因修改自己的原始设计的可能性 ^_^.

实际它一点也不复杂,但需要你静下心来用直尺在白纸上画一张示意图. (注意:如果你贪图一时的便宜,而随手用铅笔比画,只会使你的心情更烦躁) 或者按云风的习惯用C程序来作图(推荐使用风魂 等方便的图形库;-) 当然你 AutoCAD 玩的好的话, 用 ADS 也非常方便. 这些方法不只针对这个特定问题, 我想每个程序员都应该养成这些良好的习惯 ;-) 这里云风为你做了这步. 看下图:

我是按前面的文章中推算的 Tile 形状, 和每个地图块的大小来作的 1:1 的示意图. 绿色框住的是 320x240 的 1/4 屏幕大小的地图块. Tile 的大小设定为 81x41 由于 Tile 间有 1 点的重叠, 所以实际占的空间是 80x40 的. 另外我将左上角的一块 Tile 设定为 (0,0) 其它的 Tile 按相对坐标编了坐标值, 方便大家的观看.

如图,由于 Tile 的大小比较合理, 正好可以在一个地图块中容纳整数个 Tile, 所以我们的坐标变换就非常容易了.按图中所标的相对坐标, 左上角一块为 (0,0). 那么我们将相对坐标 (x,y) 的 Tile 变换为地图块上的像素坐标为(40*(y-x),20*(x+y))

一个地图块里, 包含了 59 个 Tile, 其中边缘部分是和四周重叠的. 如果我们的地图数据是按游戏世界坐标储存. 我们可以看到, 这 59 个 Tile X 范围是 [-4,6] Y 范围是 [-1,9]. 就是说我们至少需要读入游戏世界中的一个 11x11 的地图描述矩阵才能包含这个地图块的数据. 这里, 我建议游戏地图描述数据用 16x16 做一块保存. 在加载几个地图块的图象数据的同时, 我们也可以将相应的地图描述数据的范围计算出来, 分块加载到内存.

两个坐标的换算在解决不同的问题中, 可能有些麻烦, 但我们只需要记住两条基本公式. 游戏世界中的坐标(X,Y)换算到屏幕矩形坐标系(X0,Y0)中是 X0=a*(Y-X),Y0=b*(X+Y) 而反向的转换可以推算出来是 X=(1/2)*(Y0*2-X0)/a, Y=(1/2)*(Y0*2+X0)/a. 在计算除法的时候, 应该注意正负号的问题. (-1/7 取整在这里应该是 -1 而不是 0) 这里 a,b 是矩形的长宽系数, 比如在前面的提到的换算公式中用到的 a=40,b=20 就是 Tile 的长(80)宽(40) 的一半. 同样你要对地图块进行某种运算的时候; 4 和 6 这两个参数将会派上用场 ;-)

by codingnow.com

Tile Based Engine的设计 - 精灵链表

通常说来, 第三人称 2D 游戏中通常把景物和精灵分开处理 (至少我是这样的) 尤其是游戏机上, 硬件对精灵有支持. 现在我们的显卡多也支持显存间的 keycolor 检查 Blt 操作, 实际就是用来加快精灵处理的 (也包括景物)

精灵在运动时, 往往是基于像素的 (虽然有人喜欢简化设计, 精灵在停止的时候仍旧是站在格子里) 而景物却是静止在格子中. 如果能使用更有针对性的方法分别绘制, 将可以提高游戏的速度. 本文的观点源于云风早前所写的斜视角图形引擎的设计系列. 并在近期实践(制作一商业A.RPG游戏)中得到完善.

实际上, 游戏中每帧图象, 没有必要每次用各个图素合成, 尤其在使用了大量如Alpha轮廓, 透明等大运算操作的情况下, 屏幕上并未更改的区域重复被运算非常的浪费时间. 所以我们可以借鉴游戏机的做法, 场景图面创建的稍稍比屏幕大一圈, 只在屏幕移出窗口时再补绘场景, 把精灵提出, 每帧重绘于场景上.

合成精灵与场景有三大问题, 一是如何处理精灵的遮挡问题, Isometric Tile Engine 的遮挡处理一文阐述了云风的观点; 其二是如何清除上一帧的精灵: (当然这一步也可以省略, 改为将场景层和精灵层合成到第三缓冲区) 如果不想多建立一屏幕后台缓冲区, 减少合成图层时的数据移动量, 我们可以采取在绘制精灵前保存精灵原处的场景图象. 具体实施方案在后面将有详细解说。

最后一大要点是以正确的次序绘制精灵了. 这里我想使用一个链表, 串起场景中的所有精灵, 姑且将它称为精灵链表吧. 同时我们还需要另一个链表保存屏幕上可见的精灵, 减少我们处理精灵的数量.

场景精灵链表是创建场景时创建的, 在某精灵消失(死亡)后从链表中删除, 精灵也可以在游戏时被创建加入链表. 而屏幕精灵链表却是绘制屏幕时动态生成, 保证其中精灵满足从后到前的顺序. 这样我们只需要在产生链表后, 依链表次序将精灵绘制到屏幕就可以了 :-)

那么整个处理过程如下:

根据当前的屏幕精灵链表清除上帧图象中所有的精灵 清除当前屏幕精灵链表

遍历整个场景精灵链表, 处理每个精灵的动作和状态, 如果精灵正在屏幕上, 就将其按前后次序插入屏幕精灵链表.

将即将绘制的精灵位置的场景保存 按屏幕精灵链表次序绘制精灵到屏幕 显示一帧图象 循环这些步骤

第一步清除上帧精灵的具体操作中, 我们可以为每一个精灵分配一个缓存图, 保存绘制前的场景. 但这样比较浪费. 因为不在屏幕上的精灵根本用不着这个缓存图. 所以我们可以进一步优化为, 精灵进入屏幕时才分配缓存图, 移出屏幕就释放掉. 不过内存分配实际是一个很消耗时间的过程. 当精灵大小都类似时,进一步的优化方案是限定屏幕上可以同时出现的精灵的数量, 初始化时就统一分配一批等大的缓存图(我称其为缓存池), 其每一个都足已容纳最大的精灵. 当某个屏幕精灵需要缓存图时, 就从缓存池中找一个没有使用的供其使用, 精灵移出屏幕后, 放弃对缓存图的控制即可.

关于第3步将存在于屏幕的精灵插入屏幕精灵链表的操作, 涉及如何判定精灵的前后关系. 我们可以在作图时, 就将精灵的重心定为其参考点保存中图素的文件中. 利用这个参考点的坐标就可以完成精灵的前后判定了 ;-)

本文总结了云风近期制作游戏中的些经验, 希望对后来者有所借鉴. 成文仓促, 错误在所难免. 提及方法也是我的个人观点, 游戏设计的魅力在于其设计时可以不遵循常例,任意发挥, 在此欢迎大家一起探讨, 指出错误和不足.

by codingnow.com

Tile Based Engine的设计 - Tile形状的选取

Tile Base Engine 的优点在于其处理速度. 如果我们设计 Isometric Engine 而无视这个优点, 那未免太亏了. 所以贪图一时编程或美工的方便, 将游戏设计成 Tile 大小随意, 而又不去发挥任意大小 Tile 的优势, (例如形状任意, Sprite 运动路线的任意等等) 将无法超越从前的游戏 Engine。我们必须向效率和表现力两方面中之一努力. 这次我选择了效率.

不象非 Tile Engine 那么自由, Tile Base 的 Engine 必须保证图素被分割成一块块等大的图片, 有利于遮挡运算和重复利用图片.而Isometric Tile 的拼接并不象矩形Tile 那么容易. 计算机位图数据不允许斜线的存在, 所以 Tile 的形状必须细心设计才能保证无缝拼接. 所有拼接问题, 通常指的底面菱形. 下面展示了一组典型的可以相互拼接的菱形. 请注意它们的形状, 四个顶点都是两个点, 这样才能保证可以无缝拼接. 菱形的两条对角线分别是 2n 和 4n+2 (图中 n=5).

除了这种形状,我们还有另一种选择. 就是 4 个顶点都是一个点的. 如图:

看看这种 Tile 的拼接, 我们要注意的是需要在拼接的时候, 顶点重叠对齐. 看看图:

重叠部分是必须的. 这样的菱形对角线分别是 2n-1, 4n+1(图中 n=5). 我的选择正是这第二种形状. 因为它的单个Tile 宽虽然是4n+1, 但每两个间有一点的重合, 整体上看,每个Tile宽占 4n. 而屏幕的宽也是 4 的倍数, 这在后面的设计中可以带来些方便.

P.S. 需要注意的是, Isometric 的 Tile 形状不能任意取, 所以视角也就固定下来了. 我们用 3D 建模软件作图片的时候需要注意视角的问题, 就是摄象机的角度. 在我的另一篇文章《斜视角引擎设计:视角的选择和坐标变换》中讲述过视角的换算方法. 我们这里选取的视角换算出来是 30 度 (arc sin 2n/4n)

by codingnow.com

VAE范例:Apple 制作过程

我的主页http://vczh.cstc.net.cn介绍VAE的文章终于写成几篇了,我便忍不住要开发一个完整的小游戏并写一篇文章来展示展示。所以,这个Apple就新鲜出炉了。关于Apple的代码请见http://vczh.cstc.net.cn。谢谢。 事先声明一下:

图片取自金山打字通2003的“拯救苹果”。

如果金山公司觉得这侵犯了金山公司的利益的话,

请告诉我,我会另换图片。

开始制作了!

事先作一下准备。说是VAE的范例,当然是先下载VAE,然后把vaeDataFile.pas和vaeBmex.pas扔进去。在准备两张图片,一张是Back.bmp,做背景;另一张是Apple.bmp,当然是苹果啦。我依稀记得在几千年前开发Apple的时候,突然心血来潮,使用了Class来编程。所以,大家一定要再建立一个Display.pas来放TApple。哈哈哈……

上面五个文件都放在同一个文件夹里,然后再文件夹里建立一个新的工程叫Apple.exe,现在正式开工。当把vae的两个文件加入工程并且新建Display.pas后,Apple.dpr肯定会变成这样的: program Apple; uses

Forms,

untApple in 'untApple.pas' {frmApple}, Display in 'Display.pas', vaeBmex in 'vaeBmex.pas',

vaeDataFile in 'vaeDataFile.pas';

{$R *.res}

begin

Application.Initialize;

Application.CreateForm(TfrmApple, frmApple); Application.Run; end.

如果不是这样的话,那就要赶快改一改了。 接着,打开Display.pas,开始编写TApple的代码。 先在interface后加入代码如下 unit Display;

interface uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, vaeBmex;

TApple的功能是控制Apple.exe的行为和显示方式。在新的游戏开始后,苹果们会掉下来,并且每个苹果上面印有一个字母。只有按下相应的字母键,该苹果才会消去。如果不行让10个苹果掉了下来,游戏就结束了。所以,玩Apple一定会以失败告终的。因此,Apple提供了一个最高分记录,所以,就当超过最高分为胜利吧。哈哈哈……

Apple.exe一共有五个版面。 1:Logo 2:开始画面

3:游戏画面 4:结束画面

5:难度选择画面

我偷懒了一下,把第1和第4合并了起来。因此,建立五个常数: const

MaxLeft=10;//掉了10个苹果就输了。 R_Static=0;//开始画面

R_Running=1;//游戏画面

R_ShowScore=2;//Logo;结束画面

R_Difficulty=4;//难度选择画面

然后,在implementation上面写一个空的TApple框架: type

TApple=class(TObject) private protected published

public end;

我们需要3个TBmex,一个用来放置背景图片,一个用来放置苹果图片,还有一个做

为游戏画面的缓冲区。于是,我就在private里加入了三个变量:FBack:TBmex; FApple:TBmex; FBuffer:TBmex;。

在游戏画面的上方有一段操作说明文字。这段文字是要程序来设定,而不是TApple来设定的,所以,我又加入了一个FCaption:String;并在public加入了property Caption:String read FCaption write FCaption;以便让程序可以修改这段说明文字。

程序必须知道当前状态(与显示画面差不多),才能正确的控制TApple。所以,我又加入了FRunning:Integer;并在public加入了property Running:Integer read FRunning;以便让程序知道当前状态,并且防止程序改变当前状态使得TApple被破坏。

游戏在运行的时候,除了苹果的位置以外,还依赖于三个参数:1:剩余量。来记录在掉几个苹果游戏就结束了。2:当前分数。3:苹果生产速度。因此,又有了: FLeft:Integer; FScore:Integer; FAppleBuilder:Integer; 与

property Left:Integer read FLeft; property Score:Integer read FScore;

property AppleBuilder:Integer read FAppleBuilder write FAppleBuilder;

在游戏中,我们需要关于苹果的一些信息。有字母,位置,有没有显示这三个量,因此,我就果断地在TApple与type中间加入了: TAppleImp=record X,Y:Integer;

Speed:Integer; Used:Boolean; end;

并在private里写道:Apples:array[65..90]of TAppleImp;

FAppleBuilder是时间间隔,所以,要有一个变量来记录时间,于是TimeAB:Integer;就光荣地诞生了。

Apple.exe为了吸引人们去玩,于是有了一个最高分的概念,所以,我挥一挥手,就不知道从哪里来了一个FHighestScore:Integer;。在结束画面的时候,如果最高分被打破的话,就要先是一句话,所以,我就顺手拿了一个FMadeHighest:Boolean;,并且在显示之后会自动变为False。当最高分被打破的时候,Apple就要在硬盘上记录分数。于是,TApple就要求程序在Create TApple时输入分数文件。于是,为了记录分数文件的文件名,FScoreFile:String;与property ShowScoreCaption:Boolean read FShowScoreCaption write FShowScoreCaption;便呱呱落地了。

我上面写道,我把Logo与结束画面合起来。为了区别,就有了一个变量FShowScoreCaption:Boolean;。为True时就是Logo。

现在,大家所看到的代码必须是: unit Display; interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, vaeBmex;

const

MaxLeft=10; R_Static=0; R_Running=1; R_ShowScore=2; R_Difficulty=4; type

TAppleImp=record X,Y:Integer; Speed:Integer; Used:Boolean; end;

TApple=class(TObject) private

FBack:TBmex; FApple:TBmex; FBuffer:TBmex; FCaption:String; FRunning:Integer;

FLeft:Integer; FScore:Integer;

FAppleBuilder:Integer;

FShowScoreCaption:Boolean;

FScoreFile:String;

FHighestScore:Integer;

FMadeHighest:Boolean;

Apples:array[65..90]of TAppleImp; TimeAB:Integer; protected published

public

property Caption:String read FCaption write FCaption; property Running:Integer read FRunning; property Left:Integer read FLeft; property Score:Integer read FScore;

property AppleBuilder:Integer read FAppleBuilder write FAppleBuilder;

property ShowScoreCaption:Boolean read FShowScoreCaption write FShowScoreCaption; end;

implementation end.

否则的话,赶快改!!!

我依稀记得在写完这段声明之后,一大堆富有建设性的代码就跟着涌了出来。TApple的第一件事就是Create与Free。于是,我先机械地在published里写道:

constructor Create(ScoreFile:String); procedure Free;

当然,接着就是那些富有建设性的代码了。 constructor TApple.Create(ScoreFile:String); var SL:TStringList;//用来打开分数文件 begin

//建立TBmex

FBack:=TBmex.Create; FApple:=TBmex.Create;

FBuffer:=TBmex.Create;

FBuffer.Canvas.Font.Name:='Arial Black';

//接下来的这一行代码是没有意义的。我只是忘了删掉而已,顺便保持代码与文章的一致性。

FRunning:=R_Static; //设置一些东西

FShowScoreCaption:=False; FMadeHighest:=False; FScoreFile:=ScoreFile; //打开分数文件

SL:=TStringList.Create;

SL.LoadFromFile(FScoreFile); FHighestScore:=StrToInt(SL[0]);

SL.Free; end;

Free的代码我就不介绍了。一定是那些东西。 procedure TApple.Free; begin

FBack.Free; FApple.Free;

FBuffer.Free;

inherited Free;//如果你不怕内存泄漏的话,这句代码你可以考虑删掉。 end;

接下来,在private里写一些工具代码。 1:把背景图复制到缓冲区。 procedure TApple.DrawBack; begin

FBuffer.Draw(FBack,0,0);

end;

2:打印字符串。字符串是白色的,外面还包围着一圈黑色。怎么办呢? procedure TApple.DrawText(X,Y:Integer;Caption:String);

var C,R:Integer;

begin

FBuffer.TextOut(X,Y,Caption,clWhite,clBlack); end;

有了VAE就是不一样。哈哈哈……

3:画苹果。现在不知要复制图片,还要画一个字母: procedure TApple.DrawApple(X,Y:Integer;C:Char); var W,H:Integer; begin

DrawTrans(FBuffer,FApple,X-FApple.Width div 2,Y-FApple.Height div 2); FBuffer.Canvas.Brush.Style:=bsClear; FBuffer.Canvas.Font.Size:=24; FBuffer.Canvas.Font.Color:=clWhite; W:=FBuffer.Canvas.TextWidth(C);

H:=FBuffer.Canvas.TextHeight(C); FBuffer.Canvas.TextOut(X-W div 2,Y-H div 2,C);

end;

X,Y代表的是中心位置。为了使字符被画在中间,字符的位置自然就是(X-W div 2,Y-H div 2)了。

4:画透明图像。

procedure TApple.DrawTrans(Dst,Src:TBmex;Left,Top:Integer); begin

Dst.DrawTrans(Src,0,0,Src.Width,Src.Height,Left,Top,0); end;

DrawApple就有用到这个。 5:获得空闲的苹果。

function TApple.GetUnusedApple:Integer; var I,C:Integer; begin C:=0;

for I:=65 to 90 do

if Apples[I].Used then C:=C+1; if C=26 then result:=-1 else begin

repeat

I:=Random(26)+65; until not Apples[I].Used; result:=I;

end; end;

苹果的字符信息被苹果在数组里的位置隐含了。如果所有的苹果都显示了的话,就返

回-1。否则,随机返回一个还没被显示的苹果。

以后,就可以在public里写代码了。

TApple并不知道程序的窗体的大小,因此,就有了一个SetSize,用于传入窗体的大小,并可以用来设置FBuffer的大小:

procedure TApple.SetSize(Width,Height:Integer); begin

FBuffer.Width:=Width;

FBuffer.Height:=Height; end;

接着,程序必须向TApple传入两张图片文件,才能将结果正确地显示出来: procedure TApple.SetBack(FileName:String); begin

FBack.LoadFromFile(FileName); end;

procedure TApple.SetApple(FileName:String); begin

FApple.LoadFromFile(FileName); end;

接下来,写一个把FBuffer画到Form上面的代码: procedure TApple.PaintToForm(Form:TForm); begin

FBuffer.DrawToCanvas(Form.Canvas,0,0); end;

接着,写几段打开画面的代码。 1:Logo/结束画面: procedure TApple.ShowScore; begin

FRunning:=R_ShowScore; Draw;

end;

Draw例程以后再来。如果想显示Logo的话,就先把ShowScoreCaption设置为True,然后再调用ShowScore。

2:难度选择画面:

procedure TApple.ShowDifficulty; begin

FRunning:=R_Difficulty; Draw; end;

3:开始画面: procedure TApple.Reset; begin

FRunning:=R_Static; end;

4:结束,回到开始画面: procedure TApple.StopGame; var I:Integer; begin

for I:=65 to 90 do Apples[I].Used:=False; FRunning:=R_Static;

end;

先把所有的苹果都隐藏了之后再设置FRunning。 5:游戏画面:

procedure TApple.NewGame; var I:Integer;

begin

//初始化随机数发生器 Randomize; //隐藏所有苹果 for I:=65 to 90 do

Apples[I].Used:=False; //当前时间设置为0 TimeAB:=0; //打开游戏画面

FRunning:=R_Running; //剩余总数设置为MaxLeft FLeft:=MaxLeft; //当前分数设置为0

FScore:=0; end;

TApple的最后三段代码,就是富有建设性的代码的核心部分,来了! 先写击键后消除苹果的代码: procedure TApple.HitKey(Key:Word); begin

//如果按键代表的苹果存在的话 if Apples[key].Used then begin

//增加分数

FScore:=FScore+1; //隐藏被击中的苹果 Apples[Key].Used:=False; end;

end;

用位置代替字符的好处就在这里,在检测的时候不需要用循环。 然后写游戏过程的代码。这个有点长: procedure TApple.Run; var NewApple:Integer;

I:Integer;

SL:TStringList; begin

if TimeAB=0 then//如果当前时间是0的话,那么,就必须生产一个苹果。 begin

//获得空闲的苹果

NewApple:=GetUnusedApple;

//如果返回的是 –1,那么,代表所有的苹果都被显示了,不能生产苹果。 if NewApple>-1 then begin

//接触苹果的隐藏

Apples[NewApple].Used:=True; //设定苹果的高度

Apples[NewApple].Y:=-FApple.Height div 2; //设定苹果的速度

Apples[NewApple].Speed:=Random(3)+1; //设定苹果的位置

Apples[NewApple].X:=Random(FBuffer.Width-FApple.Width)+FApple.Width div 2; end; end;

TimeAB:=TimeAB+1;

//当前时间超过FAppleBuilder的话,就要重置为0,否则,将永远没有苹果产生。 if TimeAB=FAppleBuilder then TimeAB:=0; //移动苹果

for I:=65 to 90 do

if Apples[I].Used then

Apples[I].Y:=Apples[I].Y+Apples[I].Speed; //如果苹果掉地上了,就要扣FLeft了。 for I:=65 to 90 do

if Apples[I].Used then

if Apples[I].Y>=FBuffer.Height-FApple.Height div 2 then begin

//隐藏苹果

Apples[I].Used:=False; FLeft:=FLeft-1; end;

//如果FLeft被扣完了的话 if FLeft=0 then begin

//打开结束画面

FRunning:=R_ShowScore; //如果分数打破了纪录 if FScore>FHighestScore then

begin

//显示相应的语句 FMadeHighest:=True; FHighestScore:=FScore; //更改分数文件的内容 SL:=TStringList.Create; SL.Text:=IntToStr(FScore); SL.SaveToFile(FScoreFile); SL.Free; end; end; //刷新

Draw; end;

最后,就是显示代码了: procedure TApple.Draw; var I:Integer; T:String; begin DrawBack;

case FRunning of

R_ShowScore://显示一些字符串。 begin

FBuffer.Canvas.Font.Size:=12;

FBuffer.Canvas.Brush.Style:=bsClear; if FShowScoreCaption then begin

T:='Welcome to the Apple!';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,130,T); T:='Vczh''s first cartoon-style game.';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,150,T); T:='Developer:Vczh';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,170,T); T:='Web Site:http://vczh.cstc.net.cn';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,190,T); T:='Pictures from KingSoft';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,210,T); T:='Press any key to start.';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,230,T); end else

begin

T:='You are lost!Your score is '+IntToStr(FScore)+'.';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,170,T);

if FMadeHighest then

begin

T:='You made the highest score!';

DrawText((FBuffer.Width-FBuffer.Canvas.TextWidth(T))div 2,190,T); FMadeHighest:=False; end; end; exit; end; R_Difficulty:

begin

DrawText(200,150,'1:Easy'); DrawText(200,170,'2:Normal'); DrawText(200,190,'3:Hard'); DrawText(200,210,'4:Impossible'); DrawText(200,230,'F3:Back'); end; else

begin//游戏画面或者开始画面 //显示没有被隐藏的苹果 for I:=65 to 90 do

if Apples[I].Used then

DrawApple(Apples[I].X,Apples[I].Y,Chr(I)); FBuffer.Canvas.Font.Size:=12;

FBuffer.Canvas.Brush.Style:=bsClear; //显示标题

DrawText(2,2,FCaption); //显示最高分

DrawText(2,22,'Higest Score:'+IntToStr(FHighestScore)); //如果游戏正在进行的话

if FRunning=R_Running then begin

//显示剩余量和分数

DrawText(2,42,'Left:'+IntToStr(FLeft)); DrawText(2,62,'Score:'+IntToStr(FScore)); end; end;

end; end;

TApple大功告成!

不过,不要高兴得太早,程序部分还没有写。

程序部分倒是简单,只要响应键盘就行了。 先来几个变量:

Apple:TApple;//辛辛苦苦建立起来的TApple如果没有使用的话,那就是浪费了。

AppPath:String;//程序所在的位置,学了VB6的App.Path。 OnCreate事件

procedure TfrmApple.FormCreate(Sender: TObject); begin

//获得程序的位置

AppPath:=ExtractFilePath(ParamStr(0)); //新建TApple并设置

Apple:=TApple.Create(AppPath+'Score.txt'); Apple.SetSize(ClientWidth,ClientHeight); Apple.SetBack(AppPath+'Back.bmp');

Apple.SetApple(AppPath+'Apple.bmp');

Apple.Caption:='F2:New Game; F3:Stop; F4:Pause; Esc:Exit.'; //使窗体居中

Left:=(Screen.Width-Width)div 2;

Top:=(Screen.Height-Height)div 2; //用ShowScore显示Logo画面 Apple.ShowScoreCaption:=True; Apple.ShowScore; Apple.Draw; end;

OnDestory事件

procedure TfrmApple.FormDestroy(Sender: TObject); begin

Apple.Free; end;

OnPaint事件

procedure TfrmApple.FormPaint(Sender: TObject); begin

Apple.PaintToForm(self); end;

OnKeyUp事件

procedure TfrmApple.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin

if Apple.Running=R_ShowScore then begin

//如果显示Logo或分数的话,就回到开始画面 Apple.ShowScoreCaption:=False; Apple.StopGame; Apple.Draw; FormPaint(self); end else

case key of

本文来源:https://www.bwwdw.com/article/r5v6.html

Top