首先向关卡中添加物体,对这些物体进行Box2D物理仿真,然后让它们在游戏中动起来。我们将使用这些物体创建测试关卡,加入鼠标交互,使游戏真正具有可玩性。最后,再向测试关卡加入音效和背景音乐,制作出一个完整的游戏。
4.1 定义物体
装载的物体:英雄、坏蛋、地面和环境中的障碍物。
属性type包含:"hero", "villain", "ground", "block"这样的值。
entities 对象
4.2 添加Box2D
创建box2d对象:
4.3 创建物体
定义entities.create方法:
4.4 向关卡加入物体
levels.array数组
levels.load
4.5 设置Box2D调试绘图
<canvas id="debugcanvas" width="1000" height="480" style="border:1px solid block;"></canvas>
仅仅是设计和测试关卡
4.6 绘制物体
entities对象内部定义draw()方法
4.7 Box2D动画
时间步长
box2d.step()
4.8 加载英雄
第一个阶段load-next-hero
game.countHeroesAndVillains();
4.9 发射英雄
game.mouseOnCurrentHero()
使用以下三个阶段实现发射英雄:
- wait-for-firing:游戏画面平移至弹弓,等待鼠标单击并拖动英雄,然后切换至firing阶段
- firing:游戏随着鼠标移动英雄,直到鼠标按键松开,此时以特定的速度将英雄抛射出去,并切换到fired阶段。抛射的速度基于英雄与弹弓的距离。
- fired:游戏画面跟随英雄移动,直至英雄静止下来或者飞出关卡边界之外,然后游戏将英雄从游戏世界中移除,并切换至load-next-hero阶段。
4.10 结束关卡
显示关卡结束画面
4.11 碰撞损坏
添加listener事件
4.12 绘制弹弓橡胶带
game.drawSlingshotBand()
4.13 切换关卡
重新开始关卡和开始下一个关卡的按钮
4.14 添加声音
首先,添加一些音效,如弹弓被释放、英雄或坏蛋弹跳、障碍物被摧毁的音效。
小技巧:在http://www.ccMixter.com中可以为自己的游戏找到一些免费又优质的音乐
设置断裂和反弹的音效
碰撞时播放弹跳声:listener.PostSolve
别摧毁时播放破坏声:
被发射时播放弹弓释放的声音
添加背景音乐:
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Froot Wars</title>
<script src="js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="js/Box2dWeb-2.1.a.3.min.js" type="text/javascript" charset="utf-8"></script>
<script src="js/game.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" charset="utf-8">
</head>
<body>
<div id="gamecontainer">
<canvas id="gamecanvas" width="640" height="480" class="gamelayer"></canvas>
<div id="scorescreen" class="gamelayer">
<img id="togglemusic" src="images/icons/sound.png" οnclick="game.toggleBackgroundMusic()">
<img src="images/icons/prev.png" οnclick="game.restartLevel();">
<span id="score">Score: 0</span>
</div>
<div id="gamestartscreen" class="gamelayer">
<img src="images/icons/play.png" alt="Play Game" οnclick="game.showLevelScreen();"><br>
<img src="images/icons/settings.png" alt="Settings">
</div>
<div id="levelselectscreen" class="gamelayer">
</div>
<div id="loadingscreen" class="gamelayer">
<div id="loadingmessage"></div>
</div>
<div id="endingscreen" class="gamelayer">
<div>
<p id="endingmessage"> The Level Is Over Message<p>
<p id="playcurrentlevel" οnclick="game.restartLevel();"><img src="images/icons/prev.png" >Replay Current Level</p>
<p id="playnextlevel" οnclick="game.startNextLevel();"><img src="images/icons/next.png"> Play Next Level </p>
<p id="showLevelScreen" οnclick="game.showLevelScreen();"><img src="images/icons/return.png"> Return to Level Screen</p>
</div>
</div>
</div>
<canvas id="debugcanvas" width="1000" height="480" style="border:1px solid block;"></canvas>
</body>
</html>
style.css
#gamecontainer {
width: 640px;
height: 480px;
background: url(images/splashscreen.png);
border: 1px solid block;
}
.gamelayer {
width: 640px;
height: 480px;
position: absolute;
display: none;
}
/* 开始菜单画面 */
#gamestartscreen {
padding-top:250px;
text-align:center;
}
#gamestartscreen img{
margin:10px;
cursor:pointer;
}
/* 关卡选择画面 */
#levelselectscreen {
padding-top: 150px;
padding-left: 50px;
}
#levelselectscreen input {
margin: 20px;
cursor: pointer;
background: url(images/icons/level.png) no-repeat;
color: yellow;
font-size: 20px;
width: 64px;
height: 64px;
border: 0;
}
/* 加载画面 */
#loadingscreen {
background: rgba(100,100,100,0.3);
}
#loadingmessage {
margin-top:400px;
text-align:center;
height: 48px;
color:white;
background:url(images/loader.gif) no-repeat center;
font:12px Arial;
}
/* 计分板 */
#scorescreen {
height: 60px;
font: 32px Comic Sans MS;
text-shadow: 0 0 2px #000;
color: white;
}
#scorescreen img{
opacity: 0.6;
top: 10px;
position: relative;
padding-left: 10px;
cursor: pointer;
}
#scorescreen #score {
position: absolute;
top: 5px;
right: 20px;
}
/* 结束画面 */
endingscreen {
text-align: center;
}
#endingscreen div {
height: 430px;
padding-top: 50px;
border: 1px;
background: rgba(1,1,1,0.5);
text-align: left;
padding-left: 100px;
}
#endingscreen p {
font: 20px Comic Sans MS;
text-shadow: 0 0 2px #000;
color: white;
}
#endingscreen p img {
top: 10px;
position: relative;
cursor: pointer;
}
#endingscreen #endingmessage {
font: 32px Comic Sans MS;
text-shadow: 0 0 2px #000;
color: white;
}
game.js
// Declare all the commonly used objects as variables for convenience
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
// 建立requestAnimationFrame和cancelAnimationFrame以在游戏代码中使用
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var x=0; x<vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
window[vendors[x] + 'CancelAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16-(currTime-lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);},
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame){
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
//
var game = {
//开始初始化对象,预加载资源,并显示开始画面
init: function(){
// 初始化对象
levels.init();
loader.init();
mouse.init();
//加载所有的音效及背景音乐
//由Gurdonark创作的"Kindergarten"
game.backgroundMusic = loader.loadSound('audio/gurdonark-kindergarten');
game.slingshotReleaseSound = loader.loadSound('audio/released');
game.bounceSound = loader.loadSound('audio/bounce');
game.breakSound = {
"glass": loader.loadSound('audio/glassbreak'),
"wood": loader.loadSound('audio/woodbreak')
};
//隐藏所有的游戏图层,显示开始画面
$('.gamelayer').hide();
$('#gamestartscreen').show();
//获取游戏画布及其绘图环境的引用
game.canvas = $('#gamecanvas')[0];
game.context = game.canvas.getContext('2d');
},
showLevelScreen: function(){
$('.gamelayer').hide();
$('#levelselectscreen').show('slow');
},
//控制背景音乐的方法
startBackgroundMusic: function() {
var toggleImage = $("#togglemusic")[0];
game.backgroundMusic.play();
toggleImage.src = "images/icons/sound.png";
},
stopBackgroundMusic: function() {
var toggleImage = $("#togglemusic")[0];
toggleImage.src = "images/icons/nosound.png";
game.backgroundMusic.pause();
game.backgroundMusic.currentTime = 0;
},
toggleBackgroundMusic: function() {
var toggleImage = $("#togglemusic")[0];
if (game.backgroundMusic.paused) {
game.backgroundMusic.play();
toggleImage.src = "images/icons/sound.png";
} else {
toggleImage.src = "images/icons/nosound.png";
game.backgroundMusic.pause();
}
},
// 游戏阶段
mode: "intro", //游戏状态(intro,waiting for firing, firing, fired)
// 弹弓的x和y坐标
slingshotX: 140,
slingshotY: 280,
start: function() {
//隐藏其他所有的图层
$('.gamelayer').hide();
//显示游戏画布和得分
$('#gamecanvas').show();
$('#scorescreen').show();
game.startBackgroundMusic();
game.mode = "intro";
game.offsetLeft = 0; //记录游戏画面在关卡中平移的距离
game.ended = false;
game.animationFrame = window.requestAnimationFrame(game.animate, game.canvas);
},
// 画面最大平移速度,单位为像素每帧
maxSpeed: 3,
// 画面最大和最小平移范围
minOffset: 0,
maxOffset: 300,
// 画面当前平移位置
offsetLeft: 0,
// 游戏得分
score: 0,
// 画面中心移动到newCenter
panTo: function(newCenter) {
if (Math.abs(newCenter - game.offsetLeft - game.canvas.width/4) > 0
&& game.offsetLeft <= game.maxOffset && game.offsetLeft >= game.minOffset) {
var deltaX = Math.round((newCenter - game.offsetLeft - game.canvas.width/4)/2);
if (deltaX && Math.abs(deltaX) > game.maxSpeed) {
deltaX = game.maxSpeed*Math.abs(deltaX)/(deltaX);
}
game.offsetLeft += deltaX;
} else {
return true;
}
if (game.offsetLeft < game.minOffset) {
game.offsetLeft = game.minOffset;
return true;
} else if (game.offsetLeft > game.maxOffset) {
game.offsetLeft = game.maxOffset;
return true;
}
return false;
},
countHeroesAndVillains:function(){
game.heroes = [];
game.villains = [];
for (var body = box2d.world.GetBodyList(); body; body = body.GetNext()) {
var entity = body.GetUserData();
if(entity){
if(entity.type == "hero"){
game.heroes.push(body);
} else if (entity.type =="villain"){
game.villains.push(body);
}
}
}
},
//鼠标是否放置在当前英雄上
mouseOnCurrentHero:function(){
if(!game.currentHero){
return false;
}
var position = game.currentHero.GetPosition();
var distanceSquared = Math.pow(position.x*box2d.scale - mouse.x-game.offsetLeft,2) + Math.pow(position.y*box2d.scale-mouse.y,2);
var radiusSquared = Math.pow(game.currentHero.GetUserData().radius,2);
return (distanceSquared<= radiusSquared);
},
showEndingScreen: function(){
game.stopBackgroundMusic();
if (game.mode == "level-success"){
if (game.currentLevel.number < levels.data.length-1){
$('#endingmessage').html('Level Complete. Well Done!!!');
$('#playnextlevel').show();
} else {
$('#endingmessage').html('All Levels Complete. Well Done!!!');
$('#playnextlevel').show();
}
} else if (game.mode == "level-failure"){
$('#endingmessage').html('Failed. Play Again?');
$('#playnextlevel').show();
}
$('#endingscreen').show();
},
handlePanning: function() {
if (game.mode == "intro"){
if (game.panTo(700)) {
game.mode = "load-next-hero";
}
}
if (game.mode == "load-next-hero"){
game.countHeroesAndVillains();
// 检查是否有坏蛋还活着,如果没有,结束关卡
if (game.villains.length == 0){
game.mode = "level-success";
return;
}
// 检查是否还有可装填英雄,如果没有,结束关卡
if (game.heroes.length == 0){
game.mode = "level-failure";
return;
}
// 装填英雄
if (!game.currentHero){
game.currentHero = game.heroes[game.heroes.length-1];
game.currentHero.SetPosition({x:180/box2d.scale,y:200/box2d.scale});
game.currentHero.SetLinearVelocity({x:0,y:0});
game.currentHero.SetAngularVelocity(0);
game.currentHero.SetAwake(true);
} else {
//等待英雄结束弹跳并进入休眠
game.panTo(game.slingshotX);
if (!game.currentHero.IsAwake()){
game.mode = "wait-for-firing";
}
}
}
if (game.mode == "wait-for-firing"){
if (mouse.dragging) {
if (game.mouseOnCurrentHero()){
game.mode = "firing";
} else {
game.panTo(mouse.x + game.offsetLeft);
}
} else {
game.panTo(game.slingshotX);
}
}
if (game.mode == "firing"){
if (mouse.down) {
game.panTo(game.slingshotX);
game.currentHero.SetPosition({x:(mouse.x + game.offsetLeft)/box2d.scale,
y:mouse.y/box2d.scale});
} else {
game.mode = "fired";
game.slingshotReleaseSound.play();
var impulseScaleFactor = 0.75;
var impulse = new b2Vec2((game.slingshotX+35-mouse.x-game.offsetLeft)*
impulseScaleFactor, (game.slingshotY+25-mouse.y)*impulseScaleFactor);
game.currentHero.ApplyImpulse(impulse, game.currentHero.GetWorldCenter());
}
}
if (game.mode == "fired"){
// 跟随当前英雄移动画面
var heroX = game.currentHero.GetPosition().x*box2d.scale;
game.panTo(heroX);
//直到该英雄停止移动或移除边界
if (!game.currentHero.IsAwake() || heroX<0
|| heroX>game.currentLevel.foregroundImage.width){
//然后删除旧的英雄
box2d.world.DestroyBody(game.currentHero);
game.currentHero = undefined;
//加载下一个英雄
game.mode = "load-next-hero";
}
}
if (game.mode == "level-success" || game.mode == "level-failure"){
if (game.panTo(0)) {
game.ended = true;
game.showEndingScreen();
}
}
},
animate: function() {
//移动背景
game.handlePanning();
//使角色运动
var currentTime = new Date().getTime();
var timeStep;
if (game.lastUpdateTime){
timeStep = (currentTime - game.lastUpdateTime)/1000;
box2d.step(timeStep);
}
game.lastUpdateTime = currentTime;
//使用视差滚动绘制背景,背景图像和前景图像以不同的速度移动,
//这个差异会造成一种错觉:背景上的云彩离我们更远
game.context.drawImage(game.currentLevel.backgroundImage,
game.offsetLeft/4, 0, 640, 480, 0, 0, 640, 480);
game.context.drawImage(game.currentLevel.foregroundImage,
game.offsetLeft, 0, 640, 480, 0, 0, 640, 480);
// 绘制弹弓
game.context.drawImage(game.slingshotImage, game.slingshotX-
game.offsetLeft, game.slingshotY);
// 绘制所有的物体
game.drawAllBodies();
//发射英雄时绘制橡胶带
if (game.mode == "firing"){
game.drawSlingshotBand();
}
// 再次绘制弹弓的外侧支架
game.context.drawImage(game.slingshotFrontImage, game.slingshotX-
game.offsetLeft, game.slingshotY);
if (!game.ended) {
game.animationFrame = window.requestAnimationFrame(game.animate, game.canvas);
}
},
//绘制弹弓橡胶带
drawSlingshotBand: function() {
game.context.strokeStyle = "rgb(68, 31, 11)"; //暗棕色
game.context.lineWidth = 6; //
//用英雄被拖拽的角度和半径计算英雄的末端,相对于英雄的中心
var radius = game.currentHero.GetUserData().radius;
var heroX = game.currentHero.GetPosition().x * box2d.scale;
var heroY = game.currentHero.GetPosition().y * box2d.scale;
var angle = Math.atan2(game.slingshotY+25-heroY, game.slingshotX+50-heroX);
var heroFarEdgeX = heroX - radius * Math.cos(angle);
var heroFarEdgeY = heroY - radius * Math.cos(angle);
game.context.beginPath();
//从弹弓顶端开始绘制(背面)
game.context.moveTo(game.slingshotX+50-game.offsetLeft, game.slingshotY+25);
//画到英雄的中心
game.context.lineTo(heroX-game.offsetLeft, heroY);
game.context.stroke();
//再次绘制英雄
entities.draw(game.currentHero.GetUserData(), game.currentHero.GetPosition(),
game.currentHero.GetAngle());
game.context.beginPath();
//移动到英雄离弹弓顶部最远的边缘
game.context.moveTo(heroFarEdgeX-game.offsetLeft, heroFarEdgeY+25);
//将线画回弹弓(正面)
game.context.lineTo(game.slingshotX-game.offsetLeft+10,game.slingshotY+30);
game.context.stroke();
},
//重新开始和下一关
restartLevel: function() {
window.cancelAnimationFrame(game.animationFrame);
game.lastUpdateTime = undefined;
levels.load(game.currentLevel.number);
},
startNextLevel: function() {
window.cancelAnimationFrame(game.animationFrame);
game.lastUpdateTime = undefined;
levels.load(game.currentLevel.number+1);
},
drawAllBodies: function() {
box2d.world.DrawDebugData();
// 遍历所有的物体,并在游戏canvas上绘制出来
for (var body=box2d.world.GetBodyList(); body; body=body.GetNext()){
var entity = body.GetUserData();
if (entity){
//判断生命值,是否要显示
var entityX = body.GetPosition().x * box2d.scale;
if (entityX<0 || entityX>game.currentLevel.foregroundImage.width ||
(entity.health && entity.health<0)) {
box2d.world.DestroyBody(body);
if (entity.type == "villain"){
game.score += entity.calories;
$('#score').html('Score: ' + game.score);
}
if (entity.breakSound){
entity.breakSound.play();
}
} else {
entities.draw(entity, body.GetPosition(), body.GetAngle());
}
}
}
},
}
$(window).load(function() {
game.init();
});
// 关卡
var levels = {
//关卡数据
data: [
{ //第一关
foreground: 'desert-foreground',
background: 'clouds-background',
entities:[
{type:"ground", name:"dirt", x:500,y:440,width:1000,height:20,isStatic:true},
{type:"ground", name:"wood", x:185,y:390,width:30,height:80,isStatic:true},
{type:"block", name:"wood", x:520,y:380,angle:90,width:100,height:25},
{type:"block", name:"glass", x:520,y:280,angle:90,width:100,height:25},
{type:"villain", name:"burger",x:520,y:205,calories:590},
{type:"block", name:"wood", x:620,y:380,angle:90,width:100,height:25},
{type:"block", name:"glass", x:620,y:280,angle:90,width:100,height:25},
{type:"villain", name:"fries", x:620,y:205,calories:420},
{type:"hero", name:"orange",x:80,y:405},
{type:"hero", name:"apple",x:140,y:405},
]
},
{ //第二关
foreground: 'desert-foreground',
background: 'clouds-background',
entities:[
{type:"ground", name:"dirt", x:500,y:440,width:1000,height:20,isStatic:true},
{type:"ground", name:"wood", x:185,y:390,width:30,height:80,isStatic:true},
{type:"block", name:"wood", x:820,y:380,angle:90,width:100,height:25},
{type:"block", name:"wood", x:720,y:380,angle:90,width:100,height:25},
{type:"block", name:"wood", x:620,y:380,angle:90,width:100,height:25},
{type:"block", name:"glass", x:670,y:317.5,width:100,height:25},
{type:"block", name:"glass", x:770,y:317.5,width:100,height:25},
{type:"block", name:"glass", x:670,y:255,angle:90,width:100,height:25},
{type:"block", name:"glass", x:770,y:255,angle:90,width:100,height:25},
{type:"block", name:"wood", x:720,y:192.5,width:100,height:25},
{type:"villain", name:"burger",x:715,y:155,calories:590},
{type:"villain", name:"fries",x:670,y:405,calories:420},
{type:"villain", name:"sodacan",x:765,y:400,calories:150},
{type:"hero", name:"strawberry",x:30,y:415},
{type:"hero", name:"orange",x:80,y:405},
{type:"hero", name:"apple",x:140,y:405},
]
}
],
// 初始化关卡选择画面
init: function() {
var html = "";
for (var i=0; i<levels.data.length; i++) {
var level = levels.data[i];
html += '<input type="button" value="' + (i+1) + '">';
};
$('#levelselectscreen').html(html);
//单击按钮时加载关卡
$('#levelselectscreen input').click(function(){
levels.load(this.value - 1);
$('#levelselectscreen').hide();
});
},
// 为某一关加载所有的数据和图像
load: function(number){
box2d.init();//
//声明一个新的当前关卡对象
game.currentLevel = {number:number, hero:[]};
game.score = 0;
$('#score').html('Score: ' + game.score);
game.currentHero = undefined;
var level = levels.data[number];
//加载背景、前景和弹弓图像
game.currentLevel.backgroundImage = loader.loadImage("images/backgrounds/" + level.background + ".png");
game.currentLevel.foregroundImage = loader.loadImage("images/backgrounds/" + level.foreground + ".png");
game.slingshotImage = loader.loadImage("images/slingshot.png");
game.slingshotFrontImage = loader.loadImage("images/slingshot-front.png");
// 加载所有的物体
for (var i=level.entities.length-1; i>=0; i--){
var entity = level.entities[i];
entities.create(entity);
};
// 一旦所有的图像加载完成,就调用game.start()函数
if (loader.loaded){
game.start();
} else {
loader.onload = game.start;
}
}
}
//
var entities = {
//定义物体类型(玻璃、木材和地面),以及英雄和坏蛋(橙子、苹果和汉堡)
definitions:{
"glass":{
fullHealth:100,
density:2.4,
friction:0.4,
restitution:0.15,
},
"wood":{
fullHealth:500,
density:0.7,
friction:0.4,
restitution:0.4,
},
"dirt":{
density:3.0,
friction:1.5,
restitution:0.2,
},
"burger":{
shape:"circle",
fullHealth:40,
radius:25,
density:1,
friction:0.5,
restitution:0.4,
},
"sodacan":{
shape:"rectangle",
fullHealth:80,
width:40,
height:60,
density:1,
friction:0.5,
restitution:0.7,
},
"fries":{
shape:"rectangle",
fullHealth:50,
width:40,
height:50,
density:1,
friction:0.5,
restitution:0.6,
},
"apple":{
shape:"circle",
radius:25,
density:1.5,
friction:0.5,
restitution:0.4,
},
"orange":{
shape:"circle",
radius:25,
density:1.5,
friction:0.5,
restitution:0.4,
},
"strawberry":{
shape:"circle",
radius:15,
density:2.0,
friction:0.5,
restitution:0.4,
},
},
//根据物体创建Box2D物体,并将其加入世界
create:function(entity){
var definition = entities.definitions[entity.name];
if (!definition){
console.log("undefined entity name", entity.name);
return;
}
//console.log(entity);
switch (entity.type){
case "block": //简单的矩形
entity.health = definition.fullHealth;
entity.fullHealth = definition.fullHealth;
entity.shape = "rectangle";
entity.sprite = loader.loadImage("images/entities/"
+entity.name+".png");
entity.breakSound = game.breakSound[entity.name];
box2d.createRectangle(entity, definition);
break;
case "ground": //简单的矩形
entity.shape = "rectangle";
box2d.createRectangle(entity, definition);
break;
case "hero":
case "villain":
entity.health = definition.fullHealth;
entity.fullHealth = definition.fullHealth;
entity.sprite = loader.loadImage("images/entities/"
+entity.name+".png");
entity.shape = definition.shape;
entity.bounceSound = game.bounceSound;
if (definition.shape == "circle"){
entity.radius = definition.radius;
box2d.createCircle(entity, definition);
} else if (definition.shape == "rectangle") {
entity.width = definition.width;
entity.height = definition.height;
box2d.createRectangle(entity, definition);
}
break;
default:
console.log("Undefined entity type", entity.type);
break;
}
},
//以物体、物体的位置和角度为参数,在游戏画面中绘制物体
draw: function(entity, position, angle){
game.context.translate(position.x*box2d.scale-game.offsetLeft,
position.y*box2d.scale);
game.context.rotate(angle);
switch(entity.type){
case "block":
game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
entity.sprite.height, -entity.width/2-1, -entity.height/2-1,
entity.width+2, entity.height+2);
break;
case "villain":
case "hero":
if (entity.shape == "circle"){
game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
entity.sprite.height, -entity.radius-1, -entity.radius-1,
entity.radius*2+2, entity.radius*2+2);
} else if (entity.shape == "rectangle") {
game.context.drawImage(entity.sprite,0,0,entity.sprite.width,
entity.sprite.height, -entity.width/2-1, -entity.height/2-1,
entity.width+2, entity.height+2);
}
break;
case "ground":
break;
}
game.context.rotate(-angle);
game.context.translate(-position.x*box2d.scale + game.offsetLeft, -position.y*box2d.scale);
},
}
//创建box2d对象
var box2d = {
scale: 30,
init: function() {
//创建Box2D世界,大部分物理运算将在其中完成
var gravity = new b2Vec2(0, 9.8); //重力加速度9.8m/s^2
var allowSleep = true; //允许静止的物体进入休眠状态,休眠物体不参与物理仿真计算
box2d.world = new b2World(gravity, allowSleep);
//设置调试绘图
var debugContext = document.getElementById('debugcanvas').getContext('2d');
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(debugContext);
debugDraw.SetDrawScale(box2d.scale);
debugDraw.SetFillAlpha(0.3);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
box2d.world.SetDebugDraw(debugDraw);
//监听事件
var listener = new Box2D.Dynamics.b2ContactListener;
//参数为接触和冲击力(法向和切向冲击力)
listener.PostSolve = function(contact, impulse){
var body1 = contact.GetFixtureA().GetBody();
var body2 = contact.GetFixtureB().GetBody();
var entity1 = body1.GetUserData();
var entity2 = body2.GetUserData();
var impulseAlongNormal = Math.abs(impulse.normalImpulses[0]);
// 监听器被调用得有些太频繁了,滤去非常小的冲击
// 尝试不同的值后,5似乎比较好
if (impulseAlongNormal > 5) {
//如果对象有生命值,用冲击值消弱生命值
if (entity1.health) {
entity1.health -= impulseAlongNormal;
}
if (entity2.health) {
entity2.health -= impulseAlongNormal;
}
//如果物体具有弹跳音,则播放它
if (entity1.bounceSound){
entity1.bounceSound.play();
}
if (entity2.bounceSound){
entity2.bounceSound.play();
}
}
};
box2d.world.SetContactListener(listener);
},
step: function(timeStep){
if (timeStep > 2/60){
timeStep = 2/60;
}
box2d.world.Step(timeStep, 8, 3);
},
createRectangle:function(entity, definition){
var bodyDef = new b2BodyDef;
if (entity.isStatic) {
bodyDef.type = b2Body.b2_staticBody;
} else {
bodyDef.type = b2Body.b2_dynamicBody;
}
bodyDef.position.x = entity.x/box2d.scale;
bodyDef.position.y = entity.y/box2d.scale;
if (entity.angle){
bodyDef.angle = Math.PI*entity.angle/180;
}
var fixtureDef = new b2FixtureDef;
fixtureDef.density = definition.density;
fixtureDef.friction = definition.friction;
fixtureDef.restitution = definition.restitution;
fixtureDef.shape = new b2PolygonShape; //多边形
fixtureDef.shape.SetAsBox(entity.width/2/box2d.scale,
entity.height/2/box2d.scale); //60宽,100高
var body = box2d.world.CreateBody(bodyDef);
body.SetUserData(entity);
var fixture = body.CreateFixture(fixtureDef);
return body;
},
createCircle:function(entity, definition){
var bodyDef = new b2BodyDef;
if (entity.isStatic) {
bodyDef.type = b2Body.b2_staticBody;
} else {
bodyDef.type = b2Body.b2_dynamicBody;
}
bodyDef.position.x = entity.x/box2d.scale;
bodyDef.position.y = entity.y/box2d.scale;
if (entity.angle){
bodyDef.angle = Math.PI*entity.angle/180;
}
var fixtureDef = new b2FixtureDef;
fixtureDef.density = definition.density;
fixtureDef.friction = definition.friction;
fixtureDef.restitution = definition.restitution;
fixtureDef.shape = new b2CircleShape(entity.radius/box2d.scale);
var body = box2d.world.CreateBody(bodyDef);
body.SetUserData(entity);
var fixture = body.CreateFixture(fixtureDef);
return body;
},
}
//图像/声音资源加载器loader
var loader = {
loaded: true,
loadedCount: 0, //已加载的资源数
totalCount: 0, //需要被加载的资源总数
init: function(){
//检查浏览器支持的声音格式;
var mp3Support, oggSupport;
var audio = document.createElement('audio');
if (audio.canPlayType){
//
mp3Support = "" != audio.canPlayType('audio/mpeg');
oggSupport = "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
} else {
// audio标签不被支持
mp3Support = false;
oggSupport = false;
}
// 都不支持,就将soundFileExtn设置为undefined
loader.soundFileExtn = oggSupport?".ogg":mp3Support?".mp3":undefined;
console.log(loader.soundFileExtn);
},
loadImage: function(url){
this.totalCount++;
this.loaded = false;
$('#loadingscreen').show();
var image = new Image();
image.src = url;
image.onload = loader.itemLoaded;
return image;
},
soundFileExtn: ".ogg",
loadSound: function(url){
this.totalCount++;
this.loaded = false;
$('#loadingscreen').show();
var audio = new Audio();
audio.src = url + loader.soundFileExtn;
audio.addEventListener("canplaythrough", loader.itemLoaded, false);
return audio;
},
itemLoaded: function() {
loader.loadedCount++;
$('#loadingmessage').html('Loaded ' + loader.loadedCount + ' of ' + loader.totalCount);
if (loader.loadedCount === loader.totalCount) {
// loader完成了资源加载
loader.loaded = true;
$('#loadingscreen').hide();
if (loader.onload){
loader.onload();
loader.onload = undefined;
}
}
}
}
//处理鼠标事件
var mouse = {
x: 0,
y: 0,
down: false,
init: function() {
$('#gamecanvas').mousemove(mouse.mousemovehandler);
$('#gamecanvas').mousedown(mouse.mousedownhandler);
$('#gamecanvas').mouseup(mouse.mouseuphandler);
$('#gamecanvas').mouseout(mouse.mouseuphandler);
},
mousemovehandler: function(ev) {
var offset = $('#gamecanvas').offset();
mouse.x = ev.pageX - offset.left;
mouse.y = ev.pageY - offset.top;
if (mouse.down) {
mouse.dragging = true;
}
},
mousedownhandler: function(ev) {
mouse.down = true;
mouse.downX = mouse.x;
mouse.downY = mouse.y;
ev.originalEvent.preventDefault();
},
mouseuphandler: function(ev) {
mouse.down = false;
mouse.dragging = false;
}
}