显示关卡
搭建项目
- 将地图素材放在
Res/images/map目录中 - 向解决方案添加一个新项目
- 按第2节课配置好环境

分析关卡构成
从我们对游戏的直观感知而言,一个关卡中显而易见元素有:路,墙,玩家,箱子,目标点。 关卡设计就是将这些元素巧妙的组合起来,下面的数字组合是地图文件中一个关卡的定义。
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0]
[0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0]
[0,0,0,0,1,4,3,0,1,0,0,0,0,0,0,0]
[0,0,0,0,1,1,3,0,1,1,0,0,0,0,0,0]
[0,0,0,0,1,1,0,3,0,1,0,0,0,0,0,0]
[0,0,0,0,1,2,3,0,0,1,0,0,0,0,0,0]
[0,0,0,0,1,2,2,5,2,1,0,0,0,0,0,0]
[0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
这里,关卡有16行、16列共计256个数字。从定义中我们并没有直观的看到路、墙这些元素,但数字确实代表的就是这些元素。现在假设关卡中的一个0代表一块平坦的石块,再将所有数字想像为0,这样你将会得到一个巨大的广场。
脑海中的想像,从程序的角度说,就是整个关卡被渲染(绘制)后,得到了可见的场景(地图)。现在广场就是这个关卡的地图,它由256个小方块组成的,每个小方块称为一个瓦片。在我们的程序中,瓦片的大小与美术素材直接相关,这个素材就是block.png,这是一个35x35像素大小的图片,所以整个广场就是560x560像素大小。
对于这些信息,我们通过定义一些常量表达:
#define MAP_ROWS 16 // 地图行数
#define MAP_COLUMNS 16 // 地图列数
#define TILE_WIDTH 35 // 地面贴片元素宽度
#define TILE_HEIGHT 35 // 地面贴片元素宽度
#define SCENE_WIDTH TILE_WIDTH * MAP_COLUMNS // 窗口宽度
#define SCENE_HEIGHT TILE_HEIGHT * MAP_ROWS // 窗口高度
而对于地图,我们则使用一个二维数组来表达。
char g_map[MAP_ROWS][MAP_COLUMNS] =
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,
0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,0,
0,1,0,0,0,2,1,1,1,0,1,0,0,0,0,0,
0,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,
0,1,0,1,0,3,0,3,1,2,0,1,0,0,0,0,
0,1,0,1,0,0,5,0,0,1,0,1,0,0,0,0,
0,1,0,2,1,3,0,3,0,1,0,1,0,0,0,0,
0,1,1,0,0,0,0,1,0,1,0,1,1,1,0,0,
0,0,1,0,1,1,1,2,0,0,0,0,4,1,0,0,
0,0,1,0,0,0,0,0,1,1,0,0,0,1,0,0,
0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
现在我们列出数字与元素的对应关系,其中:
- 0: 路面
- 1: 障碍(墙面)
- 2: 目标点
- 3: 箱子
- 4: 玩家
- 5: 箱子 + 目标点
可以使用枚举来描述这些元素,这种有具体意义的名称会提高程序的可读性。
enum ELEMENT
{
ROAD, // 路基
WALL, // 障碍(墙面)
TARGET, // 目标点
BOX, // 柳木箱
PLAYER, // 玩家
GOAL, // 柳木箱 + 目标点
};
分析图片资源
美术资源并不一定是跟场景元素是一一对应的关系,如元素ROAD仅对应到Block.png,而PLAYER元素,却需要上下左右4个方位的图像资源。这些资源在整个游戏期间一直被使用,为此我们依然可以用枚举来定义一些名称以便显式的索引这些资源。
enum TEXTURE
{
TEX_ROAD, // 路基
TEX_WALL, // 障碍(墙面)
TEX_TARGET, // 目标点
TEX_BOX, // 柳木箱
TEX_PLAYER_UP, // 玩家上
TEX_PLAYER_DOWN, // 玩家下
TEX_PLAYER_LEFT, // 玩家左
TEX_PLAYER_RIGHT, // 玩家右
TEX_COUNT // 纹理数量
};
这里的一个技巧是TEX_COUNT的定义,它并不用于索引图像资源,而是定义了场景中所有图像资源的数量。
接下来,我们实现资源加载方法。
// 存储载入的所有图象
int g_textures[TEX_COUNT];
bool Load_Resource()
{
struct
{
int id;
const char *file;
}
img_map[] =
{
BLOCK, "block.png",
WALL, "wall.png",
BALL, "ball.png",
BOX, "box.png",
PLAYER_UP, "up.png",
PLAYER_DOWN, "down.png",
PLAYER_LEFT, "left.png",
PLAYER_RIGHT, "right.png"
};
char path[64];
for (int i = 0; i < TEX_COUNT; i++)
{
sprintf_s(path, 64, "./res/images/map/%s", img_map[i].file);
g_textures[img_map[i].id] = E2D_CreateTexture(path);
if (g_textures[img_map[i].id] == E2D_INVALID_RESID)
{
char message[128];
sprintf_s(message, 128, "Load resource: \"%s\" failed!\n", path);
E2D_MessageBox("PushBox", message, E2D_STOP);
return false;
}
}
return true;
}
注意这里加载图像的路径是./res/images/map/xxx.png,由于我们是通过一个解决方案管理多个项目的方式来构建整套项目,所以当通过VS运行程序时,其工作目录(当前目录)就是项目所在目录PushBox_02,加载的是PushBox_02/res/iamges/map/xxx.png,而实际的图像资源则在PushBox_02的上层目录(../res)中,因此加载会失败。程序之所以这么设计,是考虑到游戏最终发布时,程序文件与资源文件在同一层级,即以下面的目录结构组成。
PushBox
|---------- PushBox.exe
|__________ Res
|__________ Images
|__________ Map.data
为此,我们可以在VS中配置一下工作目录,将$(ProjectDir)变更为$(SolutDir),即将当前工作目录从原有的“项目所在文件夹”变更为“解决方案所在文件夹”。

显示关卡
图象加载后可以称为纹理。所有资源加载完成后,我们就可以通过渲染(绘制)来看看关卡的真实模样!
void Render()
{
for (int r = 0; r < MAP_ROWS; r++)
{
for (int c = 0; c < MAP_COLUMNS; c++)
{
int x = c * TILE_WIDTH;
int y = r * TILE_HEIGHT;
// 绘制路面
E2D_RenderTexture(g_textures[TEX_ROAD].tex, x, y, E2D_FLIP_NONE);
if (g_map[r][c] != ROAD)
{
int t = -1;
switch (g_map[r][c])
{
case ROAD:
t = TEX_ROAD;
break;
case WALL:
t = TEX_WALL;
break;
case TARGET:
t = TEX_TARGET;
break;
case BOX:
case GOAL:
t = TEX_BOX;
break;
case PLAYER:
t = TEX_PLAYER_DOWN;
break;
}
if (t != -1)
{
E2D_RenderTexture(g_textures[t].tex, x, y, E2D_FLIP_NONE);
}
}
}
}
}

可以看到,结果并不理想,但仍有几点要说明的。
首先,无论地图中当前元素是什么,我们都绘制了路面,然后在路面上再绘制元素本身,这是因为路面Block.png会铺满整个瓦片,但对于障碍Wall.png却因为有镂空部分而会显露出窗口的黑色背景。

另一个问题是,所有的图象元素好像被压缩到一个瓦片空间中了,致使图象显示不完整。实际上是因为绘制时,我们以当前瓦片的左上角为起点,绘制出了整个图像,即使图像大小大于瓦片大小。但由于瓦片是紧密排列的,下一个元素绘制时,会遮住上一个图像的右边超出部分,下一行的元素则会遮住上一行元素的底部超出部分,因此造成了这样的视觉表现。

观察一下整个场景及所有的图像资源,它们带有仰视效果,因此我们可以通过让所有的图片相对于瓦片,左右居中底端对齐来达到目的。为此我们需要一些额外信息来描述资源的绘制,这通过TEXINFO结构来表达。
// 纹理信息
typedef struct tagTEXINFO
{
int tex; // 纹理
int dx; // x向偏移
int dy; // y向偏移
} TEXINFO;
TEXINFO g_textures[TEX_COUNT];
然后,我们在载入元素时,计算这些值。
bool Load_Resource()
{
//...
for (int i = 0; i < TEX_COUNT; i++)
{
char path[64];
sprintf_s(path, 64, "./res/images/map/%s", img_map[i].file);
int eid = img_map[i].id;
int tex = E2D_CreateTexture(path);
if (tex != E2D_INVALID_RESID)
{
int w, h;
E2D_GetTextureSize(tex, &w, &h);
g_textures[eid].tex = tex;
g_textures[eid].dx = (w - TILE_WIDTH) / 2; // 水平方向使纹理居中
g_textures[eid].dy = (h - TILE_HEIGHT); // 垂直方向纹理与当前行底端对齐
}
}
return true;
}
相应的,渲染函数也要作出更改;至此,一切都美好如初了。
void Render()
{
for (int r = 0; r < MAP_ROWS; r++)
{
for (int c = 0; c < MAP_COLUMNS; c++)
{
int x = c * TILE_WIDTH;
int y = r * TILE_HEIGHT;
E2D_RenderTexture(g_textures[TEX_ROAD].tex, x, y, E2D_FLIP_NONE);
if (g_map[r][c] != ROAD)
{
const TEXINFO *t = NULL;
switch (g_map[r][c])
{
case ROAD:
t = &g_textures[TEX_ROAD];
break;
case WALL:
t = &g_textures[TEX_WALL];
break;
case TARGET:
t = &g_textures[TEX_TARGET];
break;
case BOX:
case GOAL:
t = &g_textures[TEX_BOX];
break;
case PLAYER:
t = &g_textures[TEX_PLAYER_DOWN];
break;
}
if (t != NULL)
{
E2D_RenderTexture(t->tex, x - t->dx, y - t->dy, E2D_FLIP_NONE);
}
}
}
}
}
