横向滚动游戏(如超级马里奥兄弟)背后的原理是什么





4.00/5 (2投票s)
在本文中,我们将实现一个简单的横向卷轴游戏,我们称之为“尼莫鱼”。
引言
这是本系列关于 CodeProject 游戏开发的最后一篇文章!我过去几周的工作被大量的游戏开发任务填满,我想与大家分享我发现的许多有趣的知识。
正如文章标题所示,在本文中,我们将深入探讨横向卷轴游戏的基础,这些游戏你们肯定都知道,比如“超级马里奥兄弟”、“魂斗罗”、“功夫大师”等等。
为了避免法律问题,我们将从零开始开发一款横向卷轴游戏(以下简称 HSSG),它将被命名为“尼莫鱼”!在我设想中,尼莫是一条蓝色的小鱼,将在海中游弋,寻找古代宝藏。我们将使用 **jQuery** 和 **HTML Canvas** 来实现这个原型,使其可以在互联网上访问!
遗憾的是,我不是一个出色的平面设计师,我将只使用可以在谷歌图片中找到的图片。所以,目前,我设想的第一个关卡如下图所示。我不明白**如何在文章中嵌入 YouTube 视频**,如果可能的话,请告诉我。
背景
为了阅读本文,你需要了解游戏开发的基础知识(至少了解绘图时间和逻辑时间之间的划分)。
HSSG 的第一个特点是**卷轴**:我们需要让玩家能够滚动关卡,从起始位置到结束位置。当然,我们不可能绘制关卡的每一米(我们又不是开发像 GTA 圣安地列斯那样的全 3D 游戏),所以我们将使用一些技巧来营造 2D 世界中的移动感。
我们的第一个目标将是实现无限横向卷轴:为此,我们需要使用一张图片,在连接左右两侧时不会出现明显的瑕疵,可以无缝循环。接下来,你可以通过 YouTube 视频链接来查看已实现无限卷轴的效果。
我们的第二个目标将是实现玩家运行时,一个负责处理关卡中所有玩家动作的对象。正如你所能想象的,我们的玩家将是一条小鱼,它可以向前游(从左到右)并跳出水面来捕捉一些东西。
接着,你还可以通过 YouTube 链接来观看关卡运行时效果。
我们的第三个目标将是实现关卡运行时,一个负责处理关卡中所有对象 Object 的对象。**关卡运行时必须根据玩家当前位置绘制障碍物、敌人等对象**。接着,你还可以通过 YouTube 链接来观看关卡运行时效果。
关于玩家位置和关卡绘制
我对关卡运行时的想法比较简单。首先,玩家的起始位置是 (0, 100)。在之前的视频预览中,你可以看到我们很棒的黄色小鱼在待机状态。正如你从之前的视频中看到的,小鱼可以跳跃,所以它的**y**坐标会随着跳跃曲线(感谢重力加速度)而改变。最后,小鱼每次都将绘制在 **(0,y)** 位置。其他对象会改变它们的 (x,y) 坐标,但小鱼始终固定在 **x=0**。
我将关卡分成了三个图层(海面顶部层、中间水层和底层)。然后,我使用了三维数组来表示关卡对象的位置。我将一个对象的位置 **x** 视为其数组索引*2米。因此,在每个 x 位置最多可以有 3 个对象,其中一个对象在三个关卡图层中的一个图层上拥有 y 坐标。我附上了索引为 0 的对象(距离起点 2 米)的图片。
关卡管理器将处理场景中所有其他对象的位置。我们需要知道,除了玩家之外,每个对象都会从屏幕的**右侧**滚动到**左侧**。此外,所有对象将在玩家到达距离 **D**(记住数组索引*2米)时出现。然后,在检测碰撞时,我们只考虑绘制在屏幕上的对象的实际坐标,而不是相对对象(距离起点的距离)。
在最后一个视频中,你可以看到 LevelManager
操作了两种对象:一种是金币,可以喂养小鱼;另一种是鱼钩,可以移除鱼的食物。请注意控制台输出,以查看碰撞何时出现。
开始游戏
你可以在线玩这个原型 这里。你也可以通过页面顶部的直接链接下载原型。
Using the Code
首先,我们需要使用 Canvas
加载所有我们想要使用的图像。因为我们无法对部分图像调用 drawImage
函数。我知道,下面的代码不是“科学的典范”,但它能完成它的任务。下面的代码以顺序模式加载图像。
// Load game images
var levelBackground = new Image();
var levelSky = new Image();
var coinImage = new Image();
var enemyImage = new Image();
var player = new Image();
levelBackground.src='./undersea.png';
levelBackground.onload= function() {
levelSky.src='./sky.jpg';
{
levelSky.onload= function () {
player.src="./fish.png";
player.onload= function() {
coinImage.src="./fish-feed.png";
coinImage.onload=function() {
enemyImage.src="./enemy.png";
enemyImage.onload=function () {
_initGameObject()
}
}
};
};
};
}
然后,我们需要设置游戏环境。
// keyboard mapping (handling space and right button)
var keyboardMap = {39:false, 32: false};
var ctx= null; //canvas 2d context
var playGame = null; //timer for redrawing
var feedCatched = 0; // how many feed was caught
var distance = 0; // distance from starting point
// variables for FPS and delta counting
var startTime = null;
var delta = 0;
// THE LEVEL MANAGER
// Please note: each slot correspond to 2meters
// Also, each there are tree sublevel (for simplicity)
// E = Enemies
// C = Coin
var Level1 = [["C"],["E"],["C"]];
var LevelManager = [];
// Game Width and height
var cWidth=0;
var cHeight=0;
// Player coordinates
var playerObject = null;
// a canvas effect (ignore it if you want, it is only my test)
var seaGradient= null;
var sBlueColor = 231;
var sVerse = 0;
function _waveGradientUpdate()
{
seaGradient = ctx.createLinearGradient(0,0,0,cHeight);
seaGradient.addColorStop(0,"rgb(138,212,"+sBlueColor+")");
seaGradient.addColorStop(1,"#ffffff");
if (sVerse==0)
sBlueColor-=3;
else
sBlueColor+=3;
if (sBlueColor<=180) sVerse=1;
if (sBlueColor>=255) sVerse=0;
}
最重要的代码片段是:playerObject
运行时和 gameObject
运行时。在下面的函数中,你将看到我处理玩家行为的方式,但你可以选择任何其他代码模式。请**牢记**,**绘图时间与逻辑时间无关**。在真正的游戏中,你需要编写一个线程来容纳所有对象,以及另一个线程来调用所有绘图函数。
function createPlayer(sX,sY)
{
return {
x:sX,
y:sY,
currentFrame:0,
currentFrameY:0,
totalForwardAmount:0,
srcImage:null,
state:0,
isJumping:false,
isForwarding:false,
forwardSpeed:0,
jumpSpeed:0,
jump: function()
{
if (this.isJumping) return;
this.jumpSpeed=15;
this.isJumping=true;
},
startForward: function()
{
this.forwardSpeed=0.5;
this.isForwarding=true;
},
stopForward: function()
{
this.totalForwardAmount=0;
this.currentFrame=0;
this.isForwarding=false
},
drawPlayer: function(ctx,delta)
{
// handle forward speed
if (this.forwardSpeed>0) {
this.x += this.forwardSpeed*delta;
distance += (delta*this.forwardSpeed)/1000;
if (this.x>=cWidth)
this.x = 0;
if (!this.isForwarding)
this.forwardSpeed-= 0.002*delta;
this.totalForwardAmount+=delta;
if (this.totalForwardAmount>=75) {
this.currentFrame=0;
this.totalForwardAmount=0;
}
else if (this.totalForwardAmount>=50)
this.currentFrame=2;
else if (this.totalForwardAmount>=25)
this.currentFrame=1;
}
// if (this.forwardSpeed<0.1) this.forwardSpeed=0.1;
// { this.currentFrame=0; this.totalForwardAmount=0; }
// handle jump speed
if (this.isJumping) {
this.currentFrameY=1;
if (this.jumpSpeed > 0)
this.currentFrame=0;
else
this.currentFrame=1;
this.y -= (delta*this.jumpSpeed)/10;
this.jumpSpeed -= (0.05*delta);
if (this.y>=400)
{
this.isJumping=false;
this.jumpSpeed=0;
this.y=400;
this.currentFrameY=0;
}
}
ctx.drawImage(this.srcImage,this.currentFrame*300,
this.currentFrameY*270,300,256,100,this.y,100,100);
}
};
}
我避免粘贴其他代码片段,因为你可以下载示例并分析完整的原型。
关注点
我需要向 Level1
数组添加元素,以便向关卡中添加游戏对象。你只能使用 C(Coin
)和 E(Enemies
)。
历史
- 2016 年 10 月 31 日:发布