65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2016 年 10 月 31 日

CPOL

5分钟阅读

viewsIcon

12112

downloadIcon

315

在本文中,我们将实现一个简单的横向卷轴游戏,我们称之为“尼莫鱼”。

引言

这是本系列关于 CodeProject 游戏开发的最后一篇文章!我过去几周的工作被大量的游戏开发任务填满,我想与大家分享我发现的许多有趣的知识。

正如文章标题所示,在本文中,我们将深入探讨横向卷轴游戏的基础,这些游戏你们肯定都知道,比如“超级马里奥兄弟”、“魂斗罗”、“功夫大师”等等。

为了避免法律问题,我们将从零开始开发一款横向卷轴游戏(以下简称 HSSG),它将被命名为“尼莫鱼”!在我设想中,尼莫是一条蓝色的小鱼,将在海中游弋,寻找古代宝藏。我们将使用 **jQuery** 和 **HTML Canvas** 来实现这个原型,使其可以在互联网上访问!

遗憾的是,我不是一个出色的平面设计师,我将只使用可以在谷歌图片中找到的图片。所以,目前,我设想的第一个关卡如下图所示。我不明白**如何在文章中嵌入 YouTube 视频**,如果可能的话,请告诉我。

背景

为了阅读本文,你需要了解游戏开发的基础知识(至少了解绘图时间和逻辑时间之间的划分)。

HSSG 的第一个特点是**卷轴**:我们需要让玩家能够滚动关卡,从起始位置到结束位置。当然,我们不可能绘制关卡的每一米(我们又不是开发像 GTA 圣安地列斯那样的全 3D 游戏),所以我们将使用一些技巧来营造 2D 世界中的移动感。

我们的第一个目标将是实现无限横向卷轴:为此,我们需要使用一张图片,在连接左右两侧时不会出现明显的瑕疵,可以无缝循环。接下来,你可以通过 YouTube 视频链接来查看已实现无限卷轴的效果。

观看 YouTube 视频

我们的第二个目标将是实现玩家运行时,一个负责处理关卡中所有玩家动作的对象。正如你所能想象的,我们的玩家将是一条小鱼,它可以向前游(从左到右)并跳出水面来捕捉一些东西。

接着,你还可以通过 YouTube 链接来观看关卡运行时效果。

观看 YouTube 视频

我们的第三个目标将是实现关卡运行时,一个负责处理关卡中所有对象 Object 的对象。**关卡运行时必须根据玩家当前位置绘制障碍物、敌人等对象**。接着,你还可以通过 YouTube 链接来观看关卡运行时效果。

观看 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 日:发布
© . All rights reserved.