游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

如何制作一款2D平台游戏之区块与图层

发布时间:2012-02-08 10:51:44 Tags:,,,

作者:Paul Firth

灵感

孩童时期,我总是很喜欢玩Taito旗下的《The Newzealand Story》和《Rainbow Islands》。而近来,让我深深着迷的平台游戏是《Generic》,所以我便决定写下制作一款平台游戏的过程(如下图所示)。

2d game(from wildbunny)

2d game(from wildbunny)

背景

首先,你会注意到这些游戏的背景中存在多个视差图层,通过一些区块我们很容易制作出这种效果。

背景(from wildbunny)

背景(from wildbunny)

中景(from wildbunny)

中景(from wildbunny)

观察这些区块为何会有不同大小?这么做能够避免在摄像机移动的过程中有过多缝隙线性排列在一起。

而这个阶段的最难点便是如何在将每个区块叠加起来的同时,让这些设计无缝隙地结合在一起。更关键的是,我是程序员而不是美工,根本就不擅长这种工作!而我将在后来谈论到的如何创造游戏区块中解释这一点。

跟随摄像镜头移动的区块

为了创造一个看起来拥有无限区块的背景,我们将通过渲染器铺设区块,就像电影《Wallace and Gromit》这样,随着摄像镜头在一个水平面上的移动铺设区块。

Gromit铺设车轨(from wildbunny)

Gromit铺设车轨(from wildbunny)

上图的Gromit坐在飞速前进的火车上铺设着轨道;而如果你把Gromit比作渲染器而火车比作摄像镜头,你就很容易理解我的意思。而不同之处在于,在火车的另一端我们需要另一个Gromit朝着不同的方向在火车通过的位置上拾起轨道并传递给最前方的Gromit!

为了保持屏幕画面的连贯,我们需要一个缓存,即在X轴上,N=屏幕宽度/图层宽度+2,在Y轴上,N=屏幕高度/图层高度+2。

图1(from wildbunny)

图1(from wildbunny)

图1的蓝色屏幕将在一系列网格区块中移动;绿色区块是移动过程中添加于前方的内容,而红色区块则是将从尾端移除以便循环使用的部分。以下是所使用的代码:

package Code.Graphics
{
import flash.display.*;
import Code.Geometry.*;
import Code.Maths.*;
import Code.Constants;
import Code.Platformer;

public class TileRenderer
{
private var m_tiles:Vector.<MovieClip>;
private var m_numX:int;
private var m_numY:int;
private var m_tileWidth:int;
private var m_tileHeight:int;
private var m_camera:Camera;
private var m_zDepth:Number;

private var m_tempAabb:AABB;

/// <summary>
/// Constructor
/// </summary>
public function TileRenderer( tileType:Class, width:int, height:int, camera:Camera, stage:Platformer, zDepth:Number )
{
m_tileWidth = width;
m_tileHeight = height;
m_camera = camera;
m_zDepth = zDepth;

m_tempAabb = new AABB( );

m_numX = Constants.kScreenDimensions.m_x/width+2;
m_numY = Constants.kScreenDimensions.m_y/height+2;

m_tiles = new Vector.<MovieClip>( m_numX*m_numY );

// run though and create all the tiles we need, this fuction takes
// a closeure which actually does the work
PositionLogic( function( index:int, xCoord:int, yCoord:int ):void
{
m_tiles[index] = new tileType( );
m_tiles[index].x = xCoord;
m_tiles[index].y = yCoord;
m_tiles[index].cacheAsBitmap = true;

// add the tile and send it to the back
stage.addChild( m_tiles[index] );
stage.setChildIndex( m_tiles[index], 0 );
});
}

/// <summary>
/// This function runs through and computes the position of each tile – it takes a closeure
/// so you can insert your own inner logic to run at each location
/// </summary>
private function PositionLogic( action:Function ):void
{
m_camera.GetWorldSpaceOnScreenAABB( m_tempAabb );

var screenTopLeft:Vector2 = m_tempAabb.m_TopLeft;

// stop the background from crawling around due to pixel trucation
screenTopLeft.RoundTo( );

// calculate the top left of the screen, scaled for z depth
var scaledTopLeft:Vector2 = screenTopLeft.MulScalar( 1/m_zDepth );
var tileX:int = Math.floor(scaledTopLeft.m_x / m_tileWidth);
var tileY:int = Math.floor(scaledTopLeft.m_y / m_tileHeight);

// this offset corrects for translation caused by the divide by z
var offset:Vector2 = scaledTopLeft.Sub( screenTopLeft );

// get the starting tile coords
var startX:int = tileX*m_tileWidth – offset.m_x;
var startY:int = tileY*m_tileHeight – offset.m_y;
var xCoord:int = startX;
var yCoord:int = startY;

// run though and call the closure for each tile position
for ( var j:int = 0; j<m_numY; j++ )
{
xCoord = startX;
for ( var i:int = 0; i<m_numX; i++ )
{
var index:int = j*m_numX+i;

action(index, xCoord, yCoord);

xCoord += m_tileWidth;
}
yCoord += m_tileHeight;
}
}

/// <summary>
/// Update all the tiles to the new coordinates based on the camera’s new position
/// </summary>
public function Update( ):void
{
PositionLogic( function( index:int, xCoord:int, yCoord:int ):void
{
m_tiles[index].x = xCoord;
m_tiles[index].y = yCoord;
});
}
}
}

关于使用这一技巧需要注意的问题:

为了避免区块在屏幕上分布不均衡,我们必须确保屏幕左上方的参照点能够在达到整数像素的最高点时即时调头:

// stop the background from crawling around due to pixel trucation
screenTopLeft.RoundTo( );

RoundTo()的功能是强调在最近的整数区间内调头。

除以区块中的z值能够快速得到视差,然后再纠正区块的位置使它们能够从屏幕上的正确位置再次启动:

var scaledTopLeft:Vector2 = screenTopLeft.MulScalar( 1/m_zDepth );
var tileX:int = Math.floor(scaledTopLeft.m_x / m_tileWidth);
var tileY:int = Math.floor(scaledTopLeft.m_y / m_tileHeight);
var offset:Vector2 = scaledTopLeft.Sub( screenTopLeft );

上述代码是什么意思?我们正在处理的是明确屏幕左上方的位置,通过除以区块分辨率而获得每个轴面所需的区块数量,并最终整合出结果。让我们再次回到Gromit的例子中,这么做让Grommit无法将每一个火车轨道铺设在最后方或者最前面,他只能一个接一个地铺设轨道,这就意味此时所出现的区块不存在任何视觉差异。最后一部分是校正除以区块层z深度值时所出现的偏移状况——这可以让区块始于屏幕的左上方。

图2(from wildbunny)

图2(from wildbunny)

图2是游戏的截图,从中你可以看到摄像头区域外部的区块设置。

区块

区块是任何老式平台游戏的中心,它构成了游戏中的静态世界部分。游戏中的所有区块都具有固定的大小,即64×64像素,但是你也可以选择最适合的区块大小。

区块具有多种功能,因为它们不仅能够在构建每个关卡时创造出建筑模块,而且也能够解决玩家在面对游戏关卡时所遇到几何学问题。除此之外我们也可以使用区块去安置敌人以及其它能够影响特定AI行为的无形标记。

tile Set(from wildbunny)

图3:游戏的完整区块设置(from wildbunny)

上图的每个区块都具有一个代表性的特定整数,如此从o开始,从左向右,从上至下递增。在特定关卡中的所有整数集合可称为地图(Map)。而地图总是呈现出矩形的形状。

其代码如下:

// map for the level
private var m_map:Vector.<uint> = Vector.<uint>
(
[
04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,
04,52,19,51,00,00,00,52,19,51,00,52,19,51,00,19,51,04,
04,17,15,18,00,00,00,17,15,18,35,17,15,49,51,15,18,04,
04,17,15,49,51,15,00,50,15,18,34,17,15,15,49,15,18,04,
04,00,48,15,49,15,50,15,47,29,34,17,15,48,15,15,18,04,
04,00,00,48,15,15,15,47,29,00,34,17,15,00,48,15,18,04,
04,00,00,00,16,16,16,29,00,00,00,00,16,00,31,16,29,04,
04,00,42,42,42,42,42,42,42,42,42,42,42,42,42,42,00,04,
04,00,13,00,00,00,00,00,00,00,00,00,00,00,00,00,00,04,
04,02,02,02,02,02,02,02,02,02,02,02,02,02,02,02,02,04
]
);

我能够通过一个较大的枚举类型判断哪些整数与哪些区块对应:

public class eTileTypes
{
// foreground
static public const kEmpty:uint = 0;
static public const kEarthGrassLeft:uint = 1;
static public const kEarthGrass:uint = 2;
static public const kEarthGrassRight:uint = 3;
static public const kEarthMid:uint = 4;
static public const kEarthTop:uint = 5;
static public const kEarthRight:uint = 6;
static public const kEarthLeft:uint = 7;
static public const kEarthBottom:uint = 8;

}

在创建关卡期间,我浏览了整个地图,挑选出每个整数并构造了每一个能够与之对应的区块。游戏世界中每个区块的位置也就是它们在地图中的位置。

区块坐标系统

游戏世界的总体尺寸是64 x Nx, 64 x Ny,在这里Nx和Ny是指地图中X轴和Y轴的区块数量。同时我也添加了一个偏移量,以在地图中间的像素坐标轴上设置o,o点。

区块坐标主要反映地图中的区块指数,而游戏世界坐标则与像素有关。并且区块坐标和游戏世界坐标之间的相互转换也非常普遍:

/// <summary>
/// calculate the position of a tile: 0,0 maps to Constants.kWorldHalfExtents
/// </summary>
static public function TileCoordsToWorldX( i:int ):Number
{
return i*Constants.kTileSize – Constants.kWorldHalfExtents.m_x;
}

/// <summary>
/// go from world coordinates to tile coordinates
/// </summary>
static public function WorldCoordsToTileX( worldX:Number ):int
{
return ( worldX+Constants.kWorldHalfExtents.m_x )/Constants.kTileSize;
}

其它坐标轴也有与之相应的转换版本。

区块状态

如何设置区块对游戏关卡中所需要的区块数量具有重要影响。如果你还不甚熟悉其操作方法,便会觉得反复连接区块是一件非常乏味的工作。

cheese Design(from wildbunny)

cheese Design(from wildbunny)

让我们以“奶酪区块”为例(我本来想把它设计得像石头,但最后却变成了奶酪形状)。它本身看来已经很精密,但是当你多次将它们拼凑在一起时,将会出现一些明显的空隙:

cheese Design Tile(from wildbunny)

cheese Design Tile(from wildbunny)

因为最初设计的空间太小了,并不能安插较大的空洞。而解决方法便是一个接一个切掉空洞的另外一边,就像这样:

cheese Side Bits(from wildbunny)

cheese Side Bits(from wildbunny)

结合最初的图像我们便能够获得:

cheese Design Final(from wildbunny)

cheese Design Final(from wildbunny)

这样的区块让人看起来更加舒服:

cheese Design Final Tile(from wildbunny)

cheese Design Final Tile(from wildbunny)

但是,在设计每个图块的终端片(游戏邦注:这些终端片是指每片奶酷或者泥土的最后一端)时会产生一个重要的副作用,它会让区块看起来不再是那么方方矩钜。

chese With Ends(from wildbunny)

chese With Ends(from wildbunny)

终端片将能够打破原本方形的区块,但是这样也存在相应的弊端。但是因为我选择去拼凑奶酷的角落,也就意味着我不只需要终端片,同时也需要弯角片以填充上图你们所看到的缺口。

我们不但要在区块设计中完成繁重任务,在绘制图像的过程中也要投入更多精力,因为我们总是希望能够绘制更多区块以更好地整合设计。

绘图

因为不想针对每个区块类型手动输入各个整数,我使用了地图编辑器Mappy(这是一个免费的插件,并且具有脚本文件能够输出AS3数据)。

Mappy中的一个关卡截图如下:

mappy Level(from wildbunny)

mappy Level(from wildbunny)

你可以在截图右边看到一个区块,以及当你将其绘制在左边画面上时它的呈现方式。

图层

如果你仔细观察游戏,将会发现每个关卡的区块并不只有一个图层。实际上共有3个图层!

*主前景图层,包括玩家所接触的内容

*中间图层,呈现角色和特殊控制方式

*背景图层,玩家可视但却不能改变的内容

背景(from wildbunny)

背景图层(from wildbunny)

我总是尽可能地让背景区块暗于前景区块,避免玩家混淆不同区块。

中景(from wildbunny)

中景图层(from wildbunny)

瓢虫两边的箭头标志用于控制它们的移动。

前景图层(from wildbunny)

前景图层(from wildbunny)

前景是玩家真正接触到的内容——你能够碰触这个图层上的所有内容。

layers All(from wildbunny)

结合在一起的所有图层(from wildbunny)

将三个图层结合在一起便得到上图的画面。

之所以设置了三个图层是在创造关卡时更有灵活性;如果你只使用一个图层,便无法绘制出玩家在阶梯上的场景,因为你只有一个区块的位置无法同时容纳玩家和阶梯。

它同时还可以添加预先设定的深度排列次序,即背景区块总是置于所有内容之后,在它上面就是中间区块(尽管大部分属于非可视内容),而前景区块则总是位于最上层。

拥有多个图层并不会增加系统的复杂性,而能提供更多灵活性。

摄像镜头

我们使用区块时最重要的一点是,确保玩家在不同关卡间移动时,区块之间不存在任何视觉断层。这种断层将会破坏玩家在持续性游戏世界中体验。为了避免这种情况,我们就必须确保摄像镜头总是能够瞄准整个像素的范围。

为了做到这点我添加了以下摄像镜头系统代码:

// this is essential to stop cracks appearing between tiles as we scroll around – because cacheToBitmap means
// sprites can only be positioned on whole pixel boundaries, sub-pixel camera movements cause gaps to appear.
m_worldToScreen.tx = Math.floor( m_worldToScreen.tx+0.5 );
m_worldToScreen.ty = Math.floor( m_worldToScreen.ty+0.5 );

m_worldToScreen是附属于游戏精灵的矩阵,可以让游戏世界随着玩家的移动而变化,从而让玩家感觉自己真的在游戏世界中前行。

游戏邦注:原文发表于2011年12月11日,所涉事件和数据均以当时为准。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How to make a 2d Platform Game – part 1

Posted on December 11, 2011 by Paul Firth

Hello, and welcome back to my blog!

Its been a long time coming, but finally I’ve managed to get some time to work on the blog again…

In this first instalment of a series blog articles, I’m going to be explaining step by step how you make this 2d platform game:

Click the game to give it focus… Apologies for the programmer art, and my level design (not my best qualities!)

The language is actionscript 3.0, but the techniques are applicable to all languages.

Inspiration

As a kid I always used to love playing The Newzealand Story and Rainbow Islands by Taito

The NewZealand Story

Rainbow Islands

And of late my love of platformers was rekindled by this awesome work in progress Generic:

So I decided to write about the process of making one – you’ll see influences from all three of these games in my bad graphics!

The background

Ok, one of the first things you will notice is that the game has a couple of layers of parallax in the background; this was relatively easy to achieve with the aid of a couple of tiles.

Background Tile

Mid-ground tile

Notice how the tiles are different sizes? This is important to stop the seams lining up too much as the camera moves about.

The most difficult thing was actually getting these designs to tile seamlessly when stacked next to each other, but that’s because I’m a programmer not an artist!

Anyway, I’ll cover how I did that later when I talk about creating the tile set for the game.

Tiles follow camera

In order to get a seemingly infinite tiling background, the tiles are laid down by the renderer as they are needed, Wallace and Gromit style, as the camera moves around the level.

Gromit lays down the track just in time

In the picture above Gromit is laying down the track as the train moves him along; if you replace Gromit with the renderer and the train with the camera you should be able to see what I mean. The difference is that at the other end of the train, we would need another Gromit facing in the opposite direction picking up the track as the train passes over it, and then handing it back to the Gromit at the front!

I found that I needed a cache of N=Screen Width/Tile width + 2 in the X and N=Screen Height/Tile height + 2 in Y number of tiles in order to keep the screen full constantly.

Figure 1

Figure 1 shows the screen in blue moving around a grid of tiles; green tiles are added at the front of the motion and red tiles are removed (or rather recycled) at the rear. Here is the code used:

A couple of important caveats with this technique:

In order to stop the tiles crawling around the screen very slightly due to coordinate trucation it was important to make sure the reference point for the top left of the screen was correctly rounded to an integer pixel boundary:

My function RoundTo() just rounds to the nearest integer.

Also, the parallax was achieved simply by dividing by the z value of the tile, and then correcting the position of the tiles so they started in the correct place on screen:

So what’s going on in the above code? What we’re actually doing is taking the world-space position of the top left of the screen, dividing by the resolution of the tile to get the number of tiles required in each axis and then ‘integerising’ the result. Going back to the Gromit analogy, this stops Grommit from putting one piece of train track over the top of the last, or in front of it – he is forced to place them exactly one after the other, which means they tile without visual gaps.

The last part is correcting for the offset which happens when we divide by the z depth of the tile layer – this forces the tiles to start at the top left of the screen.

Figure 2

Figure 2 shows a grab of the game where you can see how the tiles are being placed outside the area normally visible to the camera.

The Tiles

Central to any old-school platform game are the tiles which make up the static part of the world. All the tiles in the game are a fixed size; 64×64 pixels, but you can obviously choose whatever tile size is most appropriate.

Tiles are multi-purpose in that they not only provide the building blocks from which every level is constructed, but that they also make up the collision geometry which the player interacts with as they play the level. They are also used to place down enemies and other special non-visible markers which affect certain AI behaviours.

Figure 3

Figure 3 shows the complete tile-set from the game.

Each of the tiles above is assigned a unique integer which represents it, in this case starting from 0 and increasing left to right, top to bottom. The set of all of these integers for a particular level is called the Map. Maps must always be rectangular.

They are represented in code like this:

There is a big enum which allows me to identify which integer corresponds to which tile:

At level construction time, I run through the Map picking out each integer and constructing the relevant tile which represents it. The position of each tile in the world relates exactly to its position in the Map.

Tile coordinate system

The total extent of the world is defined as 64 x Nx, 64 x Ny, where Nx and Ny are the number of tiles in X and Y axis in the Map. I’ve also added an offset to locate 0,0 in pixel coordinates to the middle of the Map, in tile coordinates.

Tile coordinates are simply the tile indices into the map, and world coordinates are in pixels (in this case, since I’m using flash and that’s most convenient). Its quite common to want to be able to convert from tile coordinates into world coordinates and vice versa, I do so like this:

There are corresponding versions for the other axis.

Tile psychology

How you actually design your tiles can have a massive effect on the number of them required to represent your game levels. Getting the tiles to seamlessly repeat is a quite irksome process if your not familiar with it.

Cheese

Consider this cheese tile for example (which was actually designed to look like stone, but ended up like cheese due to my fantastic design skill). Looks nice enough by itself, but when you try tiling it a few times, obvious empty spaces emerge:

Tiling cheese

This is due to the spaces in the original design which were too small to fit a decent sized hole into. The solution is to design holes which are cut away on one side and continue on the other, like this:

Side bits which tile

When combined with the original image we get this:

The final cheese

Which tiles much more pleasingly:

Final cheese tiling

But there is one important side effect which comes when designing the end pieces for each side of the tile – end pieces are tiles which represent the end of a piece of cheese, or earth and are there to make the design look less square and rigid.

Cheese plus 4 end pieces

As you can see the end pieces help break up the squareness of the tile, but there is a problem. Due to the choice I made to tile the holes in the corners of the cheese, it means I not only need end pieces but also corner pieces to fill in the holes you can see above in each corner!

This is not only a lot more work when designing the tiles, its also more work when it comes to mapping them, because you need to map many more actual tiles to tidy up the design.

Mapping

In order to actually map down the tiles for each level you don’t want to have to manually enter all the integers for each tile type, so I used Mappy instead, which is freely available and even has a script to output an AS3 array of data.

A level in Mappy looks like this:

A level in mappy

You can see a selection of the tiles on the right and how they look once you’ve mapped them on the left.

Layers

If you look carefully at the game, you’ll notice that there isn’t just one single layer of tiles for each level. There are actually three!

The main foreground tiles, which consist of things the player collides with

The mid-ground tiles which represent characters and special controls

The background tiles, which are purely visual

Background tiles only

I purposefully made all the background tiles much darker than the foreground so they don’t look confusing to the player.

Mid-ground tiles only

The arrow markers you can see either side of the ladybirds actually control their motion – I’ll talk more about this in a coming article.

Foreground tiles only

The foreground also acts as the collision layer – everything you see in this layer is collidable.

All layers together

Putting all there together, we get the above.

The reason to have three layers is that is gives you much greater flexibility when creating the levels; if you just used one, you wouldn’t be able to map the player on a ladder, for example, because there would be only one tile slot in which to map both player and ladder.

It also adds a predefined depth sorting order – background tiles are always behind everything else, mid-ground tiles come after (although, most are not actually visible), and then the foreground tiles are top-most of all.

Having multiple layers doesn’t add much complexity to the system and it gives a lot of flexibility.

Camera

The camera system I used here is much the same as the one I’ve described previously in this article, so I won’t repeat myself.

I will just point out one improvement I made since last time: because we are using tiles, its of the utmost importance that there not be any visual cracking between the tiles as the player moves around the level. Such cracking would destroy the illusion of a continuous world. In order to prevent this, its very important that the camera only ever be positioned on whole pixel boundaries.

In order to achieve this I’ve added the following bit of code to the camera system:

m_worldToScreen is the matrix which ultimately gets attached to the main Sprite for the game, causing the world to be translated around as the player moves, giving the illusion that the player is moving around the world.

End of part 1

That’s it for this instalment! Next time I’m going to starting talking about the collision detection of the player against the world, and also how the AI works.

As ever, if you want, you can buy the source-code for the entire game (or try a version for free), including all the assets and levels you see above. It will require Adobe Flash CS4+, the Adobe Flex Compiler 4.0+ and either Amethyst, or Flash Develop to get it to build. And you’ll want Mappy or some alternative in order to create your own levels!

Following on from feedback from the Angry Birds article, I’ve included a Flash Develop project as well as an Amethyst project inside the .zip file, to help you get started more quickly, no matter which development environment you have.

You are free to use it for whatever purposes you see fit, even for commercial games or in multiple projects, the only thing I ask is that you don’t spread/redistribute the source-code around. Please note that you will need some programming and design skills in order to make the best use of this!(source:Wildbunny)


上一篇:

下一篇: