现在,Snail Bait可以检测到碰撞(如本系列文章的前一篇文章所述),游戏必须处理一种重要的非碰撞类型:跑步者缺少平台。 在这种情况下,她开始跌倒。 在本文中,我将向您展示如何结合重力来实现逼真的下落。 重力和摔倒完善了Snail Bait需要的所有游戏玩法。 之后,我将切换齿轮并向您展示如何将声音(包括音乐)整合到游戏中。 此部分的完整示例代码可供下载 。
坠落
Snail Bait的跑步者从平台边缘跑下或从下方与平台碰撞时会掉下,如图1所示:
图1.从平台边缘掉下来
如图2所示,当她在跳台下降结束时错过平台时,她也会跌倒。
图2.在跳跃结束时跌倒
跑步者由于摔倒行为而摔倒 。 清单1显示了跑步小精灵的实例化,并指定了她的行为数组:
清单1.跑步者的行为
Sprite = function () {
...
this.runner = new Sprite('runner', // type
this.runnerArtist, // artist
[ this.runBehavior, // behaviors
this.jumpBehavior,
this.collideBehavior,
this.fallBehavior,
]);
};
当精灵可见时,Snail Bait会为每个动画帧调用跑步者的跌倒行为(与所有精灵行为一样)。 在大多数情况下,跌倒行为无能为力。 当跑步者的falling
属性为true
,跌落行为会在每个动画帧中使跑步者垂直垂直移动,以使其看起来好像在跌倒。 该属性由跑步者的fall()
方法设置,如清单2所示:
清单2.跑步者的fall()
方法
SnailBait.prototype = {
...
equipRunner: function () {
...
this.equipRunnerForJumping();
this.equipRunnerForFalling();
},
equipRunnerForFalling: function () {
...
this.runner.fallAnimationTimer = new AnimationTimer();
this.runner.fall = function (initialVelocity) {
// set attributes for the runner's fall behavior and
// start the fall animation timer
this.velocityY = initialVelocity || 0;
this.initialVelocityY = initialVelocity || 0;
this.falling = true;
this.fallAnimationTimer.start();
}
},
...
};
游戏开始时,Snail Bait会调用其equipRunner()
方法,该方法除其他功能外,还使跑步者同时具有跳跃和跌倒的能力。 equipRunnerForFalling()
方法包含跑步者的fall()
方法的实现。 跑步者的fall()
方法设置跑步者的初始速度,将其falling属性设置为true
,并启动动画计时器以跟踪跑步者掉落期间流逝了多少时间。
当跑步者的fall()
方法将跑步者的falling
属性设置为true
,它将触发跑步者的摔倒行为中的触发器。 然后,跑步者摔倒,直到游戏将跑步者的falling
属性重置为false
为止。 本引力讨论的其余部分集中于该行为的实现。
结合重力
在地球表面附近,重力以每秒9.81米/秒的速度加速坠落的物体,这意味着物体每秒掉落的速度会增加近10 m / s(或32 ft / s)。 重力对游戏开发者的影响是,除了根据精灵的速度计算精灵的位置外,还必须计算精灵掉落时的速度。
在重力影响下计算速度的数学方法很简单:将重力乘以精灵的经过的下降时间,然后将其值添加到精灵开始下降时的初始速度。 与方程式一样,令人困惑的部分不是数学而是单位,因为前面方程式的结果使您每秒只有米。 为了使该数字有意义,Snail Bait使用以下用于计算子画面位置的算法将其转换为每秒像素数:
- 游戏开始时:
- 定义游戏的宽度(以像素为单位)。
- 任意定义游戏的宽度(以米为单位)。
- 将以像素为单位的宽度除以以米为单位的宽度,以得到像素/米的比率。
- 随后,对于每个动画帧:
- 使用重力加速度(9.81 m / s / s)来计算速度,以米/秒为单位。
- 将速度(以m / s为单位)乘以在步骤3中计算出的像素/米比率,以获得像素/秒。
- 根据像素/秒的速度计算位置。
现在,您可以将前面的算法转换为代码。 第一步是定义重力和游戏的像素/米比,如清单3所示:
清单3.与重力和下落有关的常数
var SnailBait = { // SnailBait constructor function
...
this.GRAVITY_FORCE = 9.81,
this.PIXELS_PER_METER = this.canvas.width / 10; // 10 meters, randomly selected width
...
}
当跑步者跑出平台的末端或从下面与平台碰撞时,她开始没有垂直速度坠落。 但是,当跑步者在跳步结束时没有降落在平台上时,她开始以跳步下降结束时的垂直速度坠落。 清单4显示了跑步者的跳跃行为如何使用清单3中定义的GRAVITY_FORCE
和PIXELS_PER_METER
常量来计算初始速度:
清单4.在跳转结束时跌倒
this.jumpBehavior = {
...
finishDescent: function (sprite) {
sprite.stopJumping();
if (snailBait.isOverPlatform(sprite) !== -1) {
sprite.top = sprite.verticalLaunchPosition;
}
else {
// The argument passed to the sprite's fall() method
// is the sprite's initial velocity when it begins to fall
sprite.fall(snailBait.GRAVITY_FORCE *
(sprite.descendAnimationTimer.getElapsedTime()/1000) *
snailBait.PIXELS_PER_METER);
}
},
};
跑步者下降后的垂直速度是重力乘以下降的时间乘以游戏的像素/仪表比:
(9.81 m / s / s)*(以秒为单位的下降下降时间)*(800像素/ 10 m)
该计算结果以像素/秒为单位,表示跑步者在其跳下下降结束时的垂直速度。 (请参阅本系列的第六篇文章 ,以了解其余的跳转行为的实现。)
Snail Bait使用GRAVITY_FORCE
和PIXELS_PER_METER
计算跑步者速度的另一个地方是跑步者的跌倒行为,如清单5所示:
清单5.设置精灵速度并计算当前帧的跑步者垂直下落
this.fallBehavior = {
...
setSpriteVelocity: function (sprite) {
sprite.velocityY = sprite.initialVelocityY +
snailBait.GRAVITY_FORCE *
(sprite.fallAnimationTimer.getElapsedTime()/1000) *
snailBait.PIXELS_PER_METER;
},
calculateVerticalDrop: function (sprite, fps) {
return sprite.velocityY / fps;
},
};
跌倒行为的setSpriteVelocity()
方法根据跑步者跌倒了多长时间来设置其速度。 该方法要小心,以合并可能由清单2中的跳跃行为设置的跑步者的初始速度。
calculateVerticalDrop()
方法使用基于时间的运动(我将在本系列第二篇文章的基于时间的运动部分中进行讨论) 基于 setSpriteVelocity()
和当前帧计算的速度来计算跑步者的垂直下落。率。
正如我在本系列第五篇文章中详细讨论的那样,Snail Bait在每个动画帧上遍历其所有精灵。 对于每个可见的Sprite,Snail Bait迭代该Sprite的行为,依次调用每个行为的execute()
方法。 清单6显示了跑步者摔倒行为的execute()
方法:
清单6.掉落行为的execute()
方法
this.fallBehavior = {
execute: function (sprite, time, fps) { // sprite is the runner
var deltaY;
if (sprite.jumping) {
return;
}
if (this.isOutOfPlay(sprite) || sprite.exploding) {
if (sprite.falling) {
sprite.stopFalling();
}
return;
}
if (!sprite.falling) {
if (!sprite.exploding && !this.isPlatformUnderneath(sprite)) {
sprite.fall();
}
return;
}
this.setSpriteVelocity(sprite);
deltaY = this.calculateVerticalDrop(sprite, fps);
if (!this.willFallBelowCurrentTrack(sprite, deltaY)) {
sprite.top += deltaY;
}
else { // will fall below current track
if (this.isPlatformUnderneath(sprite)) {
this.fallOnPlatform(sprite);
sprite.stopFalling();
}
else {
sprite.top += deltaY;
sprite.track--;
}
}
}
},
当跑步者跳跃,摔倒或摔倒时,摔倒行为的execute()
方法将启动或停止摔倒并返回。 如果她摔跤或摔倒时爆炸,则该方法将调用她的stopFalling()
方法。 如果跑步者没有摔倒,当前没有爆炸并且在她下面没有平台,则该方法将调用她的fall()
方法。
有了这些先决条件,跌倒行为的execute()
方法将计算跑步者当前的速度和位置。 如果该新位置未将跑步者置于其当前平台下方,则该方法会将其移动到那里。 否则,跑步者会跌落到她的平台下方,因此该方法会检查她下方是否有另一个平台。 如果是这样,则该方法将她放置在该平台上并停止跌倒。 如果跑步者跌落到她的平台以下,并且在她的下方没有平台,则该方法会将她移动到新位置并减小当前轨迹。
跌倒行为的execute()
方法使用了四种便捷方法。 清单7中的两种方法确定跑步者是否失控或将落在当前轨道以下:
清单7.确定跑步者是否失控或将跌落到当前位置以下
this.fallBehavior = {
isOutOfPlay: function (sprite) {
return sprite.top > snailBait.TRACK_1_BASELINE;
},
willFallBelowCurrentTrack: function (sprite, deltaY) {
return sprite.top + sprite.height + deltaY >
snailBait.calculatePlatformTop(sprite.track);
},
...
};
清单8中的便捷方法确定平台是否在跑步者下面,并将跑步者放在平台上:
清单8.确定平台是否在流道下方,并将流道降落在平台上
this.fallBehavior = {
isPlatformUnderneath: function (sprite) {
return snailBait.isOverPlatform(sprite) !== -1;
},
fallOnPlatform: function (sprite) {
sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
sprite.stopFalling();
},
...
};
重要的是要意识到,跌倒行为的execute()
方法仅在跑步者的falling
属性为true
时才能垂直移动跑步者。 跑步者的stopFalling()
方法将该属性设置为false
,如清单9所示,除了将跑步者的垂直速度设置为0
并停止跑步者的跌倒动画计时器之外:
清单9.掉落行为的stopFalling()
方法
SnailBait.prototype = {
...
equipRunnerForFalling: function () {
...
this.runner.stopFalling = function () {
this.falling = false;
this.velocityY = 0;
this.fallAnimationTimer.stop();
}
},
...
};
跑步者摔倒时暂停
正如我在本系列第七篇文章的“ 暂停行为”部分所讨论的那样,行为必须实现pause()
和unpause()
方法,以便它们可以与整个游戏一致地暂停和恢复。 跑步者的跌倒行为符合该要求,如清单10所示:
清单10.暂停和取消暂停跌倒行为
var SnailBait = function () {
...
this.fallBehavior = {
pause: function (sprite) {
sprite.fallAnimationTimer.pause();
},
unpause: function (sprite) {
sprite.fallAnimationTimer.unpause();
},
}
...
}
跌倒行为使用跑步者的跌倒动画计时器跟踪跌倒期间的经过时间。 因此,该行为的pause()
和unpause()
方法只是暂停和取消暂停该计时器。
既然您已经了解了Snail Bait如何使跑步者摔倒,现在是时候看看完全无关的东西了:游戏的声音。
控制音效和音乐
Snail Bait可以同时播放音乐配乐和音效。 在图3的左下方,是声音和音乐复选框,可让用户控制游戏是播放声音效果,音乐还是同时播放声音和音乐:
图3.声音和音乐控件
清单11显示了复选框HTML:
清单11. Snail Bait的声音和音乐复选框
<div id='sound-and-music'>
<div class='checkbox-div'>
Sound <input id='sound-checkbox' type='checkbox' checked/>
</div>
<div class='checkbox-div'>
Music <input id='music-checkbox' type='checkbox'/>
</div>
</div>
您可以使用复选框的“ checked
属性(无值)来控制该复选框是否最初被选中。 如果存在该属性,则首先选中该复选框;否则,将选中该复选框。 否则, 如图3和清单11所示 。
在清单12中,Snail Bait以编程方式访问复选框元素,并维护两个变量( soundOn
和musicOn
),事件处理程序使这些变量与复选框保持同步。 音乐复选框事件处理程序还会播放或暂停游戏的音乐配乐,与声音效果不同,音乐配乐会不断在后台播放。
清单12.声音和音乐复选框事件处理程序
var SnailBait = function () {
...
this.soundCheckbox = document.getElementById('sound-checkbox');
this.musicCheckbox = document.getElementById('music-checkbox');
this.soundOn = this.soundCheckbox.checked;
this.musicOn = this.musicCheckbox.checked;
...
};
...
snailBait.soundCheckbox.onchange = function (e) {
snailBait.soundOn = snailBait.soundCheckbox.checked;
};
snailBait.musicCheckbox.onchange = function (e) {
snailBait.musicOn = snailBait.musicCheckbox.checked;
if (snailBait.musicOn) {
snailBait.soundtrack.play();
}
else {
snailBait.soundtrack.pause();
}
};
如果音乐开启,Snail Bait的startGame()
方法将播放音乐,如清单13所示:
清单13.开始游戏
SnailBait.prototype = {
SnailBait.prototype = {
...
startGame: function () {
if (this.musicOn) {
this.soundtrack.play();
}
requestNextAnimationFrame(this.animate);
},
...
我还修改了游戏的togglePaused()
方法,以根据游戏是否暂停来暂停或播放配乐,如清单14所示:
清单14.暂停音乐
SnailBait.prototype = {
...
togglePaused: function () {
...
if (this.paused && this.musicOn) {
this.soundtrack.pause();
}
else if ( ! this.paused && this.musicOn) {
this.soundtrack.play();
}
},
};
实现音效
Snail Bait在游戏的各个点(例如,跑步者与蜜蜂或蝙蝠碰撞时)均会播放声音效果,有时必须同时播放多种声音。 该游戏使用HTML5 audio
元素来实现声音效果。
HTML audio
元素
Snail Bait为其每种声音创建一个audio
元素,如清单15所示:
清单15. Snail Bait的audio
元素
<!DOCTYPE html>
<html>
<head>
<title>Snail Bait</title>
<link rel='stylesheet' href='game.css'/>
</head>
<body>
<audio id='soundtrack'>
<source src='sounds/soundtrack.mp3' type='audio/mp3'>
<source src='sounds/soundtrack.ogg' type='audio/ogg'>
</audio>
<audio id='plop-sound' >
<source src='sounds/plop.mp3' type='audio/mp3'>
<source src='sounds/plop.ogg' type='audio/ogg'>
</audio>
<audio id='chimes-sound' >
<source src='sounds/chimes.mp3' type='audio/mp3'>
<source src='sounds/chimes.ogg' type='audio/ogg'>
</audio>
<audio id='whistle-down-sound' >
<source src='sounds/whistledown.mp3' type='audio/mp3'>
<source src='sounds/whistledown.ogg' type='audio/ogg'>
</audio>
<audio id='thud-sound' >
<source src='sounds/thud.mp3' type='audio/mp3'>
<source src='sounds/thud.ogg' type='audio/ogg'>
</audio>
<audio id='jump-sound' >
<source src='sounds/jump.mp3' type='audio/mp3'>
<source src='sounds/jump.ogg' type='audio/ogg'>
</audio>
<audio id='coin-sound' >
<source src='sounds/coin.mp3' type='audio/mp3'>
<source src='sounds/coin.ogg' type='audio/ogg'>
</audio>
<audio id='explosion-sound' >
<source src='sounds/explosion.mp3' type='audio/mp3'>
<source src='sounds/explosion.ogg' type='audio/ogg'>
</audio>
...
</body>
</html>
在每个audio
元素中,我指定了两个不同格式的声音文件。 浏览器选择它可以播放的格式。 MP3和OGG格式涵盖了现代浏览器的所有基础。 看到相关主题有关HTML5音频格式获得更多的信息。
此外,Snail Bait通过使用文档的getElementById()
方法访问JavaScript中的每个audio
元素,如清单16所示:
清单16.在JavaScript中访问Snail Bait的音频元素
SnailBait = function () {
...
this.coinSound = document.getElementById('coin-sound'),
this.chimesSound = document.getElementById('chimes-sound'),
this.explosionSound = document.getElementById('explosion-sound'),
this.fallingWhistleSound = document.getElementById('whistle-down-sound'),
this.plopSound = document.getElementById('plop-sound'),
this.jumpWhistleSound = document.getElementById('jump-sound'),
this.soundtrack = document.getElementById('soundtrack'),
this.thudSound = document.getElementById('thud-sound'),
...
};
卷
对于播放的每种声音,Snail Bait都会定义一个音量级别,范围从0.0(无声)到1.0(最大音量)。 清单17显示了Snail Bait为卷定义的常数,我根据经验确定了这些常数:
清单17.定义Snail Bait声音的音量
SnailBait = function () {
// Sound-related constants
this.COIN_VOLUME = 1.0,
this.CHIMES_VOLUME = 1.0,
this.EXPLOSION_VOLUME = 0.25,
this.FALLING_WHISTLE_VOLUME = 0.10,
this.JUMP_WHISTLE_VOLUME = 0.05,
this.PLOP_VOLUME = 0.20,
this.SOUNDTRACK_VOLUME = 0.12,
this.THUD_VOLUME = 0.20,
...
};
通过引用表示手中音量的音频元素和常量,Snail Bait在游戏开始时会初始化音频元素,如清单18所示:
清单18.设置Snail Bait声音的音量
SnailBait.prototype = {
...
initializeSounds: function () {
this.coinSound.volume = this.COIN_VOLUME;
this.chimesSound.volume = this.CHIMES_VOLUME;
this.explosionSound.volume = this.EXPLOSION_VOLUME;
this.fallingWhistleSound.volume = this.FALLING_WHISTLE_VOLUME;
this.plopSound.volume = this.PLOP_VOLUME;
this.jumpWhistleSound.volume = this.JUMP_WHISTLE_VOLUME;
this.soundtrack.volume = this.SOUNDTRACK_VOLUME;
this.thudSound.volume = this.LANDING_VOLUME;
},
start: function () {
this.createSprites();
this.initializeImages();
this.initializeSounds();
this.equipRunner();
this.splashToast('Good Luck!');
...
},
...
};
同时播放多种声音
HTML5 audio
元素具有简单的API,包括Snail Bait用来播放声音的以下方法:
-
play()
-
pause()
-
load()
Snail Bait还使用以下audio
元素属性:
-
currentTime
-
ended
您可以在清单19中看到所有前述方法和属性的使用( 清单12和清单14中使用的pause()
除外):
清单19.使用Snail Bait的音轨播放声音
SnailBait = function () {
...
this.soundOn = true,
this.audioTracks = [ // 8 tracks is more than enough
new Audio(), new Audio(), new Audio(), new Audio(),
new Audio(), new Audio(), new Audio(), new Audio()
],
...
// Playing sounds.......................................................
soundIsPlaying: function (sound) {
return !sound.ended && sound.currentTime > 0;
},
playSound: function (sound) {
var track, index;
if (this.soundOn) {
if (!this.soundIsPlaying(sound)) {
sound.play();
}
else {
for (i=0; index < this.audioTracks.length; ++index) {
track = this.audioTracks[index];
if (!this.soundIsPlaying(track)) {
track.src = sound.currentSrc;
track.load();
track.volume = sound.volume;
track.play();
break;
}
}
}
}
},
...
};
为了同时播放多种声音,Snail Bait会创建一个包含八个audio
元素的数组。 Snail Bait的playSound()
方法遍历该数组,并使用当前未播放的第一个audio
元素播放声音。
意识到Snail Bait绝不会通过清单15中HTML中指定的原始audio
元素来播放声音效果。 相反,该游戏通过清单19中以编程方式创建的八个音频元素来播放声音效果。 游戏将声音从原始audio
元素加载到以编程方式创建的元素中,然后播放以编程方式创建的元素。
播放Snail Bait的音效
清单20至清单23显示了在各个点上播放Snail Bait的声音效果的代码的摘录。 我在本系列文章的前面讨论了这些清单中的所有代码,因此不再重复这些讨论。 我留下了足够的围绕playSound()
调用的逻辑,以赋予原始上下文。
当跑步者跳起来时,Snail Bat会发出啸叫声,如清单20所示:
清单20.跑步者跳跃时发出的声音
var SnailBait = function () {
...
this.equipRunnerForJumping: function() {
...
this.runner.jump = function () {
if (this.jumping) // 'this' is the runner
return;
this.runAnimationRate = 0;
this.jumping = true;
this.verticalLaunchPosition = this.top;
this.ascendAnimationTimer.start();
snailBait.playSound(snailBait.jumpWhistleSound);
};
},
当精灵爆炸时,Snail Bait将播放explosionSound
,如清单21所示:
清单21.爆炸
SnailBait.prototype = {
...
explode: function (sprite, silent) {
if (sprite.runAnimationRate === 0) {
sprite.runAnimationRate = this.RUN_ANIMATION_RATE;
}
sprite.exploding = true;
this.playSound(this.explosionSound);
this.explosionAnimator.start(sprite, true); // true means sprite reappears
},
...
};
当精灵降落在平台上时,它们会发出轰鸣声。 当它们落在轨道下方时,它们会发出啸叫声(不同于跳跃的啸叫声):
清单22.与跌倒有关的声音
SnailBait = function () {
...
this.fallBehavior = {
...
fallOnPlatform: function (sprite) {
sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
sprite.stopFalling();
snailBait.playSound(snailBait.thudSound);
},
execute: function (sprite, time, fps) {
var deltaY;
if (!this.willFallBelowCurrentTrack(sprite, deltaY)) {
sprite.top += deltaY;
}
else { // will fall below current track
if (this.isPlatformUnderneath(sprite)) {
this.fallOnPlatform(sprite);
sprite.stopFalling();
}
else {
sprite.track--;
sprite.top += deltaY;
if (sprite.track === 0) {
snailBait.playSound(snailBait.fallingWhistleSound);
}
}
}
...
}
};
...
};
精灵之间的碰撞会发出各种声音,具体取决于碰撞中涉及的精灵,如清单23所示:
清单23.碰撞声
var SnailBait = function () {
...
this.collideBehavior = {
...
processCollision: function (sprite, otherSprite) {
if (otherSprite.value) { // Modify Snail Bait sprites so they have values
// Keep score...
}
if ('coin' === otherSprite.type ||
'sapphire' === otherSprite.type ||
'ruby' === otherSprite.type ||
'button' === otherSprite.type ||
'snail bomb' === otherSprite.type) {
otherSprite.visible = false;
if ('coin' === otherSprite.type) {
snailBait.playSound(snailBait.coinSound);
}
if ('sapphire' === otherSprite.type || 'ruby' === otherSprite.type) {
snailBait.playSound(snailBait.chimesSound);
}
}
if ('bat' === otherSprite.type ||
'bee' === otherSprite.type ||
'snail' === otherSprite.type ||
'snail bomb' === otherSprite.type) {
snailBait.explode(sprite);
}
if (sprite.jumping && 'platform' === otherSprite.type) {
this.processPlatformCollisionDuringJump(sprite, otherSprite);
}
},
},
processPlatformCollisionDuringJump: function (sprite, platform) {
var isDescending = sprite.descendAnimationTimer.isRunning();
sprite.stopJumping();
if (isDescending) {
sprite.track = platform.track;
sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
}
else { // Collided with platform while ascending
snailBait.playSound(snailBait.plopSound);
sprite.fall();
}
},
...
};
下次
在下一部分中,我将讨论最终的游戏玩法和修饰,例如生命与游戏结束动画之间的过渡,以总结HTML5 2D游戏开发系列。
翻译自: https://www.ibm.com/developerworks/java/library/wa-html5-game9/index.html