原文:
足球
足球是掂球游戏的焦点。它响应玩家的输入,响应环境(如重力),发出声音等。它也许是游戏中最复杂的部分。幸运的是,我们会尽可能的深入浅出。
ball.js
pc.script.attribute("gravity", "number", -9.8, {description: "The value of gravity to use"});
pc.script.attribute("defaultTap", "number", 5, {description: "Speed to set the ball to when it is tapped"});
pc.script.attribute("impactEffect", "entity", null, {description: "The particle effect to trigger when the ball is tapped"});
pc.script.attribute("ballMinimum", "number", -6, {description: "When ball goes below minumum y value game over is triggered"});
pc.script.attribute("speedMult", "number", 4, {description: "Multiplier to apply to X speed when tap is off center"});
pc.script.attribute("angMult", "number", -6, {description: "Multiplier to apply to angular speed when tap is off center"});
pc.script.create('ball', function (app) {
var tmp = new pc.Vec3();
// Creates a new Ball instance
var Ball = function (entity) {
this.entity = entity;
};
Ball.prototype = {
// Called once after all resources are loaded and before the first update
initialize: function () {
this.paused = true;
// Get the "Game" Entity and start listening for events
this.game = app.root.findByName("Game");
app.on("game:start", this.unpause, this);
app.on("game:gameover", this.pause, this);
app.on("game:reset", this.reset, this);
// Initialize properties
this._vel = new pc.Vec3(0, 0, 0);
this._acc = new pc.Vec3(0, this.gravity, 0);
this._angSpeed = 0;
// Store the initial position and rotation for reseting
this._origin = this.entity.getLocalPosition().clone();
this._rotation = this.entity.getLocalRotation().clone();
},
// Called every frame, dt is time in seconds since last update
update: function (dt) {
// Don't update when paused
if (this.paused) {
this.entity.rotate(0, 30*dt, 0);
return;
}
var p = this.entity.getLocalPosition();
// integrate the velocity in a temporary variable
tmp.copy(this._acc).scale(dt);
this._vel.add(tmp);
// integrate the position in a temporary variable
tmp.copy(this._vel).scale(dt);
p.add(tmp);
// update position
this.entity.setLocalPosition(p);
// rotate by angular speed
this.entity.rotate(0, 0, this._angSpeed);
// check for game over condition
if (p.y < this.ballMinimum) {
this.game.script.game.gameOver();
}
},
/*
* Called by the input handler to tap the ball up in the air
* dx is the tap distance from centre of ball in x
* dy is the tap distance from centre of ball in y
*/
tap: function (dx, dy) {
// Update velocity and spin based on position of tap
this._vel.set(this.speedMult * dx, this.defaultTap, 0);
this._angSpeed += this.angMult * dx;
// calculate the position of the tap in world space
tmp.copy(this.entity.getLocalPosition());
tmp.x -= dx;
tmp.y -= dy;
// trigger particle effect to tap position, facing away from the center of the ball
this.impactEffect.setLocalPosition(tmp);
this.impactEffect.particlesystem.reset();
this.impactEffect.particlesystem.play();
this.impactEffect.lookAt(this.entity.getPosition());
// play audio
this.entity.sound.play("bounce");
// increment the score by 1
this.game.script.game.addScore(1);
},
// Pause the ball update when not playing the game
unpause: function () {
this.paused = false;
// start game with a tap
this.tap(0, 0);
},
// Resume ball updating
pause: function () {
this.paused = true;
},
// Reset the ball to initial values
reset: function () {
this.entity.setLocalPosition(this._origin);
this.entity.setLocalRotation(this._rotation);
this._vel.set(0,0,0);
this._acc.set(0, this.gravity, 0);
this._angSpeed = 0;
}
};
return Ball;
});
脚本属性
在脚本最顶端你会首先注意到的是一系列我们定义的脚本属性。定义脚本属性可以允许你将脚本中的值暴露给编辑器。这么做有三个原因:
第一,你可以为多个不同的实体使用相同的脚本,除了值不同以外。比如一个设置颜色的脚本属性,那么你在编辑器中创建了红色,蓝色和绿色版本的实体后,可以使用同一个脚本,只需为其设置不同的颜色。
第二,你可以快速方便的调整脚本的行为。当你修改了某个脚本属性的值(或编辑器上任意的属性),它将立即应用到游戏的所有在编辑器中加载的实例上。比如我们这里定义的ballMinimum属性,你可以载入游戏测试那些ballMinimum值是最优的而无需重新加载。也就是,测试游戏,修改值,再测试游戏。
这就是所谓的“迭代速度”。修改和测试的越快,开发的进度就越快。
对于球来说,我们定义了一系列脚本属性帮助我们调整游戏参数,比如重力,或当点击球时的脉冲。这些属性使我们能够更高效的调整游戏到我们满意的程度。
第三,脚本属性是一种极其方便的方法来把脚本绑定到一个实体或场景中的资源上。比如,当被点击时,球脚本需要触发一种粒子效果。而粒子效果实在场景中的另一个实体上。我们定义了一个叫做impactEffect的脚本属性,其类型是实体,在编辑器中我们将其与粒子效果实体链接起来即可。我们的脚本现在就可以引用那个实体了,并且我们可以修改这个实体或更换为另一个实体而无需改动代码。
物理模拟
对于那些具备矢量数学基本知识的读者来说,球的update方法循环应该是易于理解的。而对于其他的大部分读者来说,我们将会对视频游戏中模拟一个球的运动稍加解释。
在视频游戏中模拟一个东西的最简单的方法是给其一个加速度,一个速度和一个位置。在每个事件段内,加速度改变速度,而速度则改变位置。然后,你只需要在新的位置上绘制对象即可。
你可以用下列方法之一来改变物体的位置。
- 改变加速度,这适用于在一定时间内施加一个力的场景,如重力;
- 改变速度,这是一种瞬时的变化。如同一个球从地面弹起。
- 改变位置,就像瞬间转移,正在真实世界中是不存在的。
在我们的模拟中有一个由重力引起的恒定的加速度,当你点击球的时候,其速度会有一个瞬时的改变;当你复位游戏的时候,我们将球瞬移到初始的位置上。
模拟
在update循环中按照如下公式操作:
(速度的变化)=(加速度)x(上一帧到当前的时间)
(速度)=(原速度)+(速度的变化量)
(位移)=(速度)x(上一帧到当前的时间)
(新的位置)=(原位置)+(位移)
在代码中实现如下:
var p = this.entity.getLocalPosition();
// integrate the velocity in a temporary variable
tmp.copy(this._acc).scale(dt);
this._vel.add(tmp);
// integrate the position in a temporary variable
tmp.copy(this._vel).scale(dt);
p.add(tmp);
// update position
this.entity.setLocalPosition(p);
最后,为了获得更好的效果,我们调用了entity.rotate()方法来给球一个旋转角速度值,虽然这不是非常的符合实际,但看起来还不错。
响应输入
你可以还记得在(二)中input.js脚本会检查是否用户的输入击中了球,如果是的话,它会调用tap()方法。在前面定义的tap方法直接改变了球的速度和角速度。我们用几个脚本属性this.speedMult和this.angMult来使新的速度和角速度翻倍,以此来匹配我们预期的游戏效果。
另外,tap方法还在点击的点上触发了一种灰尘的粒子效果,并播放音效。
总结
ball脚本实现了一个简单的物理过程模拟来使球在重力的作用下下落并能响应点击操作。同时,它还通过监听游戏事件来获知游戏何时暂定或是复位。最终,它还与其他的系统交互,显示粒子效果并播放声音。