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();
}
 

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

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

    本系列博文翻译自以下文章 http://blog.sklambert.com/html5-canvas-game-panning-a-background/ Languages: HTML5, Jav ...

  2. 使用HTML5开发Kinect体感游戏

    一.简介 我们要做的是怎样一款游戏? 在前不久成都TGC2016展会上,我们开发了一款<火影忍者手游>的体感游戏,主要模拟手游章节<九尾袭来 >,用户化身四代,与九尾进行对决, ...

  3. Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状)

    Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状) 本篇博客来给大家介绍怎样使用Lua这门语言来开发一个简单的小游戏-记数字踩白块. 游戏的流程是这种:在界面上生成5个数1~5字并显 ...

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

    一.前言 笔者闲来无事,某天github闲逛,看到了游戏引擎的专题,引起了自己的兴趣,于是就自己捣腾了一下Cocos2dx-JS.由于是学习,所谓纸上得来终觉浅,只是看文档看sample看demo,并 ...

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

    应用:node.js 主要步骤: 创建文件夹 创建app.js(server入口,app为自定义命名) npm init -y (快速创建一个package.json文件) 依赖包安装:nodejs- ...

  6. 从零开始手把手教你使用javascript+canvas开发一个塔防游戏01地图创建

    项目演示 项目演示地址: 体验一下 项目源码: 项目源码 代码结构 本节做完效果 游戏主页面 index.html <!DOCTYPE html PUBLIC "-//W3C//DTD ...

  7. 用Python做一个飞机大战游戏

    基于pygame的一款小游戏 这是我上半年做的一款小游戏,但是一直忘记了,现在才上传代码. github项目地址:StarMan 代码基于pygame,Python版本3.5.2运行正常. 游戏很简单 ...

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

    1. Nginx 介绍        Nginx是俄罗斯人编写的十分轻量级的HTTP服务器,它的发音为“engine X”, 是一个高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/S ...

  9. Step by Step: 基于MFC下的COM组件开发-Helloworld

    http://blog.csdn.net/sybifei/article/details/45008745 [这篇文章有问题, 仅供参考] http://blog.csdn.net/define_us ...

随机推荐

  1. (转载)--SG函数和SG定理【详解】

    在介绍SG函数和SG定理之前我们先介绍介绍必胜点与必败点吧. 必胜点和必败点的概念:        P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败.        N点:必胜点 ...

  2. js获取上个月日期

    javascript根据当前日期获取上个月日期 function lastMonthDate(){ var Nowdate = new Date(); var vYear = Nowdate.getF ...

  3. passwd讲解

    root:$dffjioowwf/:16274:0:999999:7::: 1用户名:密码:最近修改密码的日期:密码不能更改的天数:密码过期时间:密码需要更改期限到拉前7发出警告:宽限天数:帐号过期时 ...

  4. 白话TCP三次握手

    在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认: 第二次握 ...

  5. Linux管道符、重定向与环境变量

    ——<Linux就该这么学>笔记 输入输出重定向输入重定向 指把文件导入到命令中输出重定向 指把原本要输出到屏幕的数据信息写入到指定文件中 输出重定向 分为标准输出重定向和错误输出重定向 ...

  6. [ Openstack ] Openstack-Mitaka 高可用之 Rabbitmq-server 集群部署

    目录 Openstack-Mitaka 高可用之 概述    Openstack-Mitaka 高可用之 环境初始化    Openstack-Mitaka 高可用之 Mariadb-Galera集群 ...

  7. [ Openstack ] Openstack-Mitaka 高可用之 环境初始化

    目录 Openstack-Mitaka 高可用之 概述    Openstack-Mitaka 高可用之 环境初始化    Openstack-Mitaka 高可用之 Mariadb-Galera集群 ...

  8. lucene 排序

    (1)排序对一个文档里什么域都没存储,使用字符串排序会排在首位 (2)排序对一个文档里什么域都没存储,使用数字类型排序会默认给其赋值为0进行排序 (3)我们可以对数字类型的null值的文档进行代码控制 ...

  9. hdu 1316(大整数)

    How Many Fibs? Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)To ...

  10. LayerDate渲染多个class出现闪现问题的解决

    填写表单的时候有时候会需要添加一行表单的业务逻辑,而表单要用到LayerDate的话便不可避免的出现多个class的情况 这种情况下后面的class是无法渲染的,layerDate官网提出了解决方法: ...