Step by Step 使用HTML5开发一个星际大战游戏(2)

HTML5 Canvas Game: 玩家飞船

本系列博文翻译自以下文章

http://blog.sklambert.com/html5-canvas-game-the-player-ship/

Languages: HTML5, JavaScript
Code: https://github.com/straker/galaxian-canvas-game/tree/master/part2

2.玩家飞船

上节,我们讲述了背景滚动的实现,本节我们来实现一下玩家飞船的移动和子弹的发射。

首先让我们看一下本次教程的效果,如果界面没有反应,确认您鼠标点击了下面的界面。

控制: 移动 – 上下左右箭头
射击 – 空格键

The HTML Page

再次,我们先看html页面代码:

<!DOCTYPE html>
<html>
    <head>
        <title>Space Shooter Demo</title>
        <style>
            canvas {
                position: absolute;
                top: 0px;
                left: 0px;
                background: transparent;
            }
            #background {
                z-index: -2;
            }
            #main {
                z-index: -1;
            }
            #ship {
                z-index: 0;
            }
        </style>
    </head>
    <body>
        <!-- The canvas for the panning background -->
        <canvas id="background" width="600" height="360">
            Your browser does not support canvas. Please try again with a different browser.
        </canvas>
        <!-- The canvas for all enemy ships and bullets -->
        <canvas id="main" width="600" height="360">
        </canvas>
        <!-- The canvas the ship uses (can only move up
         one forth the screen.) -->
        <canvas id="ship" width="600" height="360">
        </canvas>
        <script src="space_shooter_part_two.js"></script>
    </body>
</html>

与上一节的html文件相比,一些地方放生了变化,这里我们使用了3个画布(canvas),一个用来画背景,一个用来画飞船,一个用来画子弹和敌机,这样做的好处是我们不用每次都重绘游戏的每一帧。通过css定义了这三个图层的z序。

Drawable

首先,Drawable对象的init方法需要改变,代码如下:

function Drawable() {
    this.init = function(x, y, width, height) {
        // Defualt variables
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
}

因为我们的图像不再充满整个画布,所以我们补充了width, height 2个属性。

The imageRepository

图像仓库对象,也有改动,代码如下:

/**
 * Define an object to hold all our images for the game so images
 * are only ever created once. This type of object is known as a
 * singleton.
 */
var imageRepository = new function() {
    // Define images
    this.background = new Image();
    this.spaceship = new Image();
    this.bullet = new Image();
    // Ensure all images have loaded before starting the game
    var numImages = 3;
    var numLoaded = 0;
    function imageLoaded() {
        numLoaded++;
        if (numLoaded === numImages) {
            window.init();
        }
    }
    this.background.onload = function() {
        imageLoaded();
    }
    this.spaceship.onload = function() {
        imageLoaded();
    }
    this.bullet.onload = function() {
        imageLoaded();
    }
    // Set images src
    this.background.src = "imgs/bg.png";
    this.spaceship.src = "imgs/ship.png";
    this.bullet.src = "imgs/bullet.png";
}
加载三张图片,背景,飞船,子弹。三张图片都加载完成后,调用window.init();开始游戏。

玩家飞船

接下来我们将讨论飞船的移动和子弹的发射。

首先,我们先了解一个新的数据结构,对象池。

对于一些可能需要被频繁创建和销毁的对象,我们可以将其缓存起来,重复使用,以减少系统的开销,提高游戏运行的速度,这里我们的子弹就属于这种对象。

/**
 * Custom Pool object. Holds Bullet objects to be managed to prevent
 * garbage collection.
 */
function Pool(maxSize) {
    var size = maxSize; // Max bullets allowed in the pool
    var pool = [];
    /*
     * Populates the pool array with Bullet objects
     */
    this.init = function() {
        for (var i = 0; i < size; i++) {
            // Initalize the bullet object
            var bullet = new Bullet();
            bullet.init(0,0, imageRepository.bullet.width,
                        imageRepository.bullet.height);
            pool[i] = bullet;
        }
    };
    /*
     * Grabs the last item in the list and initializes it and
     * pushes it to the front of the array.
     */
    this.get = function(x, y, speed) {
        if(!pool[size - 1].alive) {
            pool[size - 1].spawn(x, y, speed);
            pool.unshift(pool.pop());
        }
    };
    /*
     * Used for the ship to be able to get two bullets at once. If
     * only the get() function is used twice, the ship is able to
     * fire and only have 1 bullet spawn instead of 2.
     */
    this.getTwo = function(x1, y1, speed1, x2, y2, speed2) {
        if(!pool[size - 1].alive &&
           !pool[size - 2].alive) {
                this.get(x1, y1, speed1);
                this.get(x2, y2, speed2);
             }
    };
    /*
     * Draws any in use Bullets. If a bullet goes off the screen,
     * clears it and pushes it to the front of the array.
     */
    this.animate = function() {
        for (var i = 0; i < size; i++) {
            // Only draw until we find a bullet that is not alive
            if (pool[i].alive) {
                if (pool[i].draw()) {
                    pool[i].clear();
                    pool.push((pool.splice(i,1))[0]);
                }
            }
            else
                break;
        }
    };
}

当对象池被初始化时,建立了一个给定个数的子弹对象数组,每当需要一个新的子弹的时候,对象池查看数组的最后一个元素,看该元素是否当前被占用。如果被占用,说明当前对象池已经满了,无法产生新子弹;如果没被占用,

取出最后一个子弹元素,放到数组的最前面。这样使得对象池中总是后面部分存放着待使用的子弹,而前面存放着正在使用的子弹(屏幕上看到的)。

当对象池animates子弹的时候,判断当前子弹是否还在屏幕中(draw方法返回false),如果draw方法返回true,说明该子弹已经脱离屏幕区域,可以被回收供再利用)。

现在对象池对象已经准备好,我们开始创建子弹对象,如下面代码:

/**
 * Creates the Bullet object which the ship fires. The bullets are
 * drawn on the "main" canvas.
 */
function Bullet() {
    this.alive = false; // Is true if the bullet is currently in use
    /*
     * Sets the bullet values
     */
    this.spawn = function(x, y, speed) {
        this.x = x;
        this.y = y;
        this.speed = speed;
        this.alive = true;
    };
    /*
     * Uses a "drity rectangle" to erase the bullet and moves it.
     * Returns true if the bullet moved off the screen, indicating that
     * the bullet is ready to be cleared by the pool, otherwise draws
     * the bullet.
     */
    this.draw = function() {
        this.context.clearRect(this.x, this.y, this.width, this.height);
        this.y -= this.speed;
        if (this.y <= 0 - this.height) {
            return true;
        }
        else {
            this.context.drawImage(imageRepository.bullet, this.x, this.y);
        }
    };
    /*
     * Resets the bullet values
     */
    this.clear = function() {
        this.x = 0;
        this.y = 0;
        this.speed = 0;
        this.alive = false;
    };
}
Bullet.prototype = new Drawable();
子弹对象初始化状态设置alive为false,子弹对象包含三个方法spawn,draw和clear,仔细看draw方法可以看到if (this.y <= 0 - this.height),代表子弹已经在屏幕中消失,这里返回的true。

脏矩形

在刚才的draw方法中,我们使用了一个被称作脏矩形的技术,我们仅仅清除(clearRect)子弹前一瞬间的矩形区域,而不是整个屏幕。如果子弹离开了屏幕,draw方法返回true,指示该子弹对象可以被再回收;否则我们重绘它。

脏矩形的使用是我们开始创建3个画布的原因。如果我们只有一个画布,我们不得不在每一帧中,重绘所有的对象,屏幕背景,子弹,飞船。如果我们使用2个画布,背景一个画布,子弹和飞船共用一个,那么子弹每一帧将被重绘,而飞船只有移动的时候才被重绘,如果子弹和飞船发生重叠,子弹的区域的清除将使得飞船不再完整,直到我们移动飞船,这将大大地影响可视效果。所以这里我们采用3个画布。

飞船对象

我们最后要创建的是飞船对象,代码如下:

/**
 * Create the Ship object that the player controls. The ship is
 * drawn on the "ship" canvas and uses dirty rectangles to move
 * around the screen.
 */
function Ship() {
    this.speed = 3;
    this.bulletPool = new Pool(30);
    this.bulletPool.init();
    var fireRate = 15;
    var counter = 0;
    this.draw = function() {
        this.context.drawImage(imageRepository.spaceship, this.x, this.y);
    };
    this.move = function() {
        counter++;
        // Determine if the action is move action
        if (KEY_STATUS.left || KEY_STATUS.right ||
            KEY_STATUS.down || KEY_STATUS.up) {
            // The ship moved, so erase it‘s current image so it can
            // be redrawn in it‘s new location
            this.context.clearRect(this.x, this.y, this.width, this.height);
            // Update x and y according to the direction to move and
            // redraw the ship. Change the else if‘s to if statements
            // to have diagonal movement.
            if (KEY_STATUS.left) {
                this.x -= this.speed
                if (this.x <= 0) // Keep player within the screen
                    this.x = 0;
            } else if (KEY_STATUS.right) {
                this.x += this.speed
                if (this.x >= this.canvasWidth - this.width)
                    this.x = this.canvasWidth - this.width;
            } else if (KEY_STATUS.up) {
                this.y -= this.speed
                if (this.y <= this.canvasHeight/4*3)
                    this.y = this.canvasHeight/4*3;
            } else if (KEY_STATUS.down) {
                this.y += this.speed
                if (this.y >= this.canvasHeight - this.height)
                    this.y = this.canvasHeight - this.height;
            }
            // Finish by redrawing the ship
            this.draw();
        }
        if (KEY_STATUS.space && counter >= fireRate) {
            this.fire();
            counter = 0;
        }
    };
    /*
     * Fires two bullets
     */
    this.fire = function() {
        this.bulletPool.getTwo(this.x+6, this.y, 3,
                               this.x+33, this.y, 3);
    };
}
Ship.prototype = new Drawable();

飞船对象设置自己的移动速度为3, 子弹对象池大小为30,开火帧率为15。

KEY_STATUS对象的代码如下:

// The keycodes that will be mapped when a user presses a button.
// Original code by Doug McInnes
KEY_CODES = {
  32: ‘space‘,
  37: ‘left‘,
  38: ‘up‘,
  39: ‘right‘,
  40: ‘down‘,
}
// Creates the array to hold the KEY_CODES and sets all their values
// to false. Checking true/flase is the quickest way to check status
// of a key press and which one was pressed when determining
// when to move and which direction.
KEY_STATUS = {};
for (code in KEY_CODES) {
  KEY_STATUS[ KEY_CODES[ code ]] = false;
}
/**
 * Sets up the document to listen to onkeydown events (fired when
 * any key on the keyboard is pressed down). When a key is pressed,
 * it sets the appropriate direction to true to let us know which
 * key it was.
 */
document.onkeydown = function(e) {
  // Firefox and opera use charCode instead of keyCode to
  // return which key was pressed.
  var keyCode = (e.keyCode) ? e.keyCode : e.charCode;
  if (KEY_CODES[keyCode]) {
    e.preventDefault();
    KEY_STATUS[KEY_CODES[keyCode]] = true;
  }
}
/**
 * Sets up the document to listen to ownkeyup events (fired when
 * any key on the keyboard is released). When a key is released,
 * it sets teh appropriate direction to false to let us know which
 * key it was.
 */
document.onkeyup = function(e) {
  var keyCode = (e.keyCode) ? e.keyCode : e.charCode;
  if (KEY_CODES[keyCode]) {
    e.preventDefault();
    KEY_STATUS[KEY_CODES[keyCode]] = false;
  }
}
最后的步骤

最后我们需要更新游戏对象以及动画功能,代码如下:

 /**
 * Creates the Game object which will hold all objects and data for
 * the game.
 */
function Game() {
    /*
     * Gets canvas information and context and sets up all game
     * objects.
     * Returns true if the canvas is supported and false if it
     * is not. This is to stop the animation script from constantly
     * running on browsers that do not support the canvas.
     */
    this.init = function() {
        // Get the canvas elements
        this.bgCanvas = document.getElementById(‘background‘);
        this.shipCanvas = document.getElementById(‘ship‘);
        this.mainCanvas = document.getElementById(‘main‘);
        // Test to see if canvas is supported. Only need to
        // check one canvas
        if (this.bgCanvas.getContext) {
            this.bgContext = this.bgCanvas.getContext(‘2d‘);
            this.shipContext = this.shipCanvas.getContext(‘2d‘);
            this.mainContext = this.mainCanvas.getContext(‘2d‘);
            // Initialize objects to contain their context and canvas
            // information
            Background.prototype.context = this.bgContext;
            Background.prototype.canvasWidth = this.bgCanvas.width;
            Background.prototype.canvasHeight = this.bgCanvas.height;
            Ship.prototype.context = this.shipContext;
            Ship.prototype.canvasWidth = this.shipCanvas.width;
            Ship.prototype.canvasHeight = this.shipCanvas.height;
            Bullet.prototype.context = this.mainContext;
            Bullet.prototype.canvasWidth = this.mainCanvas.width;
            Bullet.prototype.canvasHeight = this.mainCanvas.height;
            // Initialize the background object
            this.background = new Background();
            this.background.init(0,0); // Set draw point to 0,0
            // Initialize the ship object
            this.ship = new Ship();
            // Set the ship to start near the bottom middle of the canvas
            var shipStartX = this.shipCanvas.width/2 - imageRepository.spaceship.width;
            var shipStartY = this.shipCanvas.height/4*3 + imageRepository.spaceship.height*2;
            this.ship.init(shipStartX, shipStartY, imageRepository.spaceship.width,
                           imageRepository.spaceship.height);
            return true;
        } else {
            return false;
        }
    };
    // Start the animation loop
    this.start = function() {
        this.ship.draw();
        animate();
    };
}
/**
 * The animation loop. Calls the requestAnimationFrame shim to
 * optimize the game loop and draws all game objects. This
 * function must be a gobal function and cannot be within an
 * object.
 */
function animate() {
    requestAnimFrame( animate );
    game.background.draw();
    game.ship.move();
    game.ship.bulletPool.animate();
}
 
时间: 2024-08-24 19:36:26

Step by Step 使用HTML5开发一个星际大战游戏(2)的相关文章

Step by Step 使用HTML5开发一个星际大战游戏(1)

本系列博文翻译自以下文章 http://blog.sklambert.com/html5-canvas-game-panning-a-background/ Languages: HTML5, JavaScript Code: https://github.com/straker/galaxian-canvas-game/tree/master/part1 Part 1 最终的游戏演示界面如下: 控制:移动 –  (←↑↓→)箭头 射击 – 空格 The HTML5 Page <!DOCTYPE

使用HTML5开发Kinect体感游戏

一.简介 我们要做的是怎样一款游戏? 在前不久成都TGC2016展会上,我们开发了一款<火影忍者手游>的体感游戏,主要模拟手游章节<九尾袭来 >,用户化身四代,与九尾进行对决,吸引了大量玩家参与. 表面上看,这款游戏与其它体感体验无异,实际上,它一直运行于浏览器Chrome下,也就是说,我们只需要掌握前端相应技术,就可以开发基于Kinect的网页体感游戏. 二.实现原理 实现思路是什么? 使用H5开发基于Kinect的体感游戏,其实工作原理很简单,由Kinect采集到玩家及环境数据

使用Cocos2dx-JS开发一个飞行射击游戏

一.前言 笔者闲来无事,某天github闲逛,看到了游戏引擎的专题,引起了自己的兴趣,于是就自己捣腾了一下Cocos2dx-JS.由于是学习,所谓纸上得来终觉浅,只是看文档看sample看demo,并不会让自己有多大的提升,于是一开始就计划写一个小游戏,以作为自己完成这个阶段学习的一个标志,也算是目标导向吧.完整源码移步Github: https://github.com/RogerKang/JasonAmbitionOnline Demo: http://www.rogerkang.site:

HTML5 - 开发一个自己的websocket服务器

应用:node.js 主要步骤: 创建文件夹 创建app.js(server入口,app为自定义命名) npm init -y (快速创建一个package.json文件) 依赖包安装:nodejs-websocket (github安装讲解) npm i nodejs-websocket 依赖包在appjs中的使用 (github how to use讲解) https://github.com/sitegui/nodejs-websocket#how-to-use-it 配置完毕后启动: 命

Nginx 模块开发(1)—— 一个稍稍能说明问题模块开发 Step By Step 过程

1. Nginx 介绍 Nginx是俄罗斯人编写的十分轻量级的HTTP服务器,它的发音为“engine X”, 是一个高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/SMTP 代理服务器.Nginx是由俄罗斯人 Igor Sysoev为俄罗斯访问量第二的 Rambler.ru站点开发的,从2004年开始它已经在该站点运行了七八年了.Igor Sysoev在建立的项目时,使用基于BSD许可. 英文主页:http://nginx.org. Nginx以事件驱动的方式编写,所以有非常

C++开发WPF,Step by Step

示例代码 使用C++来开发WPF,主要是如何在MFC(Win32)的窗口中Host WPF的Page.下面我就做个详细的介绍. 一.创建工程, 由于MFC的Wizard会生成很多用不到的代码,所以我准备从一个空的工程开始创建一个MFC的工程. a)         打开VS2005,菜单File->New->Projects-, 左面选择Visual C++->Win32,右面选择Win32 Console Application,给工程起个名字CPlusPlus_WPF, Ok进入下一

HTML5实战教程———开发一个简单漂亮的登录页面

最近看过几个基于HTML5开发的移动一样,已经越来越流畅了,相信随着职能手机的配置越来越高性能越来越好,HTML5技术的使用在移动端应用的会越来越普及,应用越来越广泛,因此作为移动开发者,掌握这门技术自然有着强烈的紧迫感.今天就写一个小小的登录页面的demo,巩固最近的学习,也给有兴趣的朋友学习HTML5技术做个参考. 在这里您可以下载到我最后实现的登录页面的demo源码,地址:http://pan.baidu.com/s/1kU1I50b. 准备工作 1.webStorm或者其他网页开发工具.

一个html5开发工具

今天推荐一个Html5开发工具 sublimetext3 找了一个注册码 可用 —– BEGIN LICENSE —– Michael Barnes Single User License EA7E-821385 8A353C41 872A0D5C DF9B2950 AFF6F667 C458EA6D 8EA3C286 98D1D650 131A97AB AA919AEC EF20E143 B361B1E7 4C8B7F04 B085E65E 2F5F5360 8489D422 FB8FC1AA

[py]python写一个通讯录step by step V3.0

python写一个通讯录step by step V3.0 参考: http://blog.51cto.com/lovelace/1631831 更新功能: 数据库进行数据存入和读取操作 字典配合函数调用实现switch功能 其他:函数.字典.模块调用 注意问题: 1.更优美的格式化输出 2.把日期换算成年龄 3.更新操作做的更优雅 准备工作 db准备 - 创建数据库 mysql> create database txl charset utf8; Query OK, 1 row affecte