0%

JavaScript(十二)——弹跳球进阶版

本章为上一章弹跳球进阶练习版,请先根据要求自行完成代码。


项目简介

我们的弹球 demo 很有趣, 但是现在我们想让它更具有互动性,我们为它添加一个由玩家控制的“恶魔圈”,如果恶魔圈抓到弹球会把它会吃掉。我们还想测验你面向对象的水平,首先创建一个通用 Shape() 对象,然后由它派生出弹球和恶魔圈。最后,我们为 demo 添加一个计分器来记录剩下的球数。

程序最终会像这样:


步骤

以下各节介绍你需要完成的步骤。

创建我们的新对象

首先, 改变你现有的构造器 Ball() 使其成为构造器 Shape() 并添加一个新的构造器 Ball()

  1. 构造器 Shape() 应该像构造器 Ball() 那样的方式定义 x, y, velX, 和 velY 属性,但不包括 colorsize
  2. 还应该定义一个叫 exists 的新属性,用来标记球是否存在于程序中 (没有被恶魔圈吃掉)。这应该是一个布尔型((true/false)。
  3. 构造器 Ball() 应该从构造器 Shape() 继承 x,y, velX, velY,和 exists 属性。
  4. 构造器 Ball() 还应该像最初的构造器 Ball() 那样定义一个 color 和一个 size 属性。
  5. 请记得给构造器 Ball()prototypeconstructor 属性设置适当的值。

draw(), update(), 和 collisionDetect() 方法定义应保持不变。

你还需要为 new Ball() { ... } 构造器添加第五个参数—— exists, 且值为 true

到这里, 尝试重新加载代码(运行程序),程序以及重新设计的对象都应该像之前那样工作。

定义恶魔圈EvilCircle()

现在是时候来看看那个坏蛋了——恶魔圈 EvilCircle()! 我们的游戏中只会有一个恶魔圈,但我们仍然要使用继承自 Shape() 的构造器来定义它,这是为让你得到锻炼。 之后你可能会想再添加一个由另一个玩家控制的恶魔圈到程序中,或者有几个电脑控制的恶魔圈。你可没法通过一个恶魔圈来掌管程序中的这个世界,但这个评估中就先只这么做吧。

EvilCircle() 构造器应该从 Shape() 继承 x, y, 和 existsvelXvelY 要恒为 20。

可以这样做:Shape.call(this, x, y, 20, 20, exists);

它还应该定义自己的一些属性,如:

  • color —— 'white'
  • size —— 10
    再次记得给你的 EvilCircle() 构造器的传递的参数中定义你继承的属性,并且给 prototypeconstructor 属性设置适当的值。

定义 EvilCircle()的方法

EvilCircle() 应该有以下四个方法:

draw()

这个方法和 Ball()‘s draw() 方法有着相同的目的:它们把都是对象的实例画在画布上(canvas) 。它们实现的方式差不多,所以你可以先复制 Ball.prototype.draw 的定义。然后你需要做下面的修改:

  • 我们不想让恶魔圈是实心的,而是一个圈或者说是环。你可以通过将 fillStylefill() 修改为 strokeStylestroke() 而实现这个效果。
  • 我们还想让这个圈更厚一点, 从而使你能更好地辨认它。 可以在调用 beginPath() 的后面给 lineWidth 赋值实现这个效果。(赋值为 3 就可以了)

checkBounds()

这个方法和 Ball()update() 函数做相同的事情—— 查看恶魔圈是否将要超出屏幕的边界, 并且禁止它超出。 同样,你可以直接复制 Ball.prototype.update 的定义, 但是你需要做一些修改:

  • 删除最后两行 — 我们不想要在每一帧中自动的更新恶魔圈的位置,因为我们会以下面所述的方式移动它。
  • if() 语句中,如果检测为真(即小恶魔圈超出边界),我们不需要更新 velX/velY;取而代之的是,我们想要修改 x/y 的值,使恶魔圈稍微地弹回屏幕。增加或减去 (根据实际判断)恶魔圈 size 的值即可实现。

setControls()

这个方法将会一个 onkeydown 的事件监听器给 window 对象,这样当特定的键盘按键按下的时候,我们就可以移动恶魔圈。下面的代码块应该放在方法的定义里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.onkeydown = e => {
switch(e.key) {
case 'a':
this.x -= this.velX;
break;
case 'd':
this.x += this.velX;
break;
case 'w':
this.y -= this.velY;
break;
case 's':
this.y += this.velY;
break;
}
};

所以当一个按键按下时, 事件对象的 key 属性 就可以请求到按下的按键值。如果是代码中那四个指定的键值之一, 那么恶魔圈将会左右上下的移动。

collisionDetect()

这个方法和 Ball()'s collisionDetect() 方法很相似,所以你可以从它那里复制过来作为新方法的基础。但有一些不同之处:

  • 在外层的 if 语句中,你不需要再检验循环到的小球是否是当前 collisionDetect() 所在的对象 — 因为它不再是一个小球了,它是恶魔圈! 而是检查小球是否存在 (你可以通过哪个属性实现这个呢?)。如果小球不存在,说明它已经被恶魔圈吃掉了,那么就不需要再检测它是否与恶魔圈碰撞了。
  • 在里层的 if 语句中,你不再需要在碰撞被检测到时去改变对象的颜色 — 而是需要将与恶魔圈发生碰撞的小球设置为不存在(再次提问,你觉得你该怎么实现呢?)。

把恶魔圈带到程序中

现在我们已经定义了恶魔圈,我们需要让它显示到我们的屏幕中。为了做这件事,你需要修改一下 loop() 函数:

  • 首先,创建一个新的恶魔圈的对象实例 (指定必需的参数),然后调用它的 setControls() 方法。 这两件事你只需要做一次,不需要放在 loop 的循环中。
  • 在你每一次遍历小球并调用 draw(), update(), 和 collisionDetect() 函数的地方进行修改, 使这些函数只会在小球存在时被调用。
  • 在每个 loop 的循环中调用恶魔圈实例的方法 draw(), checkBounds(), 和collisionDetect()

计算得分

为了计算得分,需按照以下步骤:

  1. 在你的HTML文件中添加一个 <p> 元素到 <h1> 元素的下面,其中包含文本 “还剩多少个球”。
  2. 在你的CSS文件中,添加下面的代码到底部:
1
2
3
4
5
6
7
p {
position: absolute;
margin: 0;
top: 35px;
right: 5px;
color: #aaa;
}
  1. 在你的 JavaScript 文件中,做下列的修改:
  • 创建一个变量存储段落的引用。
  • 以同样的方式在屏幕上显示小球的数量。
  • 增加球数并在每次将球添加到屏幕里时显示更新的球数量。
  • 减少球数并在每次恶魔吃球时显示更新的球数(因为被吃掉的球不存在了)

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// 设置画布

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;

const para = document.querySelector('p');
// 生成随机数的函数

function random(min,max) {
const num = Math.floor(Math.random() * (max - min)) + min;
return num;
}

function randomColor() {
return 'rgb(' +
random(0, 255) + ', ' +
random(0, 255) + ', ' +
random(0, 255) + ')';
}

function Shape(x, y, velX, velY, exists) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.exists = exists;
}

function Ball(x, y, velX, velY, exists, color, size) {
Shape.call(this, x, y, velX, velY, exists);
this.color = color;
this.size = size;
}

Ball.prototype.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}

Ball.prototype.update = function() {
if ((this.x + this.size) >= width) {
this.velX = -(this.velX);
}

if ((this.x - this.size) <= 0) {
this.velX = -(this.velX);
}

if ((this.y + this.size) >= height) {
this.velY = -(this.velY);
}

if ((this.y - this.size) <= 0) {
this.velY = -(this.velY);
}

this.x += this.velX;
this.y += this.velY;

}

let balls = [];

let balls_count = 25


while (balls.length < balls_count) {
let size = random(10, 20);
let ball = new Ball(
// 为避免绘制错误,球至少离画布边缘球本身一倍宽度的距离
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
true,
randomColor(),
size
);
balls.push(ball);
}

Ball.prototype.collisionDetect = function() {
for (let j = 0; j < balls.length; j++) {
if (this !== balls[j] && balls[j].exists === true) {
const dx = this.x - balls[j].x;
const dy = this.y - balls[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < this.size + balls[j].size) {
balls[j].color = this.color = randomColor();
}
}
}
}

function EvilCircle(x, y, velX, velY, exists, color, size) {
Shape.call(this, x, y, velX, velY, exists);
this.color = color;
this.size = size;
}

EvilCircle.prototype.draw = function() {
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.stroke();
}

EvilCircle.prototype.checkBounds = function() {
if ((this.x + this.size) >= width) {
this.x -= this.size;
}

if ((this.x - this.size) <= 0) {
this.x += this.size;
}

if ((this.y + this.size) >= height) {
this.y -= this.size;
}

if ((this.y - this.size) <= 0) {
this.y += this.size;
}
}

EvilCircle.prototype.setControls = function() {
window.onkeydown = e => {
switch(e.key) {
case 'a':
this.x -= this.velX;
break;
case 'd':
this.x += this.velX;
break;
case 'w':
this.y -= this.velY;
break;
case 's':
this.y += this.velY;
break;
}
};
}

EvilCircle.prototype.collisionDetect = function() {
for (let j = 0; j < balls.length; j++) {
if (balls[j].exists === true){
const dx = this.x - balls[j].x;
const dy = this.y - balls[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < this.size + balls[j].size) {
balls[j].exists = false;
balls_count --;
para.textContent = "还剩" + balls_count + "个球";
}
}
}
}

let size = 10;
evilcircle = new EvilCircle( random(0 + size, width - size),
random(0 + size, height - size),
5,
5,
true,
"rgb(255,255,255)",
size
);

function loop() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillRect(0, 0, width, height);

evilcircle.draw();
evilcircle.setControls();
evilcircle.checkBounds();
evilcircle.collisionDetect();

for (let i = 0; i < balls.length; i++) {
if (balls[i].exists === true) {
balls[i].draw();
balls[i].update();
balls[i].collisionDetect();
}
}

requestAnimationFrame(loop);
}

loop();
-------------本文结束感谢您的阅读-------------