游戏编程入门(3):绘制基本 GDI 图形

接上文 游戏编程入门(2):创建游戏引擎和使用该引擎制作小游戏

本篇内容包括:

  • 使用Windows图形设备接口绘制图形的基础知识
  • 设备环境是什么以及它为什么对GDI图形如此重要
  • 如何在Windows中绘制文本和原始图形
  • 如何创建一个示例程序,在游戏引擎的环境中演示GDI图形

图形基础

理解图形坐标系统

所有图形计算系统都使用某种图形坐标系统来指定在一个窗口中或者屏幕上排列多少个点。图形坐标系统通常都清楚表明系统的原点(0,0)以及坐标轴和各个轴上值增加的方向。

Windows图形依靠一个类似的坐标系统来指定如何和在哪里执行绘制操作。因为Windows中的所有绘制都发生在窗口的范围内,所以Windows坐标系统是相对于一个特定窗口的。Windows坐标系统有一个原点,它位于窗口的左上角,正x值向右增加,正y值向下增加。Windows坐标系统中的所有值都是正整数。

游戏编程入门(3):绘制基本 GDI 图形_第1张图片

学习颜色的基础知识

Windows颜色是由主要颜色(红,绿,蓝)数字亮度的结合来表示的,这种颜色系统称为RGB,它是大多数图形计算机系统上的标准颜色系统。

下图显示了一些基本颜色的红,绿,蓝部分的数值。注意,各个颜色部分的亮度值范围是0~255。

游戏编程入门(3):绘制基本 GDI 图形_第2张图片

COLORREF 结构

Win32 API定义了一个名为COLORREF的结构,将RGB颜色的红、绿、蓝部分结合为一个单独的值。COLORREF结构非常重要,在整个Win32 API中都使用它来表示RGB颜色,要想创建一个颜色作为一个COLORREF结构,可以使用RGB( )宏,它接受红,绿,蓝部分作为参数。

下面是一个使用RGB( ) 创建纯绿色的例子。

COLORREF greeen=RGB(0,255,0);

在这一行代码中,因为绿色部分(中间一个参数)指定为255,而红色和蓝色部分指定为0,所以代码创建的颜色是绿色。更改这3个参数的值就会改变颜色的混合效果,越小的数产生越暗的颜色,越大的数产生越亮的颜色。

查看Windows中的图形

为了完全支持众多图形输入设备,Windows处理图形绘制的方式可能与我们想象的并不同。Windows不允许直接在屏幕上进行绘制,而是使用一个名为图形设备接口(Graphics Device Interface GDI)的层,将绘制操作与物理的图形设备(例如显示器和打印机)分开。GDI 的作用是一个程序接口,以便以一种通用的方式绘制图形。

GDI 操作与Windows图形驱动程序配合工作,与物理图形设备交流。

GDI 的结构

游戏编程入门(3):绘制基本 GDI 图形_第3张图片

使用设备环境

GDI 图形中的关键组件是图形环境或者设备环境,它充当了物理图形设备的入口。可以将设备环境视为要绘制图形的一个常规绘图表面。换句话说,设备环境就像用来绘图的一张纸,只是在绘图之后,能够在多种不同的设备上显示所得到的图像。设备环境在Windows编程中非常重要,它们使得拥有独立于设备的图形成为可能。

设备环境实际上只不过是一种允许以通用方式绘图而不必担心绘图实际上发生在什么位置的方法。无论是绘制到屏幕,内存还有打印机,都需要设备环境,这样才能够使用相同的图形例程。

当然,在游戏编程中总是绘制到屏幕上,但是这并不意味着就可以忽视GDI。要想使用GDI绘制图形,就必须使用设备环境,因此你也可能习惯了它们。要记住的一点是,在Windows中做的所有绘制工作实际上都是对设备环境执行的,然后由Windows确保在设备环境上的绘制工作会正确显示在屏幕上。

BeginPaint( ) 和 EndPaint( )

通常,通过调用Win32 BeginPaint( )函数来获得设备环境。BeginPaint( )和 EndPaint( )成对出现,构成一对图形绘制函数,如下所示:

PAINTSTRUCT ps;
HDC hDC=BeginPaint(hWindow,&ps);
***在这里执行GDI绘制操作***
EndPaint(hWindow,&ps);

BeginPaint( )函数需要一个窗口句柄和一个PAINTSTRUCT 结构,PAINTSTRUCT 包括属于设备环境的信息,很少应用在游戏编程中。BeginPaint( )函数返回一个设备环境的句柄,开始使用GDI绘制图形时,只要有这个句柄就可以了。然后,EndPaint( ) 函数负责在使用完设备环境后释放它。

也可以在成对的BeginPaint( ) / EndPaint( ) 函数外部进行绘制,在这种情况下,必须以一种稍微不同的方式获得设备环境。这是使用GetDC( )函数完成的,这个函数只需要一个窗口句柄即可获得设备环境。在使用设备环境时,必须一起使用ReleaseDC( )函数和 GetDC( )函数,以便释放设备环境。

下面这个例子说明了这两个函数是如何配合使用的:

HDC hDC=GetDC(hWindow);
***在这里执行GDI绘制操作***
ReleaseDC(hWindow,hDC);

除了设备环境之外,GDI 还支持下面常用的图形组件,在开发游戏图形时,你会发现它们很有用。

  • 画笔
  • 画刷
  • 位图
  • 调色板

使用画笔 写

GDI 中的画笔类似于现实世界中的水笔,它们用来绘制直线和曲线。可以使用不同的宽度和不同的颜色创建画笔。

有两种画笔,逻辑修饰(cosmetic)画笔和几何(geometric)画笔

逻辑修饰画笔绘制固定宽度的线条和需要快速绘制的线条。
几何画笔绘制可缩放的线条、比单个像素更宽的线条以及样式独特的线条。由于逻辑修饰画笔提供最快的绘制方法,所以他们是游戏编程中最常用的画笔类型。

使用画刷 绘制

GDI 中的画刷类似于现实世界中的刷子,它们用来绘制多边形、椭圆以的内部。虽然普遍认为刷子使用一种纯色,但是也可以根据一个位图模式定义GDI 画刷,也就是以一种模式而不是纯色进行绘制。

在使用GDI 绘制图形时,将结合使用画刷和画笔。例如,如果要绘制一个圆,则使用画笔绘制圆的轮廓,而使用画刷绘制其内部。

使用位图 绘制图像

位图是以像素数组形式存储的图形图像

位图的形状是一个矩形,因此位图中的像素数是位图的宽度乘以其高度。位图可以包含多种颜色,并且经常基于一个特定的调色板或者一组特定的颜色。

使用调色板 管理颜色

调色板是GDI 在显示位图时使用的一组颜色。

可以使用很多不同的格式存储位图图像,从每像素1位到每像素32位。256色位图需要每像素8位,因此称为8位图像。在纯粹的黑白位图中,因为各个像素的状态都是打开(黑色)或者关闭(白色),所以只需要每像素1位,它们称为1位图像。当今最流行的图像是24位图像,它们能够使用数百万种不同的颜色极好地表示出细节积极丰富的图像。

绘制窗口

Win32 API 包含一个特殊的消息,每次需要绘制一个窗口时,就会将这个消息发送给窗口,这个消息称为WM_PAINT,它是在窗口中绘制图形的主要方法之一。

第一次创建窗口时,从其他窗口后面显示窗口时或者由于其他各种原因,都可能需要重新绘制窗口。要想重新绘制窗口的内部(客户区域),就必须处理WM_PAINT消息。

绘制窗口时,实际上值得是窗口的客户区域。客户区域是指窗口的一个矩形部分,他位于窗口边框内部,并且不包括窗口框架、标题、菜单、系统菜单以及滚动条。

在一个设备环境或者DC上执行GDI 绘图操作,然后将这个DC通过 hDC 参数传递给这个函数。下面是在GamePaint( )函数中绘制一条线的例子:

void GamePaint(HCD hDC)
{
    MoveToEx(hDC,0,0,NULL);
    LineTo(hDC,50,50);
}

绘制文本

在Windows中,将文本和图形视为是相同的,也就是说,文本也是使用 GDI 函数 绘制的。用来绘制文本的主要GDI 函数是TextOut( ),如下所示:

BOOL TextOut(HDC hDC,int x,int y,LPCTSTR szString,int iLength);

下面是TextOut( )函数不同参数的含义。

  • hDC:设备环境句柄
  • x:文本位置的x坐标
  • y:文本位置的y坐标
  • szString:要绘制的字符串
  • iLength:要绘制的字符串的长度

TextOut( )的第一个参数是一个设备环境句柄,它由BeginPaint( )函数提供。所有GDI 函数都需要一个设备环境句柄,因此在图形代码中看到它应该是不足为奇的。x和y参数指定了第一个字符串字符相对于客户区域左上角的位置,而最后两个参数是一个指向字符串的指针和字符串的长度。

游戏编程入门(3):绘制基本 GDI 图形_第4张图片

下面这个例子显示了如何使用TextOut( )函数来绘制一个简单的文本串。

void GamePaint(HDC hDC)
{
    TextOut(hdc,10,10,TEXT("Hi,Jurbo"),8);
}

可以考虑使用的另一个与文本有关的函数是DrawText( ),它允许在一个矩形内部绘制文本,而不是在一个指定的点绘制文本。

举一个例子,通过指定整个客户窗口为要绘制文本的矩形,可以使用DrawText( )使一行文本在屏幕上居中。下面这个例子使用DrawText( )函数取代TextOut( )。

void GamePaint(HDC hDC)
{
    RECT rect;
    GetClientRect(hWindow,&rect);
    DrawText(hDC,Text("Hi,Jurbo"),-1,&rect,DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}

在这个例子中,绘制的文本在水平方向和垂直方向上都位于窗口的整个客户区域的中央,注意,文本串的长度不是必须的,所以这里提供的长度是-1,意思是由于字符串是以NULL结束的,因此应该自动确定其长度。

DrawText( )的最后一个参数中的标志用来确定如何绘制文本,在这里是使文本在水平以及垂直方向向上居中。

绘制图元

图元是GDI 的一个基础部分,包括直线、矩形、圆、多边形、椭圆和弧等。结合使用这些图元可以创建出给人深刻印象的图形。

下面是可以使用GDI 函数绘制的几个主要图元:

  • 直线
  • 矩形
  • 椭圆
  • 多边形

下面将介绍如何绘制这些图元以及如何使用画笔和画刷为它们添加颜色。

直线

直线是最简单的图元,因此也最容易绘制。直线是使用MoveToEx( )和LineTo( )函数绘制的,这两个函数分别设置当前的位置和绘制一条直线(从当前位置连接到指定的终点)。其思路是,使用MoveToEx( )将画笔移动到一个特定的点,然后使用LineTo( )从这个点到另一个点绘制一条线。通过再次调用LineTo( ),可以继续绘制连接各个点的直线。

下面是这些函数的原型:

BOOL MoveToEx(HDC hDC,int x,int y,LPPOINT pt);
BOOL LineTo(HDC hDC,int x,int y);

Windows中的一个xy坐标被称为一个点,通常用Win32 POINT表示。在Windows中,POINT结构用来表示各种不同操作的坐标,POINT结构只包括两个长整数字段x和y。

这两个函数都接受一个设备环境的句柄和直线上的点的x和y值。MoveToEx( )函数还允许提供一个参数来存储以前的点。如果希望获得用于绘制的最后一个点,则可以将一个点的指针作为MoveToEx( )的最后一个参数。

下面是使用MoveToEx( )和LineTo( )绘制两条线的例子:

void GamePaint(HDC hDC)
{
    MoveToEx(hDC,10,40,NULL);
    LineTo(hDC,44,10);
    LineTo(hDC,78,40);
}

在这段代码中,首先通过调用MoveToEx( )并提供一个点的xy位置来设定绘制位置。注意,最后一个参数是NULL,这表示不想获得上一个点。然后调用了两次LineTo( )函数,这将导致绘制两条相连的线。

矩形

矩形是另一种非常容易绘制的图元。Rectangle( )函数允许通过指定矩形的左上角和右下角来绘制一个矩形。

下面是Rectangle( )函数的原型:

BOOL Rectangle(HDC hDC,int xLeft,int yTop,int xRight,int yBottom);

Rectangle( )函数非常简单,只需要向其传递要绘制的矩形的两个点的位置即可。下面这个例子显示了如何绘制两个矩形。

void GamePaint(HDC hDC)
{
    Rectangle(hDC,16,36,72,70);
    Rectangle(hDC,34,50,54,70);
}

注意,Rectangle( )函数的最后两个参数是矩形的右下角的x和y位置,而不是矩形的宽度和高度。

另一个方便的矩形函数是FillRect( ),它用来使用特定的颜色填充一个矩形区域。用于填充的颜色由传递给函数的画刷决定。下面是FillRect( )函数的原型:

int FillRect(HDC hDC,CONST RECT* lprc,HBRUSH hbr);

与Rectangle( )函数不同,FillRect( )函数接受一个RECT结构的指针,而不是4个单独的与矩形有关的值。

椭圆

虽然椭圆是弯曲的,但是椭圆的绘制方式与矩形非常相似。椭圆只不过是一条闭合的曲线,因此它可以由一个边界矩形指定。

椭圆是使用Ellipse( )函数绘制的,它的原型如下所示:

BOOL Ellipse(HDC hDC,int xLeft,int yTop,int xRight,int yBottom);

使用Ellipse( )函数绘制一个标准的圆,只需指定一个宽度和高度相等的矩形即可。

Ellipse( )函数接受要绘制的椭圆的边界矩形的特征数字。下面是使用Ellipse( )函数绘制椭圆的例子:

void GamePaint(HDC hDC)
{
    Ellipse(hDC,40,55,48,65);
}

这四个值指定了椭圆的边界矩形。

多边形

最棘手的一种图元就是多边形,它是由多条线段构成的一个封闭形状。多边形是使用Polygon( )函数绘制的,函数原型如下所示:

BOOL Polygon(HDC hDC,CONST POINT* pt,int iCount);

可以看出,Polygon( )函数比其他图元函数稍微复杂一些,它需要一个点数组以及点的数量作为参数。

多边形是通过使用直线连接数组中的各个点来绘制的,下面这个例子显示了如何使用Polygon( )函数来绘制一个多边形。

void GamePaint(HDC hDC)
{
    POINT points[3];
    points[0]={5,10};
    points[1]={25,30};
    points[2]={15,20};
    Polygon(hDC,points,3);
}

这段代码关键之处在于创建点数组points,它包含了3个POINT结构,这3个POINT结构都使用一个 xy对 进行了初始化,然后将整个数组以及数组中的点数传递给Polygon( )函数,这就是绘制一个包含多条直线的多边形所需要的全部工作。

使用画笔和画刷

以默认的普通黑白样式绘制图元是一回事,而控制图元的直线和填充颜色以获得更有趣的结果又是另一回事。后者是通过使用画笔和画刷来实现的。

画笔和画刷是用于绘制图元的标准GDI 对象。

其实在绘制图元时,已经使用了画笔和画刷。默认的画笔是黑色的,而默认的画刷的颜色与窗口背景颜色相同。

创建画笔

如果希望更改图元的轮廓,则需要更改用来绘制该图元的画笔。这通常涉及创建新画笔,这是使用CreatePen( )函数实现的。

HPEN CreatePen(int iPenStyle,int iWidth,COLORREF crColor);

第一个参数是画笔的样式,它可以是下面的值之一:PS_SOLID、PS_DASH、PS_DOT、PS_DASHDOT、PS_DASHDOTDOT或者PS_NULL。除了最后一个值之外,其他所有值都指定了使用画笔绘制的不同线条类型,如实线、虚线、点线或者虚线和点线的结合。

最后一个值PS_NULL表示不绘制任何轮廓,换句话说,就是画笔没有进行绘制。

CreatePen( )的第2个参数是画笔的宽度,其单位是逻辑单元,在屏幕上绘制时,逻辑单元通常对应于像素。最后一个参数是画笔的颜色,它指定为一个COLORREF值。

下面这个例子,显示了如何创建1个像素宽的纯蓝色画笔。

HPEN hBulePen=CreatePen(PS_SOLID,1,RGB(0,0,255));

记住,只创建画笔还不能使用它来绘制,还需要将画笔选入设备坏境才能开始使用它绘制,将在稍后介绍。

创建画刷

虽然在GDI中支持集中不同的画刷,但是我们重点介绍纯色画刷,它允许使用一种纯色来填充图形形状。

使用Win32 CreateSolidBrush( )函数即可创建纯色画刷,这个函数只接受一个COLORREF结构。

下面是创建紫色画刷的例子:

HBRUSH hPurpleBrush=createSolidBrush(RGB(255,0,255));

选定画笔和画刷

要想使用已经创建好的画笔的画刷,必须使用Win32 SelectObject( )函数将其选入设备环境,这个函数用来将图形对象选入设备环境,它适用于画笔及画刷。

下面是将画笔选入设备环境的例子:

HPEN hPen=SelectObject(hDC,hBluePen);

在这个例子中,将刚刚创建的hBluePen选入了设备环境。还要注意,将从SelectObject( )返回以前选定的画笔并存储在hPen中。

这非常重要,因为在完成绘制时,通常都希望将GDI 设置恢复到原始状态。换句话说,我们希望记住原始的画笔,以便在完成后恢复其设置。

下面是使用SelectObject( )来恢复原始画笔的例子:

SelectObject(hDC,hPen);

与创建画笔有关的另一项重要任务是删除所创建的图形对象。可以使用DeleteObject( )函数来完成这个任务,这个函数适用于画笔以及画刷。在使用完任何图形对象并且不再将其选入设备环境时,删除它们是一项非常重要的工作。

下面是清除蓝色画笔的例子:

DeleteObject(hBluePen);

选定和删除画刷与选定和删除画笔非常类似。

下面来举一个例子来进行说明:

HBRUSH hBrush=SelectObject(hDC,hPurpleBrush);
//在这里执行一些绘制操作
SelectObject(hDC,hBrush);
DeleteObject(hPurpleBrush);

在这个例子中,将紫色画刷选入了设备环境,执行了一些绘制工作,恢复原来的画刷,然后删除紫色画刷。

开发Crop Circles 示例

现在我们已经基本了解了GDI 图形代码的各种细节知识,并且学习了如何执行基本的绘制操作来获得各种不同的图形形状。此外,还学习了如何通过创建和使用不同的画笔和画刷来调整这些形状的外形。

现在,我们将演示如何在游戏环境中绘制图形,Crop Circles 显示了一系列随机连接的圆圈,就像英国农场里神秘出现的麦田怪圈一样。

Crop Circles 示例的思路是,在游戏引擎的各个周期中绘制一条连接到一个随机位置上的圆的直线。虽然直线和圆是响应一个游戏循环而在GamePaint( )函数外部绘制的,但是演示如何在GamePaint( )内部绘制仍然非常重要,这样在最小化或者隐藏窗口时才不会丢失所绘制的图形。

基于这个原因,Crop Circles 在GamePaint( )中绘制了一个深黄色背景,以说明即使必须重新绘制窗口,也会保留在这个函数中绘制的图形。

实际的直线和圆是在GameCycle( )中绘制的,这意味着如果重新会指窗口,那么它们将会丢失。

Crop Circles 目录结构和效果图

Crop Circles 目录结构:

游戏编程入门(3):绘制基本 GDI 图形_第5张图片

Crop Circles 效果图:

游戏编程入门(3):绘制基本 GDI 图形_第6张图片

Crop Circles 源代码

Resource.h

//-----------------------------------------------------------------

//-----------------------------------------------------------------
// Icons                    Range : 1000 - 1999
//-----------------------------------------------------------------
#define IDI_CROPCIRCLES     1000
#define IDI_CROPCIRCLES_SM  1001

CropCircles.h

#pragma once

//-----------------------------------------------------------------
// 包含文件
//-----------------------------------------------------------------
#include 
#include "Resource.h"
#include "GameEngine.h"

//-----------------------------------------------------------------
// 全局变量
//-----------------------------------------------------------------
GameEngine* g_pGame;
RECT        g_rcRectangle;  //虽然这是一个矩形,但实际用来存储椭圆的信息

CropCircles.cpp

//-----------------------------------------------------------------
// 包含的文件
//-----------------------------------------------------------------
#include "CropCircles.h"

//-----------------------------------------------------------------
// 游戏事件函数
//-----------------------------------------------------------------
//初始化游戏
BOOL GameInitialize(HINSTANCE hInstance)
{
  // 创建游戏引擎
  g_pGame = new GameEngine(hInstance, TEXT("Crop Circles"),
    TEXT("Crop Circles"), IDI_CROPCIRCLES, IDI_CROPCIRCLES_SM);
  if (g_pGame == NULL)
    return FALSE;

  // 设置帧频为1帧/秒 使得麦田怪圈逐渐出现在游戏屏幕上
  g_pGame->SetFrameRate(1);

  return TRUE;
}

//游戏开始
void GameStart(HWND hWindow)
{
  // 生成随机数种子
  srand(GetTickCount());

  // 设置初始麦圈的位置和大小
  g_rcRectangle.left = g_pGame->GetWidth() * 2 / 5;
  g_rcRectangle.top = g_pGame->GetHeight() * 2 / 5;
  g_rcRectangle.right = g_rcRectangle.left + g_pGame->GetWidth() / 10;
  g_rcRectangle.bottom = g_rcRectangle.top + g_pGame->GetWidth() / 10;
}

//游戏结束
void GameEnd()
{
  // 清理游戏引擎
  delete g_pGame;
}
//激活游戏
void GameActivate(HWND hWindow)
{
}
//停用游戏
void GameDeactivate(HWND hWindow)
{
}
//绘制游戏
void GamePaint(HDC hDC)
{
  // 绘制一个深黄色背景,作为麦圈的田地
  RECT    rect;
  GetClientRect(g_pGame->GetWindow(), &rect);
  HBRUSH hBrush = CreateSolidBrush(RGB(128, 128, 0));   // dark yellow color
  FillRect(hDC, &rect, hBrush);
  DeleteObject(hBrush);
}
//游戏循环
void GameCycle()
{
  RECT        rect;
  HDC         hDC;
  HWND        hWindow = g_pGame->GetWindow();

  // 记住上一个麦圈的位置
  int iXLast = g_rcRectangle.left + 
    (g_rcRectangle.right - g_rcRectangle.left) / 2;
  int iYLast = g_rcRectangle.top + 
    (g_rcRectangle.bottom - g_rcRectangle.top) / 2;

  // 随机更改新麦圈的大小和位置
  GetClientRect(g_pGame->GetWindow(), &rect);
  int iInflation = (rand() % 17) - 8; // 最多将大小增加或减少8
  InflateRect(&g_rcRectangle, iInflation, iInflation);
  OffsetRect(&g_rcRectangle, 
    rand() % (rect.right - rect.left) - g_rcRectangle.left,
    rand() % (rect.bottom - rect.top) - g_rcRectangle.top);

  // 绘制到新麦圈的直线
  hDC = GetDC(hWindow);
  HPEN hPen = CreatePen(PS_SOLID, 5, RGB(192, 192, 0)); // 浅黄色
  SelectObject(hDC, hPen);
  MoveToEx(hDC, iXLast, iYLast, NULL);
  LineTo(hDC,
    g_rcRectangle.left + (g_rcRectangle.right - g_rcRectangle.left) / 2,
    g_rcRectangle.top + (g_rcRectangle.bottom - g_rcRectangle.top) / 2);

  // 绘制新麦圈
  HBRUSH hBrush = CreateSolidBrush(RGB(192, 192, 0));   // 浅黄色
  SelectObject(hDC, hBrush);
  Ellipse(hDC, g_rcRectangle.left, g_rcRectangle.top, 
    g_rcRectangle.right, g_rcRectangle.bottom);
  ReleaseDC(hWindow, hDC);
  DeleteObject(hBrush);
  DeleteObject(hPen);
}

Crop Circles 源代码 解析

CropCircles.h

#pragma once

//-----------------------------------------------------------------
// 包含文件
//-----------------------------------------------------------------
#include 
#include "Resource.h"
#include "GameEngine.h"

//-----------------------------------------------------------------
// 全局变量
//-----------------------------------------------------------------
GameEngine* g_pGame;
RECT        g_rcRectangle;  //虽然这是一个矩形,但实际用来存储椭圆的信息

这个头文件,与我们上一篇文章中的Blizzard示例中看到的头文件的唯一区别就在于g_rcRectangle 全局变量的声明。这个矩形存储的是上一个圆的矩形信息,以便使用一条直线将其与下一个圆相连。最终结果就是所有圆看起来都是相互连接的。

其实从这个例子我们可以看,我们现在利用游戏引擎简化了游戏的大量工作。实际上Crop Circles实例只需要提供核心游戏函数的实现即可,游戏引擎我们还是沿用上一篇文章GameEngine。

GamePaint( ) 函数

//绘制游戏
void GamePaint(HDC hDC)
{
  // 绘制一个深黄色背景,作为麦圈的田地
  RECT    rect;
  GetClientRect(g_pGame->GetWindow(), &rect);
  HBRUSH hBrush = CreateSolidBrush(RGB(128, 128, 0));   // dark yellow color
  FillRect(hDC, &rect, hBrush);
  DeleteObject(hBrush);
}

FillRect( )函数用来绘制一个深黄色区域,它填充了整个客户区域,因为矩形是在GamePaint( )中绘制的,所以在重新绘制窗口时它不会丢失。

GameCycle( ) 函数

GameCycle( )函数是实际绘制麦田怪圈的地方。

//游戏循环
void GameCycle()
{
  RECT        rect;
  HDC         hDC;
  HWND        hWindow = g_pGame->GetWindow();

  // 记住上一个麦圈的位置
  int iXLast = g_rcRectangle.left + 
    (g_rcRectangle.right - g_rcRectangle.left) / 2;
  int iYLast = g_rcRectangle.top + 
    (g_rcRectangle.bottom - g_rcRectangle.top) / 2;

  // 随机更改新麦圈的大小和位置
  GetClientRect(g_pGame->GetWindow(), &rect);
  int iInflation = (rand() % 17) - 8; // 最多将大小增加或减少8
  InflateRect(&g_rcRectangle, iInflation, iInflation);
  OffsetRect(&g_rcRectangle, 
    rand() % (rect.right - rect.left) - g_rcRectangle.left,
    rand() % (rect.bottom - rect.top) - g_rcRectangle.top);

  // 绘制到新麦圈的直线
  hDC = GetDC(hWindow);
  HPEN hPen = CreatePen(PS_SOLID, 5, RGB(192, 192, 0)); // 浅黄色
  SelectObject(hDC, hPen);
  MoveToEx(hDC, iXLast, iYLast, NULL);
  LineTo(hDC,
    g_rcRectangle.left + (g_rcRectangle.right - g_rcRectangle.left) / 2,
    g_rcRectangle.top + (g_rcRectangle.bottom - g_rcRectangle.top) / 2);

  // 绘制新麦圈
  HBRUSH hBrush = CreateSolidBrush(RGB(192, 192, 0));   // 浅黄色
  SelectObject(hDC, hBrush);
  Ellipse(hDC, g_rcRectangle.left, g_rcRectangle.top, 
    g_rcRectangle.right, g_rcRectangle.bottom);
  ReleaseDC(hWindow, hDC);
  DeleteObject(hBrush);
  DeleteObject(hPen);
}

首先,它使用了两个Win32函数 InflateRect( )和OffsetRect( ),以便随机更改新的麦田怪圈的大小和位置。首先计算一个随机的膨胀值,其范围是-8~8。然后,以这个值作为基础,使用InflateRect( )函数缩小或放大圆的矩形区域。然后使用OffsetRect函数将矩形偏移到一个随机的距离(-9~9)。

在计算出麦田怪圈的大小和位置之后,GameCycle( )函数将继续绘制一条直线,以便连接上一个圆和新画的圆。在绘制这条线之前,创建了一个5像素宽的浅黄色画笔并将其选入设备环境。然后使用我们熟悉的MoveToEx( )和LineTo( )函数来绘制这条线。

在绘制完连接的新的麦田怪圈的直线之后,就可以确定它的新填充颜色了,这可以通过创建一个深黄色的纯色画刷来实现。然后,只需将该画刷选入设备环境并调用Ellipse( )函数来绘制麦田怪圈即可。在绘制完圆之后,释放设备环境并删除画刷和画笔。

源代码 下载

http://pan.baidu.com/s/1ge2Vzr1

你可能感兴趣的:(?.游戏编程入门,游戏编程入门)