【Canvas】HTML5游戏开发的基本流程+P2.js物理引擎实战开发

《HTML5游戏开发的基本流程》


* 1. HTML5的简述
* 2. HTML5游戏开发所需的环境与工具
* 2.1. 开发环境
* 2.1.1. 浏览器
* 2.1.2. 开发语言
* 2.1.3. 开发平台
* 2.2. 开发工具
* 2.2.1. 代码编辑器
* 2.2.2. 版本控制器
* 2.2.3. 游戏引擎
* 3. HTML5游戏程序开发前所需的准备工作
* 3.1. 游戏策划文档
* 3.1.1. 主策划
* 3.1.2. 玩法策划
* 3.1.3. 关卡策划
* 3.1.4. 剧情策划
* 3.2. 游戏画面设计
* 3.2.1. 原画设计
* 3.2.2. 场景制作
* 3.2.3. 角色制作
* 3.2.4. 动画制作
* 3.2.5. 界面设计
* 4. 基于p2物理引擎的HTML5游戏开发
* 4.1. 确定游戏文件架构
* 4.1.1. .git
* 4.1.2. img
* 4.1.3. video
* 4.1.4. index.html
* 4.1.5. p2.js
* 4.1.6. game.js
* 4.1.7. README.md
* 4.2. 准备好游戏的素材
* 4.2.1. 图片
* 4.2.2. 音频
* 4.3. 编写游戏入口文件 index.html
* 4.4. 编写游戏主逻辑代码 game.js
* 4.4.1. 创建游戏舞台
* 4.4.2. 创建物理世界
* 4.4.3. 初始化物理世界
* 4.4.4. 图片资源加载
* 4.4.5. 物理世界的显示映射
* 4.4.6. 碰撞检测
* 4.4.7. 激活物理世界
* 4.4.8. 渲染画面
* 4.4.9. 人机交互逻辑
* 4.4.10. 图片绘制
* 4.4.11. 开始游戏
* 4.4.12. 游戏音效
* 4.4.13. 计分系统
* 4.5. 调试游戏
* 5. HTML5游戏编写代码完成后的工作
* 5.1. 打包
* 5.1.1. Electron概述
* 5.1.2. 下载Electron
* 5.1.3. 构建游戏项目并打包
* 5.1.4. 运行游戏
* 5.2. 发布





第一章:HTML5的简述

2007年1月9日,苹果公司前首席执行官史蒂夫·乔布斯发布了第一代的 iPhone 手机,与其同时,还拉开了拒绝支持 flash 的帷幕,挥舞起 HTML5 这块大旗。在当时,网络上大部分的多媒体都是 flash 格式的多媒体,而 HTML5 的标准还没被制定完成,iphone 不支持 flash 就意味着网络上的 flash 多媒体应用在 iphone 上无法使用。

对此,史蒂夫·乔布斯一边在抵制 flash,一边在极力吹捧 HTML5,甚至还联合众多浏览器厂商参与到 HTML5 标准的制定中,同时鼓吹重度依赖 flash 的公司转投到 HTML5 的怀抱。2010年4月30日,史蒂夫·乔布斯在网络上发布了著名的《Flash之我见》,从此,苹果面对 flash 由被动变成了主动。随着苹果开发的一款完全开源的 HTML5 渲染引擎 WebKit 的使用越来越广泛,越来越多的公司看到了 HTML5 的前景,越来越多的网页开发者能够在不依赖 flash 的情况下创造先进的图形、排版、动画和动态效果。

2014年10月29日,万维网联盟宣布,HTML5 标准规范制定完成。这一消息一出,flash 似乎走到了尽头。2015年1月,Youtube 抛弃 Flash,把站下的视频默认播放方式切换为 HTML5;2015年12月,Adobe 合并 HTML5 与 flash 动画制作软件,更名为 Animate CC;2016年12月,谷歌浏览器开始全面支持 HTML5 。

目前,无论是 iphone 还是 Android 平台,都已经不再支持 flash ,意味着在移动领域,flash 几乎不存在了。而在 PC 领域,flash 还常常可见,尤其在国内,仍有相当一部分视频网站或网页广告仍然采用 flash ,从而导致国内的浏览器普遍采用双核 Webkit + IE 和支持 flash 来保持国内的用户体验。

第二章:HTML5游戏开发所需的环境与工具

本章主要介绍 HTML5 游戏的运行环境和一些开发工具。

2.1. 开发环境

2.1.1. 浏览器

HTML5 游戏的运行必须是在支持 HTML5 的环境中,所以在 HTML5 游戏开发之前要先配置好支持 HTML5 的开发环境,然后在开发过程中在开发环境中进行调试和修改代码的错误。

很方便的是,HTML5 游戏的开发环境并不像客户端游戏的配置那么繁杂,只需要下载一个支持 HTML5 的浏览器,运行后便是一个 HTML5 游戏的开发环境。目前市面上最新的浏览器几乎都支持 HTML5,所以可以随便选择一款主流浏览器作为开发环境。

在众多主流的浏览器中,选择 Chrome 或者 Firefox ,即谷歌浏览器或者火狐浏览器作为开发环境是最佳的选择。因为 Chrome 启动速度和载入网页速度相当快,而且自带的调试工具非常好用,常作为开发环境是第一选择。而 Firefox 相比较于 Chrome 更加稳定和安全,而且辅助开发的插件相当丰富,一般 HTML5 游戏在 Chrome 开发环境上开发完成后,还会用 Firefox 再测试一遍。一般 HTML5游戏 在这两款浏览器通过测试后,就可以做到兼容市面上大多数的浏览器。

2.1.2. 开发语言

HTML5 游戏的首要开发语言是 JavaScript,因为只有 JavaScript 代码运行在 HTML5 环境的浏览器才能调用 W3C 定义的 HTML5 标准 API。

另外,市面上有一些游戏引擎并非支持 JavaScript,而是使用 TypeScript、CoffeeScript 这类面向对象语言编写游戏逻辑代码,最终编译成 JavaScript 代码运行。

一般情况下,开发 HTML5 小游戏直接采用原生 JavaScript 语言开发即可;而开发 HTML5 中大型游戏,则需要选择一款 HTML5 游戏引擎或 HTML5 游戏框架来作为开发的基础,在这些基础上深入开发。

2.1.3. 开发平台

理论上来说,在任何一个平台都能开发 HTML5,因为 HTML5 具有很好的跨平台性,几乎每一个平台都能开发 HTML5 游戏,如 Windows、Mac、Linux、IOS、Android 等等。但一般选择在 Windows 或者 Mac 下开发 HTML5 游戏,因为这两个平台可用的开发工具更加丰富和成熟;而如果开发的 HTML5 游戏是多人同时在线的网络游戏,选择 Linux 来开发游戏的服务器端即后台部分更加合适;更多地,如果开发的 HTML5 游戏是手机游戏,则需要在 IOS、Android 平台下作运行测试。

2.2. 开发工具

2.2.1. 代码编辑器

在 HTML5 游戏代码的编写阶段,选择一款好用的代码编辑器可以大大提升开发速度。

如今,web 代码编辑器比较流行的有:Sublime text、Atom、Visual studio code。这三款编辑器在 Windows/Linux/Mac 多个平台都可使用。其中,Sublime text 编辑器启动速度极快、扩展性高、稳定性强,缺点是中文支持不够好;Atom 编辑器是开源软件,可定制性强、插件丰富,缺点是启动速度慢、不够稳定;Visual studio code 编辑器是 Microsoft 公司的开源软件,其启动速度快、扩展性高、稳定性强,插件丰富,相当于 Atom 的进化版。

一般情况下,HTML5 游戏的代码编写首选 Visual studio code 编辑器,因为 HTML5 游戏代码的编写可能不单只使用 HTML、CSS、JavaScript 语言编写的前端部分,可能还会包括使用 Node.js、PHP 语言来开发的后台部分。而 Visual studio code 能满足使用多种语言编写代码共同开发 HTML5 游戏项目,可以大大提高开发效率。

同样的,Sublime text 编辑器也能满足使用多种语言编写代码共同开发 HTML5 游戏项目。如果 HTML5 游戏项目中不涉及中文的编写,一般会首选 Sublime text 编辑器。

2.2.2. 版本控制器

版本控制器是对某项目文件夹进行跟踪,当文件发生改动时,会记录文件改动的位置并生成一个版本,当文件发生误改、误删时,可以恢复文件到之前的版本。

Git 是一个开源的分布式版本控制系统,常在多人协作开发的项目用于同步管理项目。当项目在开发中代码运行后发生巨大错误而难以修复时,使用 Git 就可以将项目回退到上一个稳定的版本从而避免产生巨大的损失。

HTML5 游戏项目在启动时就应使用 Git 来对项目进行管理。因为 JavaScript 脚本语言并不像编译型的严谨语言,当项目变得十分巨大时,JavaScript 代码容易发生命名冲突等等问题,而且问题会变得很难解决导致重写代码,所以使用版本控制可以很快速地将项目恢复到稳定版本。

2.2.3. 游戏引擎

游戏引擎是游戏发展至今,开发者总结、整合出来的一套应用于游戏开发的通用、可复用程序,包括:渲染引擎、物理引擎、动画引擎、碰撞检测系统、资源管理系统、场景管理系统、音效控制系统、网络通信系统等等。

在开发 HTML5 游戏时,如果要从头开始编写代码的话,就要从渲染程序、碰撞程序、音效程序等等基础程序开始编写,会大大降低开发速度和增加重复工作,而一款合格的游戏引擎恰好就已经包括这类基础程序,所以选择一款成熟游戏引擎来开发中大型游戏项目,能让开发者专注于游戏逻辑程序的开发,大大提高了开发速度和稳定性。

由于游戏引擎本身就已经具备了一定规模,所以适合开发中大型的游戏。而开发小型游戏时,可以自由、灵活地选择某些程序库,如物理引擎来辅助开发。

目前国内有三款成熟的 HTML5 游戏引擎:Layabox、Egret、Hilo。其中,Layabox 的性能最高,支持2D/3D渲染,适合中大型游戏开发;Egret 开发工具丰富,支持2D/3D渲染,适合多人协作开发;Hilo 是一款轻量级的专注于开发跨终端互动游戏的游戏 HTML5 游戏引擎,适合开发小型游戏。

第三章:HTML5游戏程序开发前所需的准备工作

3.1. 游戏策划文档

无论是开发 HTML5 游戏,还是开发别的客户端游戏,在程序开发之前,都要进行游戏的策划,并把策划方案写成文档形式供程序开发人员参考与执行。对于不同类型、规模的游戏,游戏策划的内容也会变得不一样,一般情况下,游戏策划包括以下部分:

3.1.1. 主策划

主策划是游戏项目的总策划,内容包括了游戏的整体概念、游戏的玩法设计、游戏的开发日程管理等。

3.1.2. 玩法策划

将主策划中的玩法设计部分细化,将游戏的初步玩法设计从概念转变为具体。换句话说,玩法策划就是将游戏玩法粗无巨细地描述并记录到文档中。

比如,初步的玩法设计是玩家用鼠标在屏幕上随意涂鸦一幅画,然后 NPC 就会评价画并出价收购,同时玩家可以与 NPC 谈判以获取更好的价格。根据这一初步的玩法设计,玩法策划就要制定更加具体的规则,如规定不同的 NPC 会对不同类型的画有不同的高低评价、不同的 NPC 与玩家的谈判能力也不一样、定义谈判的方式、定义玩家什么情况下的画会一毛不值等等细节。

3.1.3. 关卡策划

进一步地,关卡策划又对玩法策划更加地细化,目的是保持原有核心玩法的基础上,不断加入新的元素或者更新旧的游戏元素,来丰富游戏的整体。关卡策划的内容包括关卡场景的设计、关卡流程的设计、关卡难度的设计、关卡NPC的设计等等内容。

3.1.4. 剧情策划

剧情设计常常与关卡策划密不可分,两者互相协调、推进游戏的开发。剧情策划的内容包括游戏世界观的架构、游戏人物的背景故事、游戏任务的故事发展、游戏人物与玩家交互的对白等等内容。

3.2. 游戏画面设计

为了尽快能让程序开发人员上手开发程序,游戏画面设计常常与游戏策划是同步进行的。当游戏的策划阶段将要到达尾声时,游戏画面设计人员就要完成一定量的游戏素材供程序开发人员使用。

3.2.1. 原画设计

原画设计是将游戏的人、物设计概念可视化。比如游戏策划人员提出了一个宏伟壮观的场景概念,原画设计人员就要根据这个场景概念通过颜色、线条等画法初步画出这个宏伟壮观的场景。

3.2.2. 场景制作

场景制作是根据场景原画来设计并制作出符合程序开发的格式文件。比如一个三维的场景,场景制作人员要在三维场景制作的软件中制作三维场景并输出能应用于游戏引擎或程序的格式文件供程序开发人员使用。

3.2.3. 角色制作

角色制作是根据角色原画来设计并制作符合用于制作动画的格式文件。比如一个二维游戏人物角色,角色制作人员要将完整的人物角色的身体分离成多张图片供动画制作人员制作人物动画。

3.2.4. 动画制作

动画制作是对场景或角色添加动画效果并生成应用于游戏引擎或程序的格式文件供程序开发人员使用。

3.2.5. 界面设计

界面设计又称用户界面设计,是除游戏虚拟世界的场景和人物以外,包括按钮、文字、窗口等等用于与玩家交互的游戏设计元素。

第四章 基于p2物理引擎的HTML5游戏开发

本章节通过编写代码来实现一款小游戏来讲述 HTML5 游戏开发的基本流程。

假设要实现的这款小游戏是一款双人打乒乓球的游戏,即两个玩家通过控制各自的球拍去击打乒乓球的游戏。为了增加游戏的趣味性,这款游戏不采用现实的乒乓球体育规则,而是只要乒乓球落在地上,就当作一局结束。由于是双人游戏,所以电脑屏幕前左边的玩家通过键盘 WASD 键来控制游戏左边的球拍的方向,右边的玩家通过方向键来控制右边的球拍移动。在乒乓球每次发球后,只要乒乓球落到了左边的地上,则右边玩家得1分,否则则左边玩家得分。根据这个简单的游戏策划,游戏画面设计也可以很简洁:

【Canvas】HTML5游戏开发的基本流程+P2.js物理引擎实战开发_第1张图片

接下来正式开始开发这款 HTML5 小游戏的程序。

4.1. 确定游戏文件架构

游戏项目会随着开发的过程而逐渐增大文件量,往往很容易产生大量冗余文件而导致游戏项目变得难以管理,所以一般要在最开始的时候在游戏项目文件夹下做好游戏文件的分类,为不同类别的文件建立相应的文件夹以便管理。

project/

├─ .git/
├─ img/
├─ video/

├─ index.html
├─ p2.js
├─ game.js
├─ README.md

4.1.1. .git

.git 是使用 git 版本控制器对游戏项目文件初始化后生成的文件夹,在游戏开发的过程中一般不需理会。

4.1.2. img

img 文件夹用于保存游戏的图片素材,是游戏运行时访问最多的文件夹之一。

4.1.3. video

video 文件夹用于保存游戏的音效素材,也是游戏运行时访问最多的文件夹之一。

4.1.4. index.html

index.html 是游戏项目的核心文件之一,同时也是游戏的入口文件,作为游戏执行前第一个要访问的文件。

4.1.5. p2.js

p2.js 是于默奥大学的视觉交互模拟课程的成果——用 Javascript 编写的 HTML5 2D物理引擎,集成了各种复杂的物理公式和算法,可以应用于游戏开发中轻松地实现碰撞、反弹等物理现象的模拟。

4.1.6. game.js

game.js 是游戏项目的核心文件之一,基于p2 和 h5 canvas 来编写代码从而实现游戏的逻辑。

4.1.7. README.md

README.md 一般作为游戏的介绍文档。

4.2. 准备好游戏的素材

把游戏开发的编写代码过程中所要访问到的素材事先存放到相应的位置。

4.2.1. 图片

project/img

├─ ball.png //球
├─ bat.png  //球拍
├─ bg.png   //背景
├─ table.png    //乒乓桌

4.2.2. 音频

project/img

├─ attack.wav   //击球
├─ score.mp3    //得分

4.3. 编写游戏入口文件 index.html

HTML5 与 HMLT4 的差异是 HTML5 增加了不少的标签和定义了更多的API,同时语法也更为简洁明了。比如 HTML5 中使用 代替了 HTML4中的 长长的头文件。


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>《左右互搏乒乓球》title>
head>
<body>

    
    <script src="p2.js">script>

    
    <script src="game.js">script>

body>
html>

4.4. 编写游戏主逻辑代码 game.js

本节将详细介绍 HTML5 游戏的逻辑代码编写过程。

4.4.1. 创建游戏舞台

HTML5 游戏与 HTML5 网页的不同之处在于 HTML5 游戏是在一个固定好的区域内进行游戏画面渲染,这个区域大小一般情况下是指 标签定义好的大小;而 HTML5 网页渲染超文本的范围更大。

理论上来说,基于 标签来实现的效果,使用 “div+css+js” 网页编写的方式也能实现。但是使用 的标准绘图 API 所实现的效果,无论从执行效率还是开发效率都比网页的编写方式高上一大截。

所以,以下代码的作用是创建一个宽为800像素、高为400像素的 区域以2d渲染方式作为游戏的显示舞台。

//创建显示舞台
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 400;
canvas.style.backgroundColor = "#333";
canvas.style.backgroundImage = "url(img/bg.png)";
document.body.appendChild(canvas);

4.4.2. 创建物理世界

既然选择采用了 p2 物理引擎,那么在编写代码之前,首先要搞清楚,物理引擎怎么与渲染引擎协调工作。

定义了许多 API 支持脚本化客户端绘图操作,换句话说,就是 仅仅具备绘图的功能,游戏逻辑的编写还是要依靠 JavaScript 语言。但是,当游戏项目需要对现实世界有真实的反应时,即游戏需要如重力、碰撞、反弹等等复杂的物理效果时,就需要一款物理引擎来模拟计算出数据,然后 根据得出的数据来渲染游戏画面。

物理引擎与游戏显示舞台并非同一个概念,所以还需创建一个物理世界来计算物理现象反映的数据。在这里采用了 p2 物理引擎,当然使用 p2.js 定义的函数来创建一个物理世界。

//创建一个物理世界
var world = new p2.World({
    gravity:[0, -9.82]
});

4.4.3. 初始化物理世界

p2 是一款 2d 的物理引擎,其物理世界自然就有一个直角坐标系,横坐标轴为 x 轴,纵坐标轴为 y 轴,原点往右的 x 坐标为正数,原点往上的 y 坐标为正数。

p2 与其他的 JavaScript 物理引擎不同的地方在于,p2 的单位坐标为 50像素。简单来说,设 p2 坐标系中的坐标点为 (1, 0) ,则对应的像素坐标为 (50px, 0) 。所以在初始化物体的位置或宽高参数时,为了与 canvas 相匹配,一般会对像素级的数值除以50得到的数参与物理世界的运算,这一过程常被称为物理世界的显示映射

//创建地面
var groundShape = new p2.Plane();
var groundBody = new p2.Body({
    mass:0
});
groundBody.addShape(groundShape);

//创建左边的墙
var leftWallShape = new p2.Plane();
var leftWallBody = new p2.Body({
    position: [0, 0],
    angle: -Math.PI/2
});
leftWallBody.addShape(leftWallShape);

//创建右边的墙
var rightWallShape = new p2.Plane();
var rightWallBody = new p2.Body({
    position: [canvas.width/50, 0],
    angle: Math.PI/2
});
rightWallBody.addShape(rightWallShape);

//创建天花板
var topWallShape = new p2.Plane();
var topWallBody = new p2.Body({
    position: [0, canvas.height/50],
    angle: Math.PI
});
topWallBody.addShape(topWallShape);

//添加创建好的物体到物理世界
world.addBody(groundBody);
world.addBody(leftWallBody);
world.addBody(rightWallBody);
world.addBody(topWallBody);

4.4.4. 图片资源加载

如今年代的游戏,是建立在画面基础上的,评判一款游戏的好与坏,画面占了很大的比例。编写游戏画面的渲染代码之前,首先要将构成游戏画面的若干张图片加载到内存中,以便随时被访问。上一节提到,在 p2 物理世界运转时,所有的涉及位置或宽高或角度的运算,都要进行物理世界的显示映射,才能用于游戏画面的渲染。所以,游戏画面中要参与物理运算的若干张图片在加载至内存之后,也要创建对应的物体。

//定义乒乓桌
var tableShape, tableBody;

//定义兵乓球
var ballShape, ballBody;

//定义左边球拍
var leftShape, leftBody;

//定义右边球拍
var rightShape, rightBody;

//加载乒乓桌图片
var tableReady = false;
var tableImage = new Image();
tableImage.onload = function () {
    tableReady = true;  
    //设置形状和刚体
    tableShape = new p2.Box({
        width: tableImage.width/50, //图片宽度
        height: tableImage.height/50    //图片高度 
    });
    tableBody = new p2.Body({
        mass: 10,               //重力
        position: [canvas.width/100, (tableImage.height+100)/50]        //物理世界的位置
    });
    tableBody.addShape(tableShape);
    world.addBody(tableBody);
};
tableImage.src = "img/table.png";


//加载乒乓球
var ballReady = false;
var ballImage = new Image();
ballImage.onload = function () {
    ballReady = true;   
    //设置形状和刚体
    ballShape = new p2.Circle({
        radius: ballImage.width/100
    });
    ballBody = new p2.Body({
        mass: 1,                //重力
        position: [canvas.width/100, (canvas.height - ballImage.height)/50] //物理世界的位置
    });
    ballBody.addShape(ballShape);
};
ballImage.src = "img/ball.png";


//加载左边球拍
var leftReady = false;
var leftImage = new Image();
leftImage.onload = function () {
    leftReady = true;   
    //设置形状和刚体
    leftShape = new p2.Box({
        width: leftImage.width/50,  //图片宽度
        height: leftImage.height/50 //图片高度 
    });
    leftBody = new p2.Body({
        mass: 5,    //重力
        position: [100/50, canvas.height/100],  //物理世界的位置
        gravityScale: 0,
        fixedRotation: true,
        collisionResponse: true,
        angle: -(45 * Math.PI / 180)
    });
    leftBody.addShape(leftShape);
};
leftImage.src = "img/bat.png";


//加载右边球拍
var rightReady = false;
var rightImage = new Image();
rightImage.onload = function () {
    rightReady = true;  
    //设置形状和刚体
    rightShape = new p2.Box({
        width: rightImage.width/50, //图片宽度
        height: rightImage.height/50    //图片高度 
    });
    rightBody = new p2.Body({
        mass: 5,                //重力
        position: [(canvas.width-100)/50, canvas.height/100],   //物理世界的位置
        gravityScale: 0,
        fixedRotation: true,
        collisionResponse: true,
        angle: 45 * Math.PI / 180
    });
    rightBody.addShape(rightShape);
};
rightImage.src = "img/bat.png";

4.4.5. 物理世界的显示映射

在游戏代码运行的过程中,物理引擎会在不断地对物理世界中的物体计算出模拟物理现象的数据;而渲染引擎则不断地根据物理引擎计算出的数据对游戏画面渲染出反映物理现象的画面。

由于渲染引擎即 canvas 游戏舞台的坐标系与 p2 物理引擎的坐标系有很大的不同,所以在 canvas 渲染画面之前,需要获取符合其坐标系的数据,才能对画面做出正确的渲染。于是需要能获取物理世界中不同形状的物体经过物理引擎计算后的数据的函数。

/* 获取转化成舞台坐标的方块物体的坐标(方块物体中点坐标)、宽度、高度和角度 */
function getBoxCenterPos(shape, body){
    var w = shape.width*50;
    var h = shape.height*50;

    var x = body.interpolatedPosition[0];
    var y = body.interpolatedPosition[1];

    x = x*50;
    y = canvas.height - y*50;

    var angle = -body.interpolatedAngle;

    return {
        w : w,
        h : h,
        x : x, 
        y : y,
        angle : angle
    };
}

/* 获取转化成舞台坐标的圆形物体的坐标(圆形物体中点坐标)、半径和角度 */
function getCirclePos(shape, body){
    var r = shape.radius;

    var x = body.interpolatedPosition[0];
    var y = body.interpolatedPosition[1];

    var angle = body.interpolatedAngle * Math.PI / 180;

    r = r * 50;
    x = x * 50;
    y = canvas.height - y * 50;

    return {
        x : x, 
        y : y,
        r : r,
        angle : angle
    };
}

4.4.6. 碰撞检测

碰撞检测几乎是每一个游戏开发的编写代码过程中具备的功能,用于检测物体 A 是否与物体 B 发送碰撞从而实现碰撞后的游戏逻辑。

现在在 p2 物理世界中,除墙和天花板外已经添加了地面、乒乓桌、乒乓球和左右两边的乒乓球拍共 4 个物体。每个物体间发生碰撞会执行不同的游戏逻辑。

为了实现检测不同的物体之间发生碰撞而执行不同的游戏代码,一般需要监听碰撞事件,一旦事件检测到物体发生碰撞则立即执行事件处理函数。相比于其他物理引擎,p2 中的碰撞检测事件的编写十分简单和符合 JavaScript 语言的开发思维。

//检测物理碰撞
world.on("beginContact", function(event){

    //如果乒乓桌碰到了地面
    if((event.bodyA == groundBody && event.bodyB == tableBody) ||
       (event.bodyB == groundBody && event.bodyA == tableBody)){

        if(is_game_begin === false && ballReady === true && leftReady === true && rightReady === true){
            gameBegin();    //游戏开始
        }
    }

    //如果乒乓球碰撞了
    if(event.bodyA == ballBody || event.bodyB == ballBody){
        is_ball_hit = true;
    }

    //如果球碰到了地面
    if((event.bodyA == groundBody && event.bodyB == ballBody)){
        countScore();   //计算分数
    }
});

4.4.7. 激活物理世界

当物体添加到物理世界后,是处于静止状态的。即使物理世界开启了碰撞检测,物体也是处于静止状态,并没有开始模拟并计算物体的物理活动。

物理引擎中的物体运动与动画类似,将静止的物体变为动态。有区别的是,动画是连续播放一系列画面,给视觉造成连续变化的感觉。而物理引擎的工作是不断地在很小的时间间隔内,根据物体的参数应用物理公式来计算并更新物体参数,从而用参数来模拟物理世界的过程。

由于物理引擎与渲染引擎有共同的特点,所以可以很方便地将两者结合,根据物理世界的参数用画面来渲染出物理现象。简单来说,就是在一秒钟内,大约60次推进物理世界的计算且执行渲染函数。

//激活物理世界
requestAnimationFrame(activate);

//定义物理活动
var lastTime;
var maxSubSteps = 5;
var fixedDeltaTime = 1 / 60;
function activate(time){
    requestAnimationFrame(activate);

    // Get the elapsed time since last frame, in seconds
    var deltaTime = lastTime ? (time - lastTime) / 1000 : 0;
    lastTime = time;

    // Make sure the time delta is not too big (can happen if user switches browser tab)
    deltaTime = Math.min(1 / 10, deltaTime);

    // Move physics bodies forward in time 推进物理世界
    world.step(fixedDeltaTime, deltaTime, maxSubSteps);

    // Render scene 渲染游戏画面
    render();
}

4.4.8. 渲染画面

在渲染画面之前,首先要判断用于渲染的图片是否已经加载完成。

/* 检测是否准备就绪 */
function checkReady(){
    if(tableReady && ballReady && leftReady && rightReady){
        return true;
    }
    return false;
}

如果图片都加载完成,那就要先清除上一帧的画面,重新再绘制新的画面。根据这个思路,就可以完成定义上一节代码中物理世界推进计算之后的渲染函数。

/* 渲染画面 */
function render(){
    if(checkReady() === true){

        ctx.clearRect(0, 0, canvas.width, canvas.height);   //清除上一帧的画面

        drawTable();    //绘制乒乓桌

        if(is_game_begin === true){
            drawBall();     //绘制乒乓球
            drawLeftBat();  //绘制左边乒乓球拍
            drawRightBat(); //绘制右边乒乓球拍
            drawScore();    //绘制分数

            //键盘事件的处理
            checkAndExeKeyDown();   
        }
    }
}

4.4.9. 人机交互逻辑

每一款游戏,人机交互是重头戏。玩家对一款游感觉好不好玩,在于游戏交互性打造得好不好,从网页的角度来看,就是用户体验好不好。到目前为止,游戏项目中的物理世界已经在运转了,游戏画面的渲染也在执行中,但是还缺少了人机交互。人机交互的方式有很多种,在 PC 游戏中,玩家可以用键盘和鼠标来体验游戏;在游戏主机中,玩家用游戏手柄来操作游戏;在手机上,玩家可以通过触屏或重力感应等方式来参与到游戏中;更多地,VR/AR 设备的人机交互方式与传统的很不相同。

由于本游戏是 PC 端的 HTML5 双人游戏,所以采用键盘的交互方式。键盘中 WASD 键对应左边球拍的上左下右移动,↑←↓→ 键对应右边的球拍的上左下右移动。

由于采用了基于物理引擎来开发,所以当玩家按下相应键盘进行球拍的上下左右移动时,首先不是直接对用于渲染的图片进行位移,而是通过设置物体球拍的速度来参与到物理世界的运算,然后再由渲染引擎将画面渲染出来。

// 键盘交互
var checkAndExeKeyDown = function () {

    //右玩家
    if (38 in keysDown) { // Player holding up
        rightBody.velocity[1] = 5;
    }
    if (40 in keysDown) { // Player holding down
        rightBody.velocity[1] = -5;
    }
    if (37 in keysDown) { // Player holding left
        rightBody.velocity[0] = -5;
    }
    if (39 in keysDown) { // Player holding right
        rightBody.velocity[0] = 5;
    }

    //左玩家
    if (87 in keysDown) { // Player holding up
        leftBody.velocity[1] = 5;
    }
    if (83 in keysDown) { // Player holding down
        leftBody.velocity[1] = -5;
    }
    if (65 in keysDown) { // Player holding left
        leftBody.velocity[0] = -5;
    }
    if (68 in keysDown) { // Player holding right
        leftBody.velocity[0] = 5;
    }
};

4.4.10. 图片绘制

在每一帧绘制游戏画面中的图片之前,首先要获取物理世界在物理引擎的计算下得出的参数,然后根据参数来绘制图片到游戏画面中。

/* 绘制乒乓桌 */
function drawTable(){
    var pos = getBoxCenterPos(tableShape, tableBody);   //获取参数

    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(pos.angle);
    ctx.drawImage(tableImage, -pos.w/2, -pos.h/2);  
    ctx.restore();
}

/* 绘制乒乓球 */
function drawBall(){
    if(is_game_begin === true){
        var pos = getCirclePos(ballShape, ballBody);    //获取参数

        ctx.save();
        ctx.translate(pos.x, pos.y);
        ctx.rotate(pos.angle);
        ctx.drawImage(ballImage, -pos.r, -pos.r);   
        ctx.restore();
    }

    //检测到符合条件就重置球
    resetBall();    
}

基于物理引擎开发的游戏与动画或影视不一样,区别在于游戏具有交互性,往往游戏开发者对玩家所做的交互操作是难以预测的,所以游戏对比与实现设置好参数的影视更加容易出现渲染效果与物理现象不匹配的现象。

为了使渲染的画面与物理世界的效果更加一致,在绘制跟玩家有交互性的图片时,往往要执行一些额外的代码来协调物理引擎与玩家的操作。

//限制球拍移动速度的次数
var limit_left_bat_count = 0;
var limit_right_bat_count = 0;

/* 绘制左边乒乓拍 */
function drawLeftBat(){
    var pos = getBoxCenterPos(leftShape, leftBody);

    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(pos.angle);
    ctx.drawImage(leftImage, -pos.w/2, -pos.h/2);   
    ctx.restore();

    //设置大约10/60键盘反馈时间
    limit_left_bat_count += 1;
    if(limit_left_bat_count >= 10){
        limit_left_bat_count = 0;
        leftBody.velocity[0] = 0;
        leftBody.velocity[1] = 0;
    }
}

/* 绘制右边乒乓拍 */
function drawRightBat(){
    var pos = getBoxCenterPos(rightShape, rightBody);

    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(pos.angle);
    ctx.drawImage(rightImage, -pos.w/2, -pos.h/2);  
    ctx.restore();

    //设置大约10/60键盘反馈时间
    limit_right_bat_count += 1;
    if(limit_right_bat_count >= 10){
        limit_right_bat_count = 0;
        rightBody.velocity[0] = 0;
        rightBody.velocity[1] = 0;
    }
}

4.4.11. 开始游戏

当物理世界已经可以正常运转、渲染引擎已经绘制出游戏画面、人机交互方式的相关代码完成后,就可以开始游戏了。

游戏的开始,首先要执行相关的初始化工作,比如设置好物理世界中物体的材质参数、监听玩家的键盘交互事件等。

//是否开球了?
var is_game_begin = false;

// 键盘控制的参数
var keysDown = {};


/* 游戏开始 */
function gameBegin(){

    tableBody.mass = 1000;  //变得很重
    tableBody.updateMassProperties();   //更新重量

    //开球了
    is_game_begin = true;

    //添加球到物理世界
    world.addBody(ballBody);
    //添加球拍到物理世界
    world.addBody(leftBody);
    world.addBody(rightBody);


    //创建桌子和球的材质
    var tableMaterial = new p2.Material();
    var ballMaterial = new p2.Material();

    tableShape.material = tableMaterial;
    ballShape.material = ballMaterial;

    //设置桌子和球之间的材质约束
    var ballTableContactMaterial = new p2.ContactMaterial(ballMaterial, tableMaterial, {
        friction : 0.03,            //摩擦力
        restitution : 1     //弹性
    });
    world.addContactMaterial(ballTableContactMaterial);

    //创建球拍的材质
    var batMaterial = new p2.Material();
    leftShape.material = batMaterial;
    rightShape.material = batMaterial;

    //设置球拍和球之间的材质约束
    var ballBatContactMaterial = new p2.ContactMaterial(ballMaterial, batMaterial, {
        friction : 0.3,         //摩擦力
        restitution : 1     //弹性
    });
    world.addContactMaterial(ballBatContactMaterial);

    //创建墙面的材质
    var wallMaterial = new p2.Material();
    // groundShape.material = wallMaterial;
    leftWallShape.material = wallMaterial;
    rightWallShape.material = wallMaterial;
    topWallShape.material = wallMaterial;

    //设置球与墙面的材质约束
    var ballWallContactMaterial = new p2.ContactMaterial(ballMaterial, wallMaterial, {
        friction : 0.4,         //摩擦力
        restitution : 0.8       //弹性
    });
    world.addContactMaterial(ballWallContactMaterial);


    //键盘控制球拍
    addEventListener("keydown", function (e) {
        keysDown[e.keyCode] = true;
    }, false);

    addEventListener("keyup", function (e) {
        delete keysDown[e.keyCode];
    }, false);
}

4.4.12. 游戏音效

对于一款游戏,最重要的除了画面、玩法,还有音效。纵观市面上游戏,从90年代开始到如今,音效是一款游戏的必备要素。音效要做到与游戏的交互契合;音乐要做到与游戏的内容契合。HTML5 定义了 audio 对象,一般 HTML5 游戏都是使用 audio 对象来加载和播放游戏音效。

//加载音频
var attackSound = document.createElement("AUDIO");
attackSound.src = "video/attack.wav";
attackSound.loop = false;
attackSound.autoplay = false;
attackSound.load();

//加载音频
var scoreSound = document.createElement("AUDIO");
scoreSound.src = "video/score.mp3";
scoreSound.loop = false;
scoreSound.autoplay = false;
scoreSound.load();

在定义的碰撞检测中,当球拍与球碰撞时,就播放球拍的击打音效。

//乒乓球是否碰撞了
var is_ball_hit = false;

//每次发生碰撞结束后,检测碰撞,符合条件则播放击打音效
world.on("endContact", function(event){
    if(is_ball_hit === true){
        attackSound.play(); //播放音效

        is_ball_hit = false;
    }
});

4.4.13. 计分系统

作为一款游戏,输赢是必须要有的。定义输赢的方式多种多样,其中最简单莫过于计分,通过计分来定义玩家的表现是很常见的判断输赢的方式。根据碰撞检测中的设计,当球掉到地上的时候,就要开始计算分数。

//最后得分的球拍,true为右,否则为左
var is_right_last_win = true;

//球拍得分
var leftBodyScore = 0;
var rightBodyScore = 0;

//本次输赢是否已计分
var is_count_score = false;


/* 计分系统 */
function countScore(){

    //如果未计分
    if(is_count_score === false){
        is_count_score = true;

        var pos = getCirclePos(ballShape, ballBody);

        if(pos.x < canvas.width/2){
            rightBodyScore += 1;
            is_right_last_win = true;
        }
        else{
            leftBodyScore += 1;
            is_right_last_win = false;
        }

        //播放得分音效
        scoreSound.play();
    }
}

为了显示得分情况,在每次执行 render() 渲染时,还要绘制出双方的分数。

/* 绘制分数 */
function drawScore(){
    var pos = getBoxCenterPos(tableShape, tableBody);
    var score = leftBodyScore+" - "+rightBodyScore;

    ctx.font = "bold 40px Arial";
    ctx.fillStyle = "#eee";
    ctx.textAlign="center";
    ctx.fillText(score, pos.x, pos.y+30);
}

当分数有更新的时候,延时到60帧后,就重新发球,即重开一局。

//延时60帧
var delay_reset_time = 60;

/* 重置球的下落 */
function resetBall(){
    if(is_count_score === true){
        delay_reset_time -= 1;  //倒计时

        //如果倒计时完成
        if(delay_reset_time <= 0){

            //如果最后一次击球是右球拍
            if(is_right_last_win === true){
                ballBody.position[0] = (canvas.width/2 + tableImage.width/4)/50;
                ballBody.position[1] = (canvas.height - ballImage.height)/50;
            }
            else{   //如果最后一次击球是左球拍
                ballBody.position[0] = (canvas.width/2 - tableImage.width/4)/50;
                ballBody.position[1] = (canvas.height - ballImage.height)/50;
            }


            //重置两个标志变量
            delay_reset_time = 60;
            is_count_score = false;
        }
    }
}

4.5. 调试游戏

当游戏的主逻辑代码 game.js 编写完之后,就要开始调试游戏了。调试是为了查看游戏在执行的过程中是否会出现 BUG ,或者检测游戏是否表现得与预期的一致。

HTML5 的调试十分简单,在需要获取数值、对象详细信息的代码处,加入语句 console.log(相关变量名);。然后双击 index.html 文件打开至支持 HTML5 的浏览器。以 chrome 浏览器为例,打开 index.html 文件后,按下键盘中的 F12 键,即可在 console 控制台窗口查看到执行至函数 console.log 处输出到控制台的相关信息,或者查看到 bug 报错信息。

第五章 HTML5游戏编写代码完成后的工作

HTML5 的最大特性就是跨平台,有句流行的话叫 “一次编写,多平台运行” ,这句话的意思就是,代码只需要写一次,在各个平台如 Windows、Linux、Android、IOS 等等都能运行。市面上已经有不少 HTML5 跨平台的应用,比如网易云音乐、有道云词典等等。由于支持 HTML5 的浏览器内核已经十分成熟,只要在 HTML5 内核的环境下,HTML5 就能运行,而几乎每一个系统都支持 HTML5 内核,从而 HTML5 的跨平台性前所未有,越来越受到开发者的青睐。

另外,HTML5 始终未网页发展而来的技术,HTML5 在浏览器内运行才有最好的体验。但是为了更好地发挥 HTML5 的特性,市面上诞生了各种各样的打包技术,可以将 HTML5 打包成软件形式安装于各系统中使用。在手机系统方面,有开源的打包方案 Cordova,可以将 HTML5 应用打包在 Android、IOS、Ubuntu phone os、Windows Phone、Symbian 等常见系统中运行;在桌面应用方面,有开源的打包方案 nw.js ,可以将 HTML5 应用打包成多个系统的桌面应用,包括 Windows、Linux 等系统。

5.1. 打包

本节将介绍如何使用 Electron 框架将 HTML5 游戏打包成 Windows 下的可执行文件即桌面应用。

5.1.1. Electron概述

Electron 是一个几乎不同修改任何代码即可将网站打包成一个桌面应用的开源框架。 Electron 已经被 Microsoft、Facebook、Slack、Docker 等知名大公司用于程序开发之中。市面上的
Visual Studio Code、Hyper、Simplest 等著名软件是使用 Electron 技术来构建的。

所以 Electron 是一个相当成熟的框架,而且由于其实开源项目,全球许多开发者在为其进行维护和更新,以满足更多的开发需求。

5.1.2. 下载Electron

浏览器下访问 https://coding.net/u/linhongbijkm/p/Electron-packager-build-project/git ,点击下载即可将 Electron 下载到本地中。

5.1.3. 构建游戏项目并打包

下载 Electron 完成后,解压压缩包会得到 Electron-packager-build-project-master 文件夹,为了更加简便,将这个文件夹名修改为 electron 。然后把游戏项目中的所有文件复制并替换到 electron\resources\app\project 目录下。

接着,用文本编辑器打开 electron\resources\app\main.js ,将其第16行代码 mainWindow = new BrowserWindow({width: 800, height: 600}) 中的宽高值修改并保存为:

mainWindow = new BrowserWindow({width: 850, height: 460})

这样就只需三步即可对游戏项目完成打包。

5.1.4. 运行游戏

打包完成后,双击 electron\app.exe 可执行文件即可运行游戏。

5.2. 发布

如果采用对游戏项目打包成桌面应用的话,在网络上提供对 electron 文件夹的压缩包,解压后运行其中的 app.exe 即可。

如果采用原生的 HTML5 形式发布的话,由于 HTML5 就是网页,所以发布的过程与网页前端的发布方式一致。





p2物理引擎下载地址:github.com/schteppe/p2.js


以上代码效果演示地址:linhongbijkm.coding.me/p2.js-game/

你可能感兴趣的:(游戏开发,javascript,html5,程序员与人文)