平滑卷轴效果
对于J2ME框架下的手机游戏程序的开发,其地图滚动的重绘有多种算法,由于手机性能的限制和开发周期等其他非技术条件,需要根据情况灵活选择所需的技术。但在及其苛刻条件下,如系统CPU资源不足,地图块尺寸较小等,会造成屏幕闪耀,帧数过低等情况,严重影响到游戏体验。在开发中如此类问题无法绕过以及避免(指通过修改策划方案,以及程序使用的技术框架),则需要考虑使用地图缓冲绘制技术,卡马克卷轴就是一种最经典的地图缓冲绘制技术。可有效的改善在地图绘制中的屏幕闪耀,帧数过低等情况。
平滑卷轴效果
点子是卡马克提出来的。他正在尝试一项编程上的突破:让游戏的世界不再局限于屏幕的边界——所谓的“卷轴效果”。街机是这项技术的样板,早期街机游戏的移动也是局限在一个静态的屏幕内,譬如《乒乓》,用于把球击来击去的球拍只能在屏幕的底部和顶部之间移动,再譬如《吃豆子》里的迷宫,也是局限于屏幕那么大,还有《太空入侵者》,玩家控制的飞船只能在屏幕下方左右移动,外星飞船们则是从屏幕顶部涌现。所有这些游戏都把玩家局限在那一小方天地里,缺乏一种宽广的可延伸的感觉,这种状况一直持续到1980年,在那一年,威廉斯公司推出了一款名叫《防御者》(Defender)的街机游戏,这是第一个流行的卷轴游戏,在这个科幻射击游戏里,玩家不再受屏幕大小的限制,他操纵飞船在行星的表面水平移动,一路上击落敌机营救人质,屏幕上的一幅小地图显示着玩家在整个世界里的当前位置。如果把地图扩展为正常尺寸,这个世界大概有三个半屏幕那么大。与其他街机比起来,《防御者》显得宏大得多,玩家就像进入了一个更为广阔的虚拟空间。它很快变得和《太空入侵者》一样流行,还胜过《吃豆子》成为了业界的年度游戏。无数的卷轴游戏随之出现,到1989年的时候,卷轴技术已经是新游戏的一项必不可少的标准,这其中最成功的莫过于任天堂红白机上的《超级马里奥兄弟3》(Super Mario Brothers 3)。
但在那时,1990年9月,还没有人研究出如何在PC上实现卷轴效果,大家都用一些蹩脚的技巧来让玩家觉得游戏的世界比屏幕要大,譬如当玩家移动到屏幕最右边的时候,游戏会停顿一会,然后右边的场景出现在屏幕上。部分原因是PC的性能还很差,无论是街机还是苹果机,或是任天堂那样的家用机都比PC强不少。而卡马克下定决心要找出一种办法来在PC上实现像《防御者》或《超级马里奥》那样的平滑卷轴效果。
《玩家之刃》的下一个游戏就要朝着这个方向走。当大伙聚在一起讨论的时候,卡马克给他们演示了他最新的成果,他已经可以让屏幕上的内容平滑地往下方移动,和那些成熟的卷轴游戏比起来,这项技术还很粗糙,它就像是一条传送带,图像按照固定的速度和路线落下,玩家还不可能随心所欲地在里面畅游,那就像是拖动演员背后的舞台布景。
罗梅洛这个博览过几乎每一款PC游戏的玩家,没有见过这种效果。这对他们而言是一个成为先行者的好机会。他们给游戏命名为《搜捕》(Slordax),一个简单的飞船射击游戏,就像《太空入侵者》或《小蜜蜂》(Galaga)一样。
(此为一种实现方式的具体介绍)
操作贴片引擎时,大型的地图需要卷轴以便玩家可以看到整个地图,具体说,在绘制地图时尝试去改变坐标,贴片引擎就会产生一个急促的运动。为了提高引擎的视觉质量,需要使用一种称之为平滑卷轴(smooth scrolling)的技术来使运动平滑地进行。
为了实现平滑卷轴,将贴片绘制的地图想象成一个很大的位图。在位图中的每个像素都有它自己的一对坐标,即所谓的地图的精细坐标,代表贴片像素的每个分组被赋予它自己的地图坐标集,如下图所示:
举个例子,如果贴片是16 x 16像素大小,同时地图数组为10 x 10,当完全渲染时,地图将会为160 x 160像素大小(这就意味着地图有一个分辨率为160 x 160 的精细坐标)。
创建一个地图类
为了使游戏保持运行的平滑,首先需要对每一帧所绘制的自由浮动贴片的数量(子画面)进行限制,一个宏定义将出色地完成这个工作,它会通知地图类在每一帧中绘制了多少子画面:
#define MAX_OBJECTS 1024
每个地图类的实例可以存储大量的层次(甚至超过一百万个),将每个层次的贴片数据存储到一个数组_map_info里。因为地图的尺寸大小一旦被创建,将是固定不变的,可以通过计算在_map_info数组里的当前位移,并利用一个指针对每个层次的贴片数据进行读取或写入。
来看看MAP类的定义:
#define MAX_OBJECTS 1024
typedef struct OBJECT_INFO
{
long x_pos, y_pos;
char tile_index;
} *OBJECT_INFO_PTR;
//=========================================================================================
// This class encapsulate 2D map draw.
//=========================================================================================
typedef class MAP
{
public:
MAP();
~MAP();
// function to create and free a map class
BOOL create(long num_layers, long map_column, long map_row);
void free();
// function to set a map''s layer data
BOOL set_map_layer_data(long layer_index, char* layer_data);
// function to clear and add an object to list
void clear_object_list();
BOOL add_object(long x_pos, long y_pos, char tile_index);
char* get_ptr(long layer_index); // get pointer to map array
long get_map_column(); // get column of map
long get_map_row(); // get row of map
// assign TILE object to use for drawing map tiles
BOOL use_tile(TILE_PTR tile);
// Render map using specified top-left map coordinates,
// as well as number of columns and rows to draw, plus layer used to draw objects.
BOOL render(long pos_x, long pos_y,
long num_rows, long num_columns,
long object_layer,
D3DCOLOR color = 0xFFFFFFFF,
float scale_x = 1.0f, float scale_y = 1.0f);
private:
long _map_column; // column of map
long _map_row; // row of map
long _per_layer_size; // size of per map
long _num_layers; // number of layers
char* _map_info; // array for tile informarion
TILE_PTR _tile; // pointer to TILE object
long _num_objects_to_draw; // number of object need to be drawed
OBJECT_INFO _objects_info[MAX_OBJECTS]; // object information array
} *MAP_PTR;
实现:
/*************************************************************************
PURPOSE:
Implement for 2D map.
*************************************************************************/
#include "core_global.h"
#include "tile.h"
#include "map.h"
//----------------------------------------------------------------------------------
// Constructor, zero member data.
//----------------------------------------------------------------------------------
MAP::MAP()
{
memset(this, 0, sizeof(*this));
}
//----------------------------------------------------------------------------------
// Destructor, release allocated resources.
//----------------------------------------------------------------------------------
MAP::~MAP()
{
free();
}
//----------------------------------------------------------------------------------
// Release allocated resources.
//----------------------------------------------------------------------------------
void MAP::free()
{
// free map information array
delete[] _map_info;
_map_info = NULL;
_map_column = _map_row = 0;
_num_layers = 0;
}
//----------------------------------------------------------------------------------
// Create map object.
//----------------------------------------------------------------------------------
BOOL MAP::create(long num_layers, long map_column, long map_row)
{
// free a prior map
free();
// save number of layers, map column and row.
_num_layers = num_layers;
_map_column = map_column;
_map_row = map_row;
_per_layer_size = map_column * map_row;
long total_map_size = num_layers * _per_layer_size;
// allocate map data memory
if((_map_info = new char[total_map_size]) == NULL)
return FALSE;
// clear it out
ZeroMemory(_map_info, total_map_size);
// reset number of objexts to draw
_num_objects_to_draw = 0;
return TRUE;
}
//----------------------------------------------------------------------------------
// Set map data.
//----------------------------------------------------------------------------------
BOOL MAP::set_map_layer_data(long layer_index, char* layer_data)
{
// error checking
if(layer_index >= _num_layers)
return FALSE;
// copy over data
memcpy(&_map_info[layer_index * _per_layer_size], layer_data, _per_layer_size);
return TRUE;
}
//----------------------------------------------------------------------------------
// Clear object list which need to be drawed.
//----------------------------------------------------------------------------------
void MAP::clear_object_list()
{
_num_objects_to_draw = 0;
}
//----------------------------------------------------------------------------------
// Add object to object list.
//----------------------------------------------------------------------------------
BOOL MAP::add_object(long x_pos, long y_pos, char tile_index)
{
if(_num_objects_to_draw < MAX_OBJECTS)
{
_objects_info[_num_objects_to_draw].x_pos = x_pos;
_objects_info[_num_objects_to_draw].y_pos = y_pos;
_objects_info[_num_objects_to_draw].tile_index = tile_index;
_num_objects_to_draw++;
return TRUE;
}
return FALSE;
}
//----------------------------------------------------------------------------------
// Return pointer to specfied layer map data.
//----------------------------------------------------------------------------------
char* MAP::get_ptr(long layer_index)
{
if(layer_index >= _num_layers)
return NULL;
return &_map_info[layer_index * _per_layer_size];
}
//----------------------------------------------------------------------------------
// Return map columns.
//----------------------------------------------------------------------------------
long MAP::get_map_column()
{
return _map_column;
}
//----------------------------------------------------------------------------------
// Return map rows.
//----------------------------------------------------------------------------------
long MAP::get_map_row()
{
return _map_row;
}
//----------------------------------------------------------------------------------
// Set tile to map.
//----------------------------------------------------------------------------------
BOOL MAP::use_tile(TILE_PTR tile)
{
if((_tile = tile) == NULL)
return FALSE;
return TRUE;
}
//----------------------------------------------------------------------------------
// Render map.
//----------------------------------------------------------------------------------
BOOL MAP::render(long pos_x, long pos_y,
long num_rows, long num_columns,
long object_layer,
D3DCOLOR color,
float scale_x, float scale_y)
{
// error checking
if(_map_info == NULL || _tile == NULL)
return FALSE;
long tile_width = _tile->get_tile_width(0);
long tile_height = _tile->get_tile_height(0);
// calculate smooth scrolling variables
long map_x = pos_x / tile_width;
long map_y = pos_y / tile_height;
long off_x = pos_x % tile_width;
long off_y = pos_y % tile_height;
// loop through each layer
for(long layer = 0; layer < _num_layers; layer++)
{
// get a pointer to the map data
char* map_ptr = &_map_info[layer * _per_layer_size];
// loop for each row and column
for(long row = 0; row < num_rows+1; row++)
{
for(long column = 0; column < num_columns+1; column++)
{
// get the tile index to draw
char tile_index = map_ptr[(row + map_y) * _map_column + column + map_x];
long screen_x = column * tile_width - off_x;
long screen_y = row * tile_height - off_y;
// draw tile
_tile->draw_tile(0, tile_index, (DWORD)screen_x, (DWORD)screen_y, color, scale_x, scale_y);
}
}
// draw objects if on object layer
if(layer == object_layer)
{
for(long i = 0; i < _num_objects_to_draw; i++)
{
_tile->draw_tile(0, _objects_info[i].tile_index,
_objects_info[i].x_pos - off_x, _objects_info[i].y_pos - off_y,
color, scale_x, scale_y);
}
}
}
return TRUE;
}
我们接着编写两个例子来测试,第一个例子演示了基本贴片技术的使用,第二个例子演示了平滑卷轴的使用。 来看看第一个例子:
下载源码和工程
/*****************************************************************************
PURPOSE:
Test for class TILE and MAP.
*****************************************************************************/
#include "Core_Global.h"
#include "tile.h"
#include "map.h"
#pragma warning(disable : 4996)
class APP : public APPLICATION
{
public:
APP()
{
_width = 384;
_height = 384;
_style = WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU;
strcpy(_class_name, "scale_tile_class");
strcpy(_caption, "scale tile demo");
}
BOOL init()
{
// initialize the graphics device and set display mode
if(! _graphics.init())
return FALSE;
if(! _graphics.set_mode(get_hwnd() , TRUE, FALSE))
return FALSE;
// create and load the tile set
if(! _tile.create(&_graphics, 1))
return FALSE;
if(! _tile.load_texture(0, "tiles.bmp", 64, 64))
{
err_msg_box("load texture failed.");
return FALSE;
}
// create and set the map
char map_data[3][3] = {
{ 0, 1, 0 },
{ 2, 2, 2 },
{ 1, 2, 3 }
};
_map.create(1, 3, 3);
_map.set_map_layer_data(0, (char*) &map_data);
_map.use_tile(&_tile);
return TRUE;
}
BOOL APP::frame()
{
// calculate elapsed time
static DWORD s_last_time = timeGetTime();
DWORD now_time = timeGetTime();
DWORD elapsed_time = now_time - s_last_time;
// frame lock to 30ms per frame
if(elapsed_time < 30)
return TRUE;
s_last_time = now_time;
if(_graphics.begin_scene())
{
if(_graphics.begin_sprite())
{
D3DCOLOR color;
static uchar s_red = 0, s_green = 0, s_blue = 0;
static BOOL s_increment_color = TRUE;
if(s_increment_color)
{
color = D3DCOLOR_RGBA(s_red++, s_green++, s_blue++, 255);
if(s_red >= 255)
s_increment_color = FALSE;
}
else
{
color = D3DCOLOR_RGBA(s_red--, s_green--, s_blue--, 255);
if(s_red <= 0)
s_increment_color = TRUE;
}
// draw the map
_map.render(0, 0, 3, 3, 0, color, 2.0f, 2.0f);
_graphics.end_sprite();
}
_graphics.end_scene();
_graphics.display();
}
return TRUE;
}
BOOL shutdown()
{
return TRUE;
}
private:
GRAPHICS _graphics;
TILE _tile;
MAP _map;
};
int PASCAL WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
APP app;
return app.run();
}
该程序淡入淡出地改变贴图的颜色,截图如下:
接着来看第二个例子:
下载源码和工程
/*****************************************************************************
PURPOSE:
Test for class TILE and MAP.
*****************************************************************************/
#include "Core_Global.h"
#include "tile.h"
#include "map.h"
#pragma warning(disable : 4996)
#define TILE_WIDTH 64
#define TILE_HEIGHT 64
#define MAP_COLUMNS 16
#define MAP_ROWS 16
#define TOTAL_MAP_SIZE 1024
class APP : public APPLICATION
{
public:
APP()
{
_width = 640;
_height = 480;
_num_columns_to_draw = _width / TILE_WIDTH;
_num_rows_to_draw = _height / TILE_HEIGHT;
_max_move_width = TOTAL_MAP_SIZE - _width;
_max_move_height = TOTAL_MAP_SIZE - _height;
_style = WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU;
strcpy(_class_name, "map class");
strcpy(_caption, "map demo");
}
BOOL init()
{
// initialize the graphics device and set display mode
if(! _graphics.init())
return FALSE;
if(! _graphics.set_mode(get_hwnd(), TRUE, FALSE))
return FALSE;
// create and load the tile set
if(! _tile.create(&_graphics, 1))
return FALSE;
if(! _tile.load_texture(0, "tiles.bmp", TILE_WIDTH, TILE_HEIGHT))
{
err_msg_box("load texture failed.");
return FALSE;
}
// create and set the map
char map_data[MAP_ROWS][MAP_COLUMNS] = {
{ 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 1, 2, 2, 1, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 0, 2, 0 },
{ 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 0, 0, 0, 2, 0 },
{ 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0 },
{ 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0 },
{ 3, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 3, 0, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 0, 0, 2, 2, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 2, 2, 1, 1, 0, 0, 2, 2, 0, 0, 2, 2, 2, 2, 0 },
{ 0, 1, 2, 2, 2, 0, 0, 2, 2, 0, 0, 2, 1, 1, 2, 0 },
{ 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0 },
{ 0, 0, 2, 2, 2, 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 }
};
_map.create(1, MAP_COLUMNS, MAP_ROWS);
_map.set_map_layer_data(0, (char*) &map_data);
_map.use_tile(&_tile);
return TRUE;
}
BOOL APP::frame()
{
static long s_x_pos = 0, s_y_pos = 0;
// calculate elapsed time
static DWORD s_last_time = timeGetTime();
DWORD now_time = timeGetTime();
DWORD elapsed_time = now_time - s_last_time;
// frame lock to 33ms per frame
if(elapsed_time < 33)
return TRUE;
s_last_time = now_time;
if(_graphics.begin_scene())
{
if(_graphics.begin_sprite())
{
// draw the map
_map.render(s_x_pos, s_y_pos, _num_rows_to_draw, _num_columns_to_draw, 0, 0xFFFFFFFF, 1.0f, 1.0f);
// press arrows to scroll map around
if(GetAsyncKeyState(VK_LEFT)) s_x_pos -= 8;
if(GetAsyncKeyState(VK_RIGHT)) s_x_pos += 8;
if(GetAsyncKeyState(VK_UP)) s_y_pos -= 8;
if(GetAsyncKeyState(VK_DOWN)) s_y_pos += 8;
// bounds check map coordinates
if(s_x_pos < 0)
s_x_pos = 0;
if(s_x_pos > _max_move_width)
s_x_pos = _max_move_width;
if(s_y_pos < 0)
s_y_pos = 0;
if(s_y_pos > _max_move_height)
s_y_pos = _max_move_height;
_graphics.end_sprite();
}
_graphics.end_scene();
_graphics.display();
}
return TRUE;
}
BOOL shutdown()
{
return TRUE;
}
private:
GRAPHICS _graphics;
TILE _tile;
MAP _map;
long _num_columns_to_draw;
long _num_rows_to_draw;
long _max_move_width;
long _max_move_height;
};
int PASCAL WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
APP app;
return app.run();
}
该程序展示了平滑卷轴技术的使用,用上下左右键进行控制,截图如下: