使用HTML5创建超级马里奥游戏

原文作者在浏览器中实现一个可以玩的著名的跳跃游戏,作者介绍了动画、面向对象、位运算、图片精灵等诸多干货
下载本文项目源文件 - 0.99 MB

简介

article1.png

在历史上很多优秀的电脑游戏被创造出来,同时奠定了游戏行业发展的基础。这些游戏其中包括了超级马里奥。马里奥这个人物形象第一次出现是在大金刚,随后在1983年的马里奥系列中变得非常出名。现在出现了大量的围绕马里奥形象的衍生版本以及3D版本的游戏。在这篇文章中,我们将开发一个非常简单的超级马里奥山寨版本,为了适合讲解,我们设计得更具有拓展性,具有新的敌人、物品、英雄。

游戏的代码使用OOP的方式来组织。虽然目前JavaScript被认为是一种基于原型的脚本语言,面向对象可能有很多坑,但是我们还是可以尝试使用面向对象。我们将介绍一些关于面向对象的知识和约定,这种模式在整个编码过程会极其有用。

背景

youtube.png

最开始的代码是我的两个学生用我给他们提供的“HTML5编程开发Web应用,CSS3和JavaScript”课程代码为基础。他们开发的游戏包括一个关卡编辑器,声音和图形。游戏本身到没有什么BUG,但是性能比较差,并且代码不是很容易拓展,主要原因是使用了jQuery 的插件。主要是我的责任,因为开始我推荐使用最简单的方法去实现,在简单的情况下使用jQuery插件做动画并没啥问题,但是一旦动画多了就会导致性能问题。因为每一个新的动画(即使同时产生)将会占用CPU的时间片段。

所以在这篇文章中,专注于处理在游戏中需要注意的问题。通过这个文章和重写整个游戏,解决上文的几个问题。

  • 游戏应该能容易拓展。
  • 不会因为太多动画元素导致性能变差。
  • 游戏的开始或者暂停动作将直接对所有的元素起作用。
  • 游戏的运行不应该强依赖额外的元素,例如图片和声音,注意解耦

尽管最后这一条听起来有点不可思议,但是在我看来还是非常重要,那么我们来看下面代码来阐述我的观点:

$(document).ready(function() {
    var sounds = new SoundManager();//**
    var level = new Level('world');//word 是作为相应游戏容器的DOM元素的ID
    level.setSounds(sounds);//*
    level.load(definedLevels[0]);
    level.start();
    keys.bind();
});

这样代码组织起来看起来好多了,但是实际上这里无法避免的一件事就是需要使用HTML5来实现马里奥。所需要的技术细节我们会在稍后讨论。回过头去看我们上面的使用"//**"备注的语句,这里创建了一个声音管理类的实例。它会载入声音特效。加入我们想去关闭这行代码,我们的声音管理类就不在工作。接下来需要对声音实例继续解释的是它并不是在全局作用域中,仅仅在局部作用域。我们这样做是因为没有对象在全局范围内需要对游戏的声音管理类实例的依赖。那么如果想要播放声音怎么办呢?如果某个对象想要播放声音,通过level提供的方法来实现。(每个对象存在都要依赖level,level类是整个游戏中被创建的众多对象中仅有的一个核心类)

现在假如用*好做注释的这样代码能够被用来播放声音,如果我们不调用level实例 的 setSounds方法,level 实例上就不会有一个适配的声音类的实例对象附加上去。因此所有调用声音的请求都会被丢弃掉。这样可以实现声音管理可以插拔,这样我们仅仅移除两行代码可以实现移除声音管理。另一方面,我们也不得不多写这两行代码。当然这些在C#中可以更加优雅的使用依赖注入来实现。

剩下的一点代码就是载入一个关卡(这里我们载入定义的第一个关卡),然后启动关卡。全局的按键对象调用bind()和unbind()控制文档中键盘事件。

基本的设计

mario.png

我们准备在这篇文章中跳过这个声音管理接口。那将在另外一篇关于好的关卡设计器和各种其他有趣的东西的文章中介绍,其中之一就是声音管理器的接口。那么超级马里奥游戏的基本的HTML结构就像下面这样:

<!doctype html>
<html>
<head>
<meta charset=utf-8 />
<title>Super Mario HTML5</title>
<link href="Content/style.css" rel="stylesheet" />
</head>
<body>
<div id="game">
<div id="world">
</div>
<div id="coinNumber" class="gauge">0</div>
<div id="coin" class="gaugeSprite"></div>
<div id="liveNumber" class="gauge">0</div>
<div id="live" class="gaugeSprite"></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="Scripts/testlevels.js"></script>
<script src="Scripts/oop.js"></script>
<script src="Scripts/keys.js"></script>
<script src="Scripts/sounds.js"></script>
<script src="Scripts/constants.js"></script>
<script src="Scripts/main.js"></script>
</body>
</html>

不必过多的介绍这里。需要注意的是我们看到ID为world包含了一个游戏的主区域。游戏的主要区域包括一些提示标签(和一些精灵动画标签),这个简单马里奥游戏仅仅包含了两个提示标签:获得的金币,另外一个则是剩下的生命数量。

一个很重要的部分就是这些JavaScript文件被引入的区域。处于性能的考虑我们把脚本代码放到页面的最低端,这也是为什么jQuery 从CDN中获取(在这个例子中使用了Google 的CDN)(PS:这个链接在国内被墙,大家可以使用其他的CDN获取,例如百度的CDN)。其他的脚本应该打包压缩的。(ASP.NET MVC 中内置了该特性),这篇文章中为了可读性,就没有这样做。我们来看一下这个文件的主要内容:

  • testlevels.js文件是相当大的,应尽量压缩。它包含内置的关卡。所有这些关卡,由两个学生我的课程中整理出来的。第一个关卡是对经典游戏超级马里奥第一关的山寨(如果我没记错的话)
  • oop.js文件包含帮助代码简化和面向对象的JavaScript。我们将稍后讨论这一点。
  • keys.js文件创建的按键对象。如果我们想设计一个多人游戏或其他功能,我们也应该模块化脚本(像处理声音那样抽象出来)。
  • 声音管理写在sounds.js文件。基本的想法是WEB音频API可能赶上现有的API(flash)。现在的问题是,web音频API仅仅Google Chrome 浏览器和后期的Safari浏览器。尽量使用一个好的方式在浏览器中实现声音效果。
  • 文件constants.js包含常量和非常基本的辅助方法。
  • 其他对象集中在文件main.js。

在开始详细讲解实现之前我们看下CSS样式文件:

@font-face {
   font-family: 'SMB';
   src: local('Super Mario Bros.'),
        url('fonts/Super Mario Bros.ttf') format('truetype');
   font-style: normal;
}
#game {
    height: 480px; width: 640px; position: absolute; left: 50%; top: 50%;
    margin-left: -321px; margin-top: -241px; border: 1px solid #ccc; overflow: hidden;
}
#world { 
    margin: 0; padding: 0; height: 100%; width: 100%; position: absolute;
    bottom: 0; left: 0; z-index: 0;
}
.gauge {
    margin: 0; padding: 0; height: 50px; width: 70px; text-align: right; font-size: 2em;
    font-weight: bold; position: absolute; top: 17px; right: 52px; z-index: 1000;
    position: absolute; font-family: 'SMB';
}
.gaugeSprite {
    margin: 0; padding: 0; z-index: 1000; position: absolute;
}
#coinNumber {
    left: 0;
}
#liveNumber {
    right: 52px;
}
#coin {
    height: 32px; width: 32px; background-image : url(mario-objects.png);
    background-position: 0 0; top: 15px; left: 70px;
}
#live {
    height: 40px; width: 40px; background-image : url(mario-sprites.png);
    background-position : 0 -430px; top: 12px; right: 8px;
}
.figure {
    margin: 0; padding: 0; z-index: 99; position: absolute;
}
.matter {
    margin: 0; padding: 0; z-index: 95; position: absolute; width: 32px; height: 32px;
}

现在看起来代码还是比较简短(但是还是有必要做一些解释)。最上面可以看到我们给我们的游戏添加了一个好看的像素字体,和游戏更贴切。然后我们设置了主要区域的大小为 640 x 480绝对像素。添加一个overflow为hidden是非常有必要的。因此我们我们仅仅对元素内部的世界进行操纵,意味着游戏只有一个方向的视图。关卡就放置在这个区域内部,显示计分信息的标签放到视图的头部,人物和CSS class相对应。按照同样被处理的还有声音以及其他元素:每个元素使用class来影响在视图中的效果。还有一个比较重要的就是z-index,我们可以把动态的操作对象总是放到静态元素的上面(也有例外,后续会讲到)。

JavaScript 面向对象
使用JavaScript实现面向对象其实并不困难,但是还是有那么一点混乱。主要原因就是JavaScript太过于灵活,可以通过多种方式可以去实现它。每种方式都有它的优势和缺点。对游戏而言,我们希望严格的保持一种风格,因此我建议下面这种方式:

var reflection = {};

(function(){
    var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;

    // The base Class implementation (does nothing)
    this.Class = function(){ };

    // Create a new Class that inherits from this class
    Class.extend = function(prop, ref_name) {
        if(ref_name)
            reflection[ref_name] = Class;

        var _super = this.prototype;

        // Instantiate a base class (but only create the instance,
        // don't run the init constructor)
        initializing = true;
        var prototype = new this();
        initializing = false;

        // Copy the properties over onto the new prototype
        for (var name in prop) {
        // Check if we're overwriting an existing function
        prototype[name] = typeof prop[name] == "function" && 
            typeof _super[name] == "function" && fnTest.test(prop[name]) ?
            (function(name, fn) {
                return function() {
                    var tmp = this._super;

                    // Add a new ._super() method that is the same method
                    // but on the super-class
                    this._super = _super[name];

                    // The method only need to be bound temporarily, so we
                    // remove it when we're done executing
                    var ret = fn.apply(this, arguments);        
                    this._super = tmp;

                    return ret;
                };
            })(name, prop[name]) :
            prop[name];
        }

        // The dummy class constructor
        function Class() {
            // All construction is actually done in the init method
            if ( !initializing && this.init )
                this.init.apply(this, arguments);
        }

        // Populate our constructed prototype object
        Class.prototype = prototype;

        // Enforce the constructor to be what we expect
        Class.prototype.constructor = Class;

        // And make this class extendable
        Class.extend = arguments.callee;

        return Class;
    };
})();

这段代码是非常有用的基于原型实现继承,它的作者是John Resig。他在自己的博客中写过关于整个编码问题的文章(文章来自John Resig 关于JavaScript面向对象)。这些代码被直接包裹在匿名立即执行方法中是为了避免全局作用域,尽量在局部作用域中完成任务。Class对象是window 对象的一个拓展(实际上应该是最底层的一个对象,取决于代码运行的环境,例如果被执行在web浏览器中,Class类就会被挂到window对象上)

我拓展了这些代码,让它更名副其实的作为Class。JavaScript没有原生和有效的反射特性,这就是我们自己实现类的局限,不得不多写一些代码来描述类的继承关系。将构造函数放到类的一个方法的变量中(稍后我们会看到),我们就可以通过选项把父类的名称作为第二个参数传入。如果我们这样做,一个引用的构造函数被放置到作为以第二个参数传入的属性名的对象映射(PS:这句话太难理解很翻译,看代码很容易懂,就是继承的时候传个名称,到时候生成的子类中会以这个名称创建一个属性,并把父类放到这个属性上面)。

下例为一个简单的类构造过程:

var TopGrass = Ground.extend({
    init: function(x, y, level) {
        var blocking = ground_blocking.top;
        this._super(x, y, blocking, level);
        this.setImage(images.objects, 888, 404);
    },
}, 'grass_top');

我们创建了一个TopGrass类,继承自Ground类。init()方法表示类的构造方法。为了调用父类的构造方法(有些时候不是必须的),我们必须调用上面的this._super() 方法. 这是一个特殊的方法,并且可以在任意方法中调用。

一个很重要的提示就是:这里讲的面向对象和真正的多态(例如C#中原生支持的面向对象)之间有一个很重要的区别,那就是显然不可以从外部访问到父类的方法(因为我们无法改变我们看到的对象-它总是一个动态对象)所以仅仅访问通过this._super()来访问父类的方法包括被子类重写个方法也可以通过这种方法访问。仅仅是对上面这种方式来说的,所以这么说也并不绝对(PS:继承的方式有很多有些方式可以避免这个问题)。

Ground这个类相当无趣(因为只是一个中间层)。所以让我们看Ground的基类:

var Matter = Base.extend({
    init: function(x, y, blocking, level) {
        this.blocking = blocking;
        this.view = $(DIV).addClass(CLS_MATTER).appendTo(level.world);
        this.level = level;
        this._super(x, y);
        this.setSize(32, 32);
        this.addToGrid(level);
    },
    addToGrid: function(level) {
        level.obstacles[this.x / 32][this.level.getGridHeight() - 1 - this.y / 32] = this;
    },
    setImage: function(img, x, y) {
        this.view.css({
            backgroundImage : img ? c2u(img) : 'none',
            backgroundPosition : '-' + (x || 0) + 'px -' + (y || 0) + 'px',
        });
        this._super(img, x, y);
    },
    setPosition: function(x, y) {
        this.view.css({
            left: x,
            bottom: y
        });
        this._super(x, y);
    },
});

在这里我们对基类进行拓展(这是一个顶级类)。所有的物体都静态的32*32像素和包含一个块变量(即使这个元素没有被设置为块元素),对于每一个物体和对象实例需要表现为一个实际的视图,所以我用JQuery 绑定一个dom 元素作为视图。所以每个对象都应该通过基类继承到setImage()方法

同样的原因应用到setPosition()方法。而 addToGrid()方法所有的子类都需要被继承,用于给予根据关卡创建的游戏网格提供一个统一的方法。

游戏控制

游戏基本上都是根据键盘进行操纵。因此需要绑定事件处理到文档中。我们仅仅需要使用少量的几个键位,且能够按下和释放。于是我们需要监听每个键位的状态,我们拓展keys对象添加相应的属性和方法(比如左走监听左键,右走监听右键,等等),定义并调用bind()方法来实现绑定事件和取消绑定事件。我们再次使用JQuery来实现具体的工作,可以避免不同浏览器之间的差异。使用JQuery的意义在于我们可以集中精力解决具体问题而不是处理浏览器差异上。

var keys = {
    //Method to activate binding
    bind : function() {
        $(document).on('keydown', function(event) {    
            return keys.handler(event, true);
        });
        $(document).on('keyup', function(event) {    
            return keys.handler(event, false);
        });
    },
    //Method to reset the current key states
    reset : function() {
        keys.left = false;
        keys.right = false;
        keys.accelerate = false;
        keys.up = false;
        keys.down = false;
    },
    //Method to delete the binding
    unbind : function() {
        $(document).off('keydown');
        $(document).off('keyup');
    },
    //Actual handler - is called indirectly with some status
    handler : function(event, status) {
        switch(event.keyCode) {
            case 57392://CTRL on MAC
            case 17://CTRL
            case 65://A
                keys.accelerate = status;
                break;
            case 40://DOWN ARROW
                keys.down = status;
                break;
            case 39://RIGHT ARROW
                keys.right = status;
                break;
            case 37://LEFT ARROW
                keys.left = status;            
                break;
            case 38://UP ARROW
                keys.up = status;
                break;
            default:
                return true;
        }

        event.preventDefault();
        return false;
    },
    //Here we have our interesting keys
    accelerate : false,
    left : false,
    up : false,
    right : false,
    down : false,
};

使用JQuery的另外一个强大的用处,我们可以使用相同方法同时给上键和下键绑定事件(使用JQuery 可以方便的给元素添加多个事件)。而且可以通过方法来调用手动触发事件,例如triger()方法,当然这样写也更加容易理解并且代码也更加干净。

CSS 精灵

所有的图形都是用CSS 精灵来实现,实现图片精灵很简单,但是需要注意下面几点。为了使用图片,我们需要把图片设置为对应元素的背景图。然后就是不要设置背景no-repeat 属性,我们可以利用这一点制作特殊的效果也不会出什么问题,反而可以给我们带来很多好处。

然后就是设置背景图定位和偏移,通常偏移是(0,0),指的是以元素的左上角。一般来说背景定位都是相对于当前元素的,设置为(20,10)就是左上角为原点,向下10像素,向右20像素偏移。如果设置为 (-20, -10) ,就会向下相反的地方偏移,但是只会显示包含在元素内部背景图了,超出到元素外部不会显示出来。

spritesheet.png

上面的图展示了CSS图片精灵如果工作。解释图片精灵的时候出了说明定位方式和原点之外还有,区分规则的CSS图片精灵和错杂的CSS图片精灵。规则的CSS图片精灵一般放到一个看不见的网格中,这样可以实现动画,错杂的CSS图片精灵就是把常用的图标都放到一起,不需要严格的网格,主要作用就是减少HTTP请求数量,提高web性能。

当然我们需要实现动画,就需要把图片精灵按照网格排减少不必要的麻烦和计算。游戏使用下面的脚本来播放动画效果。

var Base = Class.extend({
    init: function(x, y) {
        this.setPosition(x || 0, y || 0);
        this.clearFrames();
    },
    /* more basic methods like setPosition(), ... */
    setupFrames: function(fps, frames, rewind, id) {
        if(id) {
            if(this.frameID === id)
                return true;

            this.frameID = id;
        }

        this.frameCount = 0;
        this.currentFrame = 0;
        this.frameTick = frames ? (1000 / fps / constants.interval) : 0;
        this.frames = frames;
        this.rewindFrames = rewind;
        return false;
    },
    clearFrames: function() {
        this.frameID = undefined;
        this.frames = 0;
        this.currentFrame = 0;
        this.frameTick = 0;
    },
    playFrame: function() {
        if(this.frameTick && this.view) {
            this.frameCount++;

            if(this.frameCount >= this.frameTick) {            
                this.frameCount = 0;

                if(this.currentFrame === this.frames)
                    this.currentFrame = 0;

                var $el = this.view;
                $el.css('background-position', '-' + (this.image.x + this.width * 
                  ((this.rewindFrames ? this.frames - 1 : 0) - this.currentFrame)) + 
                  'px -' + this.image.y + 'px');
                this.currentFrame++;
            }
        }
    },
});

我们在基类中实现了图片精灵动画效果,因此具体的人物类和物品类继承了改方法,保证了动画的统一。基本上每个对象都有一个setupframes()来设置精灵动画的效果。当然精灵动画的原理就是背景从左到右定时切换,可以看做逐帧动画。需要设置动画的参数就是帧率(FPS)。

这个方法还有一个很重要的参数就是ID。在这里,我们可以指定一个值,可以确定当前的动画,作为动画对象的标示符。这可以用来区分如果动画即将创建并开始运行。如果动画还没处理完成,我们不去充值内部你一些参数。那么我们怎么来使用这个动画类呢?最好的方法就是写一个类,我们看下马里奥人物的怎么动起来的:

var Mario = Hero.extend({
    /*...*/
    setVelocity: function(vx, vy) {
        if(this.crouching) {
            vx = 0;
            this.crouch();
        } else {
            if(this.onground && vx > 0)
                this.walkRight();
            else if(this.onground && vx < 0)
                this.walkLeft();
            else
                this.stand();
        }

        this._super(vx, vy);
    },
    walkRight: function() {
        if(this.state === size_states.small) {
            if(!this.setupFrames(8, 2, true, 'WalkRightSmall'))
                this.setImage(images.sprites, 0, 0);
        } else {
            if(!this.setupFrames(9, 2, true, 'WalkRightBig'))
                this.setImage(images.sprites, 0, 243);
        }
    },
    walkLeft: function() {
        if(this.state === size_states.small) {
            if(!this.setupFrames(8, 2, false, 'WalkLeftSmall'))
                this.setImage(images.sprites, 81, 81);
        } else {
            if(!this.setupFrames(9, 2, false, 'WalkLeftBig'))
                this.setImage(images.sprites, 81, 162);
        }
    },
    /* ... */
});

在这里,我们重写setvelocity()方法。根据当前状态执行相应的行为,例如walkright()或walkleft()。然后根据具体的行为执行动画。使用ID标示获取动画对象,如果想要改变人物的位置那就创建一个新的动画即可。否则动画就一直保持存在,直到人物到达合适的位置上去。

类结构图

写一个游戏可以让激励我们学习面向对象。因为面向对象的游戏可以让代码更加简单好玩。同时也更少BUG,下面是游戏的类图:

diagram-preview.png

游戏的类结构图显示了各个类的依赖和关系。其中的好处就是可以非常容易的拓展游戏,下一节会讲到怎么拓展游戏,我们先把游戏部分说完。

在JavaScript面向对象中,继承只是面向对象带给我们的一个要点。与之类似的特性还有就是类的实例化。我们怎么判断一个对象是某个类的实例呢?我们看下面例子:

var Item = Matter.extend({
    /* Constructor and methods */
    bounce: function() {
        this.isBouncing = true;

        for(var i = this.level.figures.length; i--; ) {
            var fig = this.level.figures[i];

            if(fig.y === this.y + 32 && fig.x >= this.x - 16 && fig.x <= this.x + 16) {
                if(fig instanceof ItemFigure)
                    fig.setVelocity(fig.vx, constants.bounce);
                else if(fig instanceof Enemy)
                    fig.die();
            }
        }
    },
})

这个例子显示了 item类的一部分。这个类包括了一个新的方法bounce(),这个方法用来控制人物的跳动,并且设置isBouncing变量为 true。就像原本的马里奥游戏中,恰好在跳跃人物的下面的怪物会被杀死。其他用处场景就是比如在蘑菇被顶了后,蘑菇会被弹起。

拓展游戏

有些时候总会遇到需要加入新的动画和图片。一个例子就是在fire(火力?)模式下给马里奥找一件合适衣服,于是这个例子使用了一个大号的马里奥图片。下面图片就是在火力模式下胜利的马里奥形象。

victory1.png

游戏中有本身的几个扩展点。一个典型的扩展点是建立一个新的类,给它一个适当的表现出它的用处的名字。关卡可以用这个名字,最后在关卡创建这个类的一个实例。我们创建一个例子来换一个新的样式:

var LeftBush = Decoration.extend({
    init: function(x, y, level) {
        this._super(x, y, level);
        this.setImage(images.objects, 178, 928);
    },
}, 'bush_left');

这样就很简单实现。我们仅仅需要继承从Decoration类,然后覆盖setImage()方法即可。可以为这个新的样式取一个名称为bush_left。

现在让我们考虑一个新的敌人延长游戏的情况:鬼(不包括在源代码)!这是一个有点困难,但不是从原则。这个问题跟这一特定类型的敌人也要遵守规则。基本建设是直线前进:

现在,我们能考虑引入一个新的敌人(一个没在原来代码中出现的形象:鬼魂),来延长故事线路。还是有些难度,但是这不是借口。对待这个问题和对待敌人的原则也是一致的,基本意见就是直线前进:

var Ghost = Enemy.extend(
    init: function(x, y, level) {
        this._super(x, y, level);
        this.setSize(32, 32);
    },
    die: function() {
        //Do nothing here!
    },
});

所以首先我们看到这里没有在die()方法里面,调用 _super()方法,结果就是这个怪物永远不会死(即使ta已经死了),这是我们定好的一个规则,其他规则还有:

  • 怪物只要看到马里奥,就会一直朝马里奥移动。
  • 如果马里奥回头,这个怪物停止
  • 即使马里奥有星星状态或者射击能力,怪物也不会死

而这里也不在使用通用的setVelocity() 方法,为了实现这个例子,重新定义一个move()方法来代替:

  • 重力对幽灵不起作用
  • 幽灵满足上面的条件,然后移动就可以了

了解完幽灵敌人的剩下信息,于是我们根据分析写出下面的代码,就像这样

var Ghost = Enemy.extend({
    init: function(x, y, level) {
        this._super(x, y, level);
        this.setSize(33, 32);
        this.setMode(ghost_mode.sleep, directions.left);
    },
    die: function() {
                //Do nothing here!
        },
    setMode: function(mode, direction) {
        if(this.mode !== mode || this.direction !== direction) {
            this.mode = mode;
            this.direction = direction;
            this.setImage(images.ghost, 33 * (mode + direction - 1), 0);
        }
    },
    getMario: function() {
        for(var i = this.level.figures.length; i--; )
            if(this.level.figures[i] instanceof Mario)
                return this.level.figures[i];
    },
    move: function() {
        var mario = this.getMario();

        if(mario && Math.abs(this.x - mario.x) <= 800) {
            var dx = Math.sign(mario.x - this.x);
            var dy = Math.sign(mario.y - this.y) * 0.5;
            var direction = dx ? dx + 2 : this.direction;
            var mode = mario.direction === direction ? ghost_mode.awake : ghost_mode.sleep;
            this.setMode(mode, direction);

            if(mode)        
                this.setPosition(this.x + dx, this.y + dy);
        } else 
            this.setMode(ghost_mode.sleep, this.direction);
    },
    hit: function(opponent) {           
        if(opponent instanceof Mario) {
            opponent.hurt(this);
        }
    },
}, 'ghost');

现在满足了所有的规则,幽灵只能移动。幽灵将在马里奥显示的范围内移动(实际上是800像素),为了实现干净继续添加一个用来存幽灵状态的ghost_mode对象

var ghost_mode = {
    sleep : 0,
    awake : 1,
};

还需要介绍一些其他的新精灵。这个例子中我们使用了一个图像来放置幽灵图片。这个路径存放到images.ghost变量中然后便于引用图片

mario-ghost.png

总结下有趣的地方

这篇文章用《HTML5 创建超级马里奥》命名,但事实上是并不是使用html5来实现的。HTML5中提供了\元素和\

即使JavaScript是一个动态语言,但是我们仍然能够使用基于位操作定义枚举类型。就像下面这种方式来定义位计算的枚举类型。
e.g.:

var ground_blocking = {
    none   : 0,
    left   : 1,
    top    : 2,
    right  : 4,
    bottom : 8,
    all    : 15,
};

变量的值可以使用+操作符来实现区分,例如:
var blocking = ground_blocking.left + ground_blocking.top
(这里使用位操作来计算,但是JavaScript操作位运算不是那么方便,还可以使用其他方法)

这样可以读出变量和组合变量。
也可以从组合变量中根据&操作符读出单值

//e.g. check for top-blocking
function checkTopBlocking(blocking) {
    if((ground_blocking.top & blocking) === ground_blocking.top)
        return true;

    return false;
}

最初的文件可以在下列地址找到:
http://www.florian-rappl.de/html5/projects/SuperMario/
这个版本也在网上可以预览

版权声明

文章,以及附带的任何源码和文件,在Code Project Open License(CPOL)署名下。欢迎转载,但请保留文章版权声明以及译者、原文信息。

翻译

少个分号 http://www.printf.cn

原文地址

http://www.codeproject.com/Articles/396959/Mario