HTML5遊戲開發進階 4 :物理引擎整合
首先向關卡中新增物體,對這些物體進行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" onclick="game.toggleBackgroundMusic()">
<img src="images/icons/prev.png" onclick="game.restartLevel();">
<span id="score">Score: 0</span>
</div>
<div id="gamestartscreen" class="gamelayer">
<img src="images/icons/play.png" alt="Play Game" onclick="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" onclick="game.restartLevel();"><img src="images/icons/prev.png" >Replay Current Level</p>
<p id="playnextlevel" onclick="game.startNextLevel();"><img src="images/icons/next.png"> Play Next Level </p>
<p id="showLevelScreen" onclick="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;
}
}
相關文章
- JavaScript 遊戲開發:手把手實現碰撞物理引擎JavaScript遊戲開發
- 不懂物理的前端不是好的遊戲開發者(一)—— 物理引擎基礎前端遊戲開發
- 【h5遊戲開發】egret引擎p2物理引擎(2) - 小球碰撞地面搞笑的物理現象H5遊戲開發
- 不懂物理的前端不是好的遊戲開發者(二)—— 物理引擎的學習之路前端遊戲開發
- 喵的Unity遊戲開發之路 - 推球:遊戲中的物理Unity遊戲開發
- 聚焦遊戲開發速度與成本,穩定引擎基礎助力開發者快速整合應用遊戲開發
- Three.js 進階之旅:物理效果-3D乒乓球小遊戲 ?JS3D遊戲
- 獨立遊戲開發中的物理系統遊戲開發
- Shopee Games 遊戲引擎演進之路GAM遊戲引擎
- 《無限法則》開發經驗分享:射擊遊戲的物理引擎應用和移動模擬遊戲
- 使用Laya引擎開發微信小遊戲(上)遊戲
- 使用Laya引擎開發微信小遊戲(下)遊戲
- HTML5遊戲開發過程中的二三事HTML遊戲開發
- 使用 .NET 進行遊戲開發遊戲開發
- 前端開發入門到實戰:HTML5進階FileReader的使用前端HTML
- 遊戲趣史:遊戲引擎的發展史遊戲引擎
- html5的遊戲引擎你瞭解多少?都有哪些比較好用的引擎呢?HTML遊戲引擎
- Facebook開發小遊戲引擎列表(下載連結)遊戲引擎
- HTML5遊戲開發(二):使用TypeScript編寫程式碼HTML遊戲開發TypeScript
- 遊戲與遊戲引擎遊戲引擎
- 阿里開源HTML5小遊戲開發框架Hilo實戰教程阿里HTML遊戲開發框架
- HTML5進階FileReader的使用HTML
- 微信小遊戲和白鷺引擎開發實踐遊戲
- HTML5遊戲開發(三):使用webpack構建TypeScript應用HTML遊戲開發WebTypeScript
- 【譯】闖入遊戲開發 #4:美術遊戲開發
- 遊戲引擎Cocos接入HMS Core,華為推送服務擁抱遊戲開發者!遊戲引擎遊戲開發
- three.js cannon.js物理引擎製作一個保齡球遊戲JS遊戲
- 【iOS開發進階】-RunTimeiOS
- Web 開發進階指南Web
- javascript遊戲引擎JavaScript遊戲引擎
- HTML5遊戲開發(一):3分鐘建立一個hello worldHTML遊戲開發
- .NET 開源工作流: Slickflow流程引擎高階開發(七)--訊息佇列(RabbitMQ)的整合使用佇列MQ
- 2022遊戲出海日本市場進階指南遊戲
- 遊戲開發入門(一)遊戲開發概述遊戲開發
- 【譯】闖入遊戲開發 #0-1:關於這部指南、遊戲引擎大清單遊戲開發遊戲引擎
- 前端進階系列(三):HTML5新特性前端HTML
- BIGC 2021|國產引擎 Cocos :連線 IoT 開發者與遊戲GC遊戲
- Android應用開發進階Android
- OPENVINO官方開發進階教程