八、函数
函数是 JavaScript 的主要构件。它们允许我们以更有效和可伸缩的方式编写程序。函数通过在单个可执行名称下包含和分组操作来帮助我们管理复杂性。我们已经知道如何使用 p5.js 预定义的函数如ellipse
或background
来调用函数。我们甚至将自己的函数声明为 p5.js 迫使我们将代码放入两个函数声明中:setup
和draw
。如果我们想要创建我们自己的函数,我们将遵循我们一直用于创建或声明这些函数的相同约定。
创建函数
为了创建(或声明)一个新的函数,我们将从使用function
关键字开始,然后给函数起一个我们选择的名字,这个名字理想地描述了函数的行为或目的。见清单 8-1 。
function functionName() {
// function body
}
Listing 8-1Creating a function
在函数名的旁边,我们会打开括号。如果我们想要构建一个处理用户输入的函数,我们可以在括号内定义参数,作为将来用户输入的占位符变量名。我们稍后会看到这是如何工作的。
然后我们有花括号。在花括号内可以称为函数体。在那里,我们编写构建函数逻辑的代码。我们还可以使用参数,即我们在函数名旁边的括号中定义的变量名,作为我们希望在函数体内执行的操作的一部分。
让我们看一个简单的例子。注意 p5.js 有一个ellipse
函数,但没有一个circle
函数。这不是一个真正的问题,因为我们可以通过提供具有相同宽度和高度值的ellipse
函数来创建一个圆。不过,为了便于讨论,让我们创建一个使用三个值的circle
函数:我们想要画圆的x
和y
位置以及圆的直径。
清单 8-2 展示了如何去做。在括号内,我们将写下变量名,这些变量名最终将在调用该函数时提供。这些名称被称为参数,因为它们参数化了我们正在创建的操作的函数。我们将在函数中使用这些参数,以允许用户控制函数的内部工作。
function circle(x, y, diameter) {
ellipse(x, y, diameter, diameter);
}
Listing 8-2Declaring a circle function
我们可以选择任何东西作为参数名,但是使用能清楚表达意图的名称通常是有意义的。所以在我们的例子中,使用名字x
、y
和diameter
是有意义的。
在定义了这个函数之后,我们可以通过使用它的名字并为它提供值来调用它。提供给函数的值称为函数的参数。请注意,如果没有提供所有必需的参数,函数可能会失败或无法按预期工作(清单 8-3 )。
circle(width/2, height/2, 100);
Listing 8-3Calling the circle function
如果你觉得术语很混乱,不要太担心。可能需要一段时间来适应。函数的参数可以被认为是用户在使用函数时最终提供的值。调用函数时提供的那些相同的值被称为参数。
有了circle
函数,我们再也不用担心用ellipse
函数画圆了。我们可以用自己的函数画出完美的圆形。在我们自己实现了circle
函数之后,我们知道它实际上使用了底层的ellipse
函数来绘制那些圆。但是函数的巧妙之处在于,一旦它们对我们可用,我们就不需要知道它们是如何工作的。我们可以直接使用它们,而不用考虑它们是如何实现的。由创建 p5.js 的聪明人实现的ellipse
函数可能会使用里面的各种东西来绘制椭圆,但就我们而言,它在被调用时会绘制椭圆,这才是最重要的。
在这个例子中,创建一个circle
函数并没有给我们带来太多的效率。事实上,我们可以只传递三个参数给ellipse
函数,而不是四个参数来画一个圆。但是当我们构建更复杂的程序时,函数变得非常重要。它们通过在单个可执行名称下包含和分组操作来帮助我们管理复杂性。函数本质上是黑盒。它们封装了内部包含的代码。此外,在函数内部使用var
关键字声明的任何变量在函数外部都是不可见的。这意味着从定义它们的函数外部调用它们会导致错误。参见清单 8-4 中的示例。
function setup() {
createCanvas(800, 300);
sayHello();
}
function draw() {
background(220);
}
function sayHello() {
var message = 'Hello World!';
console.log(message);
}
console.log(message); // this line will throw an error
Listing 8-4
Variable visibility
(scope)
第 15 行的console.log
函数将抛出一个错误,因为变量message
只在函数sayHello
内部可见。
函数可以在没有输入、单个输入或多个输入的情况下工作;它们要么返回结果,要么不返回。让我解释一下返回值的含义。
假设我们想要创建一个给定数值乘以自身的函数,本质上是计算给定数值的平方。清单 8-5 显示了一个这样做的函数。它接收一个数字作为参数,并创建在屏幕上显示该数字的文本。因为我们可以使用这个函数在屏幕上显示一个数字的平方,所以这个函数有点用处。结果如图 8-1 所示。
图 8-1
Output from Listing 8-5
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
squared(10);
}
function squared(num) {
fill(237, 34, 93);
textSize(60);
textAlign(CENTER, CENTER);
text(num * num, width/2, height/2);
}
Listing 8-5Creating a multiplying function
但是如果我们想在另一个计算中使用这个结果,我们可能会遇到障碍。这个函数不会将数字返回给我们;它只是把它显示在屏幕上。调用这个函数会影响我们所处的环境,但是它不会返回一个值来用于进一步的计算。到目前为止,我们看到的一些函数,如ellipse
、rect
等,以类似的方式运行,它们做一些事情,但实际上并不返回值作为计算的结果。然而,random
函数在执行时不会在屏幕上显示任何内容,而是返回一个我们可以在变量中捕获的值。
为了能够从函数返回值,我们可以使用return
关键字。让我们将squared
函数改为两者:在屏幕上显示结果,并且返回一个值(清单 8-6 )。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(1, 75, 100);
var x = squared(10);
console.log(x);
}
function squared(num) {
fill(237, 34, 93);
textSize(60);
textAlign(CENTER, CENTER);
var result = num * num;
text(result, width/2, height/2);
// return the value of the result from the function
return result;
}
Listing 8-6Using the return keyword
现在,这个函数返回一个我们在console.log
函数中使用的值。每当程序遇到关键字return
时,程序就会终止函数的执行,并将它旁边声明的值返回给函数的调用者。这意味着如果我们在return
关键字下面有任何其他行,它们不会被执行,因为return
终止了当前函数的执行。return
关键字只在函数内部可用。试图从函数外部使用它会导致错误。因为在函数之外,没有什么要返回的。
重温设置和绘图函数
既然我们已经学习了如何创建函数,强调声明函数和调用函数之间的区别是很重要的。请注意,当我们创建函数时,为了执行它们,我们必须调用它们。例如,在清单 8-7 中的代码示例中,我们只创建或声明了一个函数:
function myFunction() {
}
Listing 8-7Creating and declaring a function
为了能够使用这个函数,我们需要执行它,通过用它的名字调用它并在名字旁边使用括号,如清单 8-8 所示。
myFunction();
Listing 8-8Calling a function
注意在 p5.js 中工作时有一点有点奇怪。我们从来没有真正调用过setup
和draw
函数,然而它们还是被执行了!这是由 p5.js 的架构决定的。p5.js 为我们处理setup
和draw
函数的执行,因为它们的执行遵循一些简单的规则,如下所示:
setup
函数在draw
函数之前执行。setup
函数只执行一次,而draw
函数以一定的默认速率连续执行。
摘要
从我们开始使用 p5.js 的那一刻起,我们就在使用函数。它自己的架构依赖于我们程序中两个函数的存在,这两个函数的名字必须是setup
和draw
。而且,我们一直在使用 p5.js 库自带的函数,比如ellipse
、rect
等。
我们已经看到,函数可以被构建为使用或不使用外部用户输入。我们也可以构建使用或者不使用return
关键字返回值的函数。
函数是创建模块化代码块的一种方式,这些代码块可以在我们的代码中重用。这些函数通过减少我们需要编写的代码量,使我们的程序更易于维护和扩展。每当我们发现自己在多个地方重复一个代码块时,它很可能是创建一个函数的好选择。
实践
创建一个名为grid
的函数,它将使用三个参数:一个numX
和一个numY
参数,它将在 x 轴上创建numX
数量的形状(比如矩形),在 y 轴上创建numY
数量的形状,还有一个size
参数,它将设置形状的大小。
例如:
grid(10, 30, 20); // Would create 10 x 30 rectangles of size 20px.
九、对象
JavaScript 包含一个名为Objects
的数据结构。Objects
帮助你组织代码,在某些情况下,它们使工作变得更容易。创建对象有两种方式:使用object initializer
或使用constructor functions
。在这一章中,我们将使用一个对象初始化器创建一个单独的对象,而构造函数作为一个蓝图,我们可以使用new
关键字创建许多对象实例。
使用Object Initializer
JavaScript 使用一种叫做Object
的数据结构来帮助组织数据。在 JavaScript 中有几种创建object
的方法。一种方法是使用花括号,如清单 9-1 所示。
var colors = {};
Listing 9-1Creating an object with curly brackets
这些花括号叫做Object Initializer
。他们创建了一个空对象。我们通过使用变量colors
来保存对对象的引用。
现在,我们可以通过在一个点之后提供所需的属性名来为这个colors
对象添加属性。这被称为点符号。我们还将为这些新创建的属性赋值。见清单 9-2 。
var colors = {};
colors.black = 0;
colors.darkGray = 55;
colors.gray = 125;
colors.lightGray = 175;
colors.white = 255;
console.log(colors);
Listing 9-2Adding properties
to an object
如果我们在这一点上用console.log
来看这个物体,我们会看到它看起来像这样:
{"black":0,"darkGray":55,"gray":125,"lightGray":175,"white":255}
我们也可以从一开始就创建一个具有相同属性的对象,通过在花括号内提供这些属性(清单 9-3 )。
var colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
};
console.log(colors);
Listing 9-3Adding properties inside the curly brackets
对象基本上是键值对。每个键存储一个值,每个键-值对组成一个对象的属性。
如清单 9-4 所示,要访问对象上的值,我们可以再次使用点符号。
console.log(colors.gray);
Listing 9-4Access a value
of an object
在某些情况下,点符号不起作用。一个例子是当我们在一个对象中使用数字作为键值时。在这种情况下,我们可以使用方括号来访问值。参见清单 9-5 。
console.log(colors[1]); // Assuming you were using numbers instead of color names as key values.
Listing 9-5Use square brackets to access values
如果我们对上面的表达式进行console.log
处理,你认为会得到什么结果?我们将得到值undefined
,因为键 1 在我们当前的colors
对象中不存在。
我们也可以将函数定义为对象中的键值。在这种情况下,产生的属性将被称为方法。
从我们的colors
对象继续,让我们在该对象内部定义一个名为paintItBlack
的方法,这将使背景颜色为黑色(清单 9-6 )。
var colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
paintItBlack: function() {
background(this.black);
}
};
Listing 9-6Defining a method
清单 9-7 显示了一个使用这个对象的 p5.js 代码。
var colors;
function setup() {
createCanvas(800, 300);
colors = {
black: 0,
darkGray: 55,
gray: 125,
lightGray: 175,
white: 255,
paintItBlack: function() {
background(this.black);
}
};
}
function draw() {
background(220);
// calling the paintItBlack method after frame 120.
if (frameCount > 120) {
colors.paintItBlack();
}
}
Listing 9-7Using an object
在这个例子中,我们在setup
和draw
函数范围之外初始化colors
变量,然后在setup
函数中创建它的内容。毕竟我们只需要一次创作的内容。如果frameCount
大于 120,我们就调用它的paintItBlack
方法,这在默认设置下会在两秒后发生。(记住frameRate
的默认值是 60,这意味着每秒大约渲染 60 帧。)
为了能够使用在对象内部定义的键,我们需要能够引用对象本身。在 JavaScript 中,有一个叫做this
的关键字,允许我们这样做(列举 9-8 )。使用this
关键字,我们可以引用定义在对象本身上的键。
paintItBlack: function() {
background(this.black);
}
Listing 9-8Using the
this keyword
一旦我们在对象上定义了一个方法,我们就可以通过使用点符号访问它来调用这个方法(清单 9-9 )。因为我们在这个实例中执行的是一个函数,所以函数名后面需要有括号。
colors.paintItBlack();
Listing 9-9
Calling the method
JavaScript(或其他实现对象的语言)中存在对象的概念,因此我们可以模仿现实世界中的对象或概念。就像现实世界中的对象有属性,有时有行为一样,编程语言对象可以有描述它们是什么的属性和指定它们如何行为的方法。
通过清单 9-10 ,让我给你一个模仿现实世界概念的编程语言对象的例子。我们将创建一个名为circle
的对象。这个circle
对象将有几个定义其外观的属性,并且它将有几个描述其行为的方法。
var circle = {
x: width/2,
y: height/2,
size: 50,
};
Listing 9-10Creating an object
这个circle
对象有一个定义其坐标的x
和y
属性以及一个定义其大小的size
属性。我们还将在它上面创建一个方法,一个作为函数的属性,它定义了某种行为(清单 9-11 )。在这种情况下,定义的行为将是在屏幕上画圆。
var circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
};
Listing 9-11Adding a draw method to the circle object
在这个例子中,我们再次使用this
关键字来访问对象的属性。this
关键字基本上指的是对象本身,允许我们在对象内部调用对象的属性。我们现在可以通过使用circle.draw()
方法调用在屏幕上画这个圆:
circle.draw();
你一定在想:这是有史以来最令人费解的事情。因为当我们可以调用一个函数在屏幕上画一个圆的时候,为什么我们还需要这样画一个圆呢?
ellipse(width/2, height/2, 50, 50);
Listing 9-12Using the ellipse function
to draw a circle to the screen
不过,我们才刚刚开始。让我们给这个圆添加另一个名为grow
的方法,每当它被调用时,这个方法都会将圆的大小增加一个单位(清单 9-13 )。
var circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
grow: function() {
if (this.size < 200) {
this.size += 1;
}
},
};
Listing 9-13Adding
grow method
现在,如果我们要在draw
函数中调用这个函数,我们会看到我们的循环随着 p5.js 不断调用draw
函数而不断增长。清单 9-14 提供了完整的例子。图 9-1 显示了结果输出。
图 9-1
Output from Listing 9-14
var circle;
function setup() {
createCanvas(800, 300);
circle = {
x: width/2,
y: height/2,
size: 50,
draw: function() {
ellipse(this.x, this.y, this.size, this.size);
},
grow: function() {
if (this.size < 200) {
this.size += 1;
}
},
};
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle.draw();
circle.grow();
}
Listing 9-14Using the circle object
如前所述,对象的使用是关于代码组织的。我们没有单独的函数来操纵这个圆,但是我们有一个circle
对象来携带这些函数和它自身的属性。在某些情况下,这可以使我们的代码更容易推理。
使用构造函数
在 JavaScript 中还有另一种创建对象的方法,那就是使用函数(清单 9-15 )。我们在创建函数的对象内部所做的声明与我们在使用对象初始化器时所做的非常相似。注意我们是如何在函数中使用width
和height
p5.js 变量的。为了让这些变量对这个函数可用,需要在createCanvas
函数之后调用它。
var Circle = function() {
this.x = width/2;
this.y = height/2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-15Using a function to create an object
一个创建对象的函数叫做constructor function
。我们可以把它看作是创建新对象的模板或蓝图,这些新对象从这个构造函数中派生出它们的属性。
清单 9-16 展示了一个例子来更好地解释我的意思。假设我们想要一个像前面例子中一样的圆,展示由这个Circle
构造函数定义的行为。在这种情况下,我们不会直接使用这个构造函数,但是我们将使用它来实例化一个模仿这个模板函数的新圆。
var myCircle = new Circle();
Listing 9-16Using a constructor function
我们使用了Circle
构造函数和new
关键字来创建一个名为myCircle
的圆的新实例,它从构造函数中获取属性。基本上,new
关键字允许我们从构造函数中创建一个对象的新实例。我们可以把Circle
构造函数想象成一个蓝图,而myCircle
则是根据这个蓝图构建的一个实际的圆。现在我们可以通过调用它的draw
方法将这个新创建的圆绘制到屏幕上(清单 9-17 )。
myCircle.draw();
Listing 9-17Calling the draw method
清单 9-18 提供了完整的示例。
var circle;
function setup() {
createCanvas(800, 300);
// instantiating a new circle using the Circle Constructor Function
circle = new Circle();
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle.draw();
circle.grow();
}
var Circle = function() {
this.x = width/2;
this.y = height/2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-18Using a constructor function
这种方法的美妙之处在于,我们可以从同一个蓝图中不断创造新的圆圈。由于这些圆是独立的实体或实例,它们可以具有彼此不同的属性。让我们看看清单 9-19 和图 9-2 中的例子。
图 9-2
Output from Listing 9-19
var circle_1;
var circle_2;
var circle_3;
function setup() {
createCanvas(800, 300);
// instantiating circles
circle_1 = new Circle();
circle_2 = new Circle();
circle_3 = new Circle();
}
function draw() {
background(220);
// circle properties
fill(237, 34, 93);
noStroke();
circle_1.draw();
circle_1.grow();
circle_2.x = 150;
circle_2.draw();
circle_2.grow();
circle_3.x = 650;
circle_3.draw();
circle_3.grow();
}
var Circle = function() {
this.x = width / 2;
this.y = height / 2;
this.size = 50;
this.draw = function() {
ellipse(this.x, this.y, this.size, this.size);
};
this.grow = function() {
if (this.size < 200) {
this.size += 1;
}
};
};
Listing 9-19Creating separate circle instances
在这个例子中,我们在 p5.js 函数之外创建了三个变量,分别叫做circle_1
、circle_2
和circle_3
。这些变量是在 p5.js 函数之外创建的,因此它们在这两个函数的范围内。
我们通过使用new
关键字将Circle
构造函数赋给这些变量,使它们成为Circle
实例。现在我们有了三个独立的圆形对象,我们可以在draw
函数中改变它们的属性,并且我们从每个对象中得到不同的结果。
需要注意的一点是,我们如何使用以大写字母开头的函数名作为构造函数。我们用一个大写字母来提醒自己和他人,这个函数是一个构造函数,需要用new
关键字来调用。
用关键字new
调用构造函数是很重要的。如果我们不这样做,它就不能正常工作,因为构造函数中的关键字this
不会引用实例对象,而是引用全局对象。
大写字母的用法不是规则,而是惯例。没有人强迫我们去做。但是我们应该遵循这个约定,因为没有实现一个函数是一个构造函数,然后在没有关键字new
的情况下调用它将会产生意想不到的后果。
摘要
在这一章中,我们学习了 JavaScript 对象。简单地说,对象是组织代码的一种方式。有两种创建对象的方法。一种方法是使用一个object initializer
,另一种方法是使用constructor functions
。
我们还学习了用于访问对象属性的点符号和方括号符号。关键字this
允许我们从对象本身内部引用对象的属性。
在不同的编程语言中有一个完整的编程范例,叫做面向对象编程,它利用对象来组织代码并使代码清晰。使用 p5.js,我们不一定需要创建对象来组织我们的代码,但是我想引入对象有两个原因:
- 它们是 JavaScript 语言的基础部分。如果你想在以后学习更多的语言,你需要熟悉对象是如何工作的。
- JavaScript 还有其他基于我们将要使用的对象的内置结构,所以进一步熟悉对象对我们来说很重要。
十、数组
Arrays
是 JavaScript 中另一个有用的数据结构。它们是用编号索引存储的数据的顺序集合,并且基于Objects
,这使得某些操作更容易执行。
在本章中,我们将使用push
方法填充一个数组。我们还将学习remainder
操作符,我们可以用它来导出在零和期望值之间循环的连续值。
使用push
方法
请记住,我们使用花括号来创建一个空对象。我们可以使用方括号以类似的方式创建一个空数组(清单 10-1 )。
var arr = [];
Listing 10-1Create an empty array
在本例中,我们创建了一个空数组,并使用一个名为arr
的变量来存储该数组。现在,如果我们想向这个数组添加元素,我们可以使用数组对象拥有的push
方法(清单 10-2 )。
var arr = [];
arr.push(1);
arr.push("hello world");
arr.push({"name":"value"});
console.log(arr);
Listing 10-2Adding elements
to the array
在本例中,我们将三个新值推送到先前的空数组中。在第一行中,我们将一个number
类型的值推入数组,在第二行中,我们将一个string
类型推入数组,在第三行中,我们将一个object
类型推入数组。
现在,如果我们使用console.log
来查看数组的内容,我们将在屏幕上看到类似这样的内容:
[1,"hello world",{"name":"value"}]
注意我们是如何使用不同的数据类型和对象来填充数组的。数组可以包含任何对象,甚至其他数组。就像 JavaScript 对象一样,我们可以在创建时填充数组,方法是在方括号内提供所需的值,并用逗号分隔它们。让我们创建一个包含四个数字的数组(清单 10-3 )。
var arr = [15, 40, 243, 53];
console.log(arr);
Listing 10-3Creating an array with different data types
我们可以使用自动生成的索引号属性来访问数组中的各个项。不过,有一点要知道,引用数组中存储项的索引是从 0 开始计数的。要访问数组中的单个项,我们可以键入存储数组的变量名,然后使用方括号中的索引号来引用该索引处的项(参见清单 10-4 )。数字 0 表示数组中的第一项——15,索引号 1 表示第二项,依此类推
var arr = [15, 40, 243, 53];
var firstItem = arr[0];
console.log(firstItem);
Listing 10-4Accessing the items of an array
如果我们试图访问一个不存在的项目,我们将得到一个undefined
值。这是有意义的,因为该项没有定义。记住,当我们试图访问一个不存在的属性时,对象也会返回一个undefined
值。
让我们看看数组数据结构如何在构建程序时简化事情。我们将从一个简单的例子开始(列出 10-5 )。假设我们想要创建五个不同大小的不同圆。要用我们目前的知识做到这一点,我们需要创建五个不同的变量,并为这些变量赋予所需的值。然后我们调用ellipse
函数五次,每次使用不同的变量。
var size1 = 200;
var size2 = 150;
var size3 = 100;
var size4 = 50;
var size5 = 25;
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
ellipse(width/2, height/2, size1, size1);
ellipse(width/2, height/2, size2, size2);
ellipse(width/2, height/2, size3, size3);
ellipse(width/2, height/2, size4, size4);
ellipse(width/2, height/2, size5, size5);
}
Listing 10-5Drawing circles of different sizes
我们只在屏幕上画了五个圆圈,但这已经看起来像是一个麻烦的解决方案。如果我们需要画 100 个甚至 1000 个圆呢?这就是数组发挥作用的地方,它使我们的工作变得更加容易。
首先,让我们创建一个所需的圆形大小的数组。如前所述,我们可以使用索引号来访问数组中的各个项目。我们将使用这些知识从数组中获取所需的值。参见清单 10-6 。
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
ellipse(width/2, height/2, sizes[0], sizes[0]);
ellipse(width/2, height/2, sizes[1], sizes[1]);
ellipse(width/2, height/2, sizes[2], sizes[2]);
ellipse(width/2, height/2, sizes[3], sizes[3]);
ellipse(width/2, height/2, sizes[4], sizes[4]);
}
Listing 10-6Using an array to store the size values
这已经看起来好多了。但是请注意重复发生的次数。当调用ellipse
函数时,我们实际上是在一遍又一遍地输入同样的东西;唯一改变的是指数。这里出现了一个非常清晰的模式:如果我们有一个结构,它会创建一个循环,让我们用递增的值调用ellipse
函数五次,那么我们就不必重复。
幸运的是,我们知道如何创建一个 for 循环来帮助我们做到这一点。清单 10-7 提供了上面重写的使用 for 循环的代码。
var sizes = [200, 150, 100, 50, 25];
for (var i = 0; i < 5; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
Listing 10-7A for-loop snippet
清单 10-8 和图 10-1 显示了 p5.js 示例中代码的用法:
图 10-1
Circles drawn using a for loop
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
for (var i = 0; i < 5; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-8Entire code using for loop
注意到for loop
标题中数字 5 的用法了吗?它在那里是因为我们使用的数组中有五个元素。因此,如果有 6 个项目,那么我们应该将这个值更新为 6。但这有点问题;如果我们把数组做得更大,但是忘记更新这个值,会怎么样?幸运的是,我们可以使用一个名为length
的数组属性,它将给出数组中的项数。我们可以重写上面的代码来利用length
属性(清单 10-9 )。
var sizes = [200, 150, 100, 50, 25];
function setup() {
createCanvas(800, 300);
}
function draw() {
// circle properties
fill(237, 34, 93);
strokeWeight(2);
for (var i = 0; i < sizes.length; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-9Using the array height property
我们的代码现在简洁多了,而且可伸缩性也非常好。我们只需不断向sizes
数组添加新值,就会为我们画出等量的圆。只是为了好玩,让我们进一步自动化这个设置。目前,我们正在手动创建具有大小值的数组。但是我们可以创建另一个for loop
,通过使用random
函数用我们选择的任意数量的随机数填充这个数组(参见清单 10-10 和图 10-2 )。
图 10-2
Output from Listing 10-10
var sizes = [];
function setup() {
createCanvas(800, 600);
noFill();
// populating the sizes array with random values
for (var i=0; i<100; i++) {
var randomValue = random(5, 500);
sizes.push(randomValue);
}
}
function draw() {
background(255);
for (var i = 0; i < sizes.length; i++) {
ellipse(width / 2, height / 2, sizes[i], sizes[i]);
}
}
Listing 10-10Using the
random function
让我们看看这个例子中发生了什么。首先,我们在draw
函数中设置背景颜色为白色。此外,我们正在调用noFill
函数,它将绘制没有填充颜色的形状。这些只是风格上的选择。我们正在创建一个空的sizes
数组,我们将用随机数填充它。然后,我们创建一个循环,将迭代 100 次。在这个循环中,对于每次迭代,我们使用random
函数创建一个介于 5 和 500 之间的随机值,并使用push
方法将生成的随机值放入sizes
数组中。
下一步不变。我们正在为存在于sizes
数组中的所有值创建椭圆。注意改变这个程序中的一个值,产生的随机数的数量,现在是 100,控制了整个结果。这是一个很好的例子,展示了简单的编程结构如何创建非常健壮和可伸缩的解决方案。
使用数组
让我们使用数组来实现另一个可视化!计划是创建一个动画,连续不断地以一种风格的方式显示给定的单词。
首先,让我们复习一下如何在 p5.js 中创建文本。我们将使用带三个参数的text
函数:要显示的文本,以及该文本的 x 和 y 位置。利用这些知识,让我们在屏幕上以浅色背景显示单词“JavaScript”(参见清单 10-11 和图 10-3 )。
图 10-3
Output from Listing 10-11
function setup() {
createCanvas(800, 300);
}
function draw() {
background(200);
text('JavaScript', width/2, height/2);
}
Listing 10-11Using the
text fucntion
请注意,我们创建的文本没有垂直对齐。看起来不居中。使用 p5.js 中一个名为textAlign
的函数很容易解决这个问题(清单 10-12 )。只需在setup
函数中调用这个函数,向它传递值CENTER
。这将负责垂直对齐。我们可以再一次将CENTER
传递给这个函数来水平对齐文本。
textAlign(CENTER, CENTER);
Listing 10-12Using the textAlign function
接下来,让我们格式化文本,使它看起来更好一点。在清单 10-13 中,我们使用textSize
函数将文本大小设置为 45 像素,并使用fill
函数将文本颜色设置为白色(结果见图 10-4 )。
图 10-4
Output for Listing 10-13
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER); // centering the text
}
function draw() {
background(200);
fill(255); // text color
textSize(45); // text size
text('JavaScript', width/2, height/2);
}
Listing 10-13Using textAlign and styling the text
完美!在这个例子中,我们想要创建一个单词数组,并不断地循环遍历它们。让我们首先创建我们将使用的阵列。我们将在draw
函数之外创建它,因为我们只需要创建这个数组一次。如果我们在draw
函数中声明它,那么它会随着对draw
函数的每次调用而不断地被创建和销毁(默认情况下,每秒钟大约发生 60 次!).
让我们在draw
和setup
函数之外创建一个名为words
的变量(清单 10-14 )。因为变量是在setup
和draw
函数之外初始化的,所以它可以在这两个函数中使用。
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
Listing 10-14Creating a words variable
接下来,我们需要设计一种方法来连续生成一个介于 0 和数组长度之间的值,以便能够引用数组中的各个项。为此,我们可以使用remainder
( %
)操作符。
使用remainder
操作符
remainder
操作符与我们之前见过的所有操作符都有点不同,比如加号或减号,所以看看它是如何工作的可能会有所帮助。给定两个值,remainder
运算符返回第一个值除以第二个值后的余数。%
操作符象征着它。
正如我们在清单 10-15 中看到的,给定一个递增的第一个值,remainder
操作符允许我们循环遍历第二个值减一。
console.log(1 % 6) // returns 1.
console.log(2 % 6) // returns 2.
console.log(3 % 6) // returns 3.
console.log(4 % 6) // returns 4.
console.log(5 % 6) // returns 5.
console.log(6 % 6) // returns 0.
console.log(7 % 6) // returns 1.
// etc..
Listing 10-15Remainder operator
你可能会发现自己在想:“你怎么会知道这些?”因为,如果我们只知道remainder
操作符做了什么,但没有任何使用它的实践,这可能是很难想到的事情。这是完全正常的。通过看到其他人使用操作符或结构,您可以了解为了某种目的可以使用哪种操作符或结构。有时这是经验和实践的问题,而不是知识的问题。
如果我要向一个remainder
操作符提供一个恒定的增量值以及数组的长度,我将能够生成在 0 和那个长度之间循环的值。
在 p5.js 上下文中,不断提供的值可以是frameCount
变量。记住frameCount
告诉我们到目前为止draw
函数被调用了多少次。如清单 10-16 所示,让我们在draw
函数中创建一个名为currentIndex
的变量,它使用remainder
操作符、frameCount
p5.js 变量和单词数组的长度来创建介于 0 和数组长度减 1 之间的值。
var currentIndex = frameCount % words.length;
Listing 10-16Using the remainder operator
我们可以console.log
这种说法来验证我们确实在期望的范围内创造价值。但是更好的方法可能是使用text
函数,我们已经用 p5.js 显示了这个值。
在这一点上要注意的一件事是,数字的显示实在是太快了;真的很难理解发生了什么。我们应该放慢 p5.js 的速度,否则我们的文本将很难阅读。一种方法是使用frameRate
功能降低帧速率。如清单 10-17 所示,让我们将设置函数中的frameRate
值改为 3。结果如图 10-5 所示。
图 10-5
Output from Listing 10-17
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
frameRate(3); // using a lower frame rate to slowdown the text
}
function draw() {
var currentIndex = frameCount % words.length;
background(200);
fill(255);
textSize(45);
text(currentIndex, width/2, height/2);
}
Listing 10-17Slowing down the frameRate
太棒了。使用这个代码,我们应该能够看到一系列的数字显示在屏幕上。但是我们对在屏幕上显示数字不感兴趣——而是对数组中的单词感兴趣。利用我们的知识,这很容易做到。我们将使用方括号符号来访问数组中的各个项。
如清单 10-18 所示,让我们创建另一个变量currentWord
。该变量将存储由currentIndex
变量确定的当前单词。现在我们可以用这个变量代替text
函数中的currentIndex
。
var currentWord = words[currentIndex];
Listing 10-18Creating variable
currentWord
我们差不多完成了。但是我想做的另一件事是改变每个单词的背景颜色,因为现在这一点也不美观。
我们将创建另一个名为colors
的数组,它将包含颜色信息。原来我们可以把一个数组传入 p5.js 颜色函数,和把值一个一个传进去是一样的。
因此,如清单 10-19 所示,这两个表达式将创建彼此相同的颜色。
fill(255, 0, 0);
fill([255, 0, 0]);
Listing 10-19Using an array as a value for the fill function
我们将创建包含我们将使用的颜色数组的colors
数组。我们可以尝试自己想出颜色值,但是那样很难找到好看的颜色。
Adobe 有一个名为 Adobe Color CC ( https://color.adobe.com
)的网页,在那里我们可以找到在设计中使用的颜色主题。我会用它来找到一个与我的想象相匹配的主题。
在 Adobe Color CC 的“浏览”选项卡下,您可以选择所需的主题。将鼠标悬停在您想要的主题上,然后单击“编辑副本”这将引导您进入一个页面,在这里您可以看到这些颜色的 RGB 值。清单 10-20 是从该网站挑选的颜色样本。
var colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
];
Listing 10-20Color samples from Adobe Color CC
请注意,我的数据格式有点不同,因为我不想让行太长,因为这会影响代码的可读性。这只是一种风格上的选择。
现在我们可以在fill
函数中使用这些颜色值来改变每一帧背景的颜色。清单 10-21 展示了最终代码的样子。
var words = ['I', 'love', 'programming', 'with', 'JavaScript'];
var colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
];
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
frameRate(3); // using a lower frame rate to slowdown the text
}
function draw() {
var currentIndex = frameCount % words.length;
var currentColor = colors[currentIndex];
var currentWord = words[currentIndex];
background(currentColor);
fill(255);
textSize(45);
text(currentWord, width / 2, height / 2);
}
Listing 10-21Final Code
摘要
在本章中,我们学习了一种叫做数组的 JavaScript 数据结构。数组允许我们以连续的方式存储任意类型的多个值。存储在数组中的值可以使用方括号符号来访问。
我们可以使用push
方法,在数组第一次创建时或创建后用所需的值填充数组。数组在与循环一起使用时特别有用。循环让我们以一种非常简单的方式访问数组中的条目。
我们还学习了remainder
操作符。余数运算符返回两个数之间除法运算的余数。使用这个运算符,我们可以导出在零和期望值之间循环的连续值。
实践
构建一个名为countdown
的函数,它将获得两个参数——一个数字和一个消息—(清单 10-22 ),并将创建一个类似于上面的可视化效果,它将显示从给定数字到数字 0 的倒计时。在倒计时结束时,它应该在屏幕上显示给定的消息,即第二个参数。
您可以随意为该函数添加另一个参数,该参数将控制每个数字在屏幕上停留的时间。
countdown(10, "Launch!");
Listing 10-22
.
十一、事件
在第六章中,我们学习了一个叫做mouseIsPressed
的 p5.js 变量,当鼠标被按下时,它假定值为true
,而对于所有其他情况,它假定值为false
。
我们还了解到,这并不是一种捕捉用户输入的好方法,因为draw
函数的执行速度会使这个变量很难以可靠的方式更新。在这一章中,我们将回顾在 p5.js 中处理用户输入的其他方法,也就是解决这个问题的事件。使用事件,我们可以在draw
函数循环之外捕获用户输入。
在 p5.js 中有许多事件函数,我们可以声明它们来利用事件系统。这里我们将关注两个事件函数:mousePressed
和keyPressed
事件函数。
使用鼠标按下
这个想法类似于draw
和setup
函数,我们用这个特殊的名字声明这个函数,p5.js 以一种特殊的方式处理它(就像setup
和draw
函数一样)。
在 p5.js 代码中,我们在名称mousePressed
下声明的函数在每次按下鼠标按钮时被触发。让我们重写之前的例子,使用变量mouseIsPressed
来使用mousePressed
事件函数(清单 11-1 )。
var toggle = true;
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
}
function draw() {
// display a different bg color based on the toggle value
if (toggle === true) {
background(1, 186, 240);
} else {
background(250, 150, 50);
}
// declaration of variables
var x = width / 2;
var y = height / 2;
var size = 200;
if (frameCount < 60) {
size = size + frameCount;
} else {
size = size + 60;
}
// circle
fill(237, 34, 93);
noStroke();
ellipse(x, y, size, size);
// rectangle
fill(255);
rect(x, y, size*0.75, size*0.15);
}
function mousePressed() {
toggle = !toggle; // change the toggle value to be opposite.
}
Listing 11-1Using mousePressed event function
嗯,这是一个简单的重构!我们只是声明了一个我们自己不执行的函数。每当相应的动作发生时,执行由 p5.js 处理。
还有很多其他的事件函数。完整列表可在 https://p5js.org/reference/#group-Events
找到。
使用按键
另一个值得学习的事件函数是keyPressed
函数。顾名思义,每当按下一个键时,keyPressed
功能就会被触发。在清单 11-2 中,让我们在一个全新的草图中快速测试一下它是如何工作的。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
}
function keyPressed() {
console.log('pressed');
}
Listing 11-2Using the keyPressed function
在这个例子中,每次我们按下一个键,我们都会在控制台上看到一条消息“pressed”。在清单 11-3 中,让我们看一个更复杂的例子,每次按一个键都会在画布中创建一个形状。
var pressed;
function setup() {
createCanvas(800, 300);
background(220);
}
function draw() {
if (pressed === true) {
ellipse(
random(width),
random(height),
50,
50
);
}
pressed = false;
}
function keyPressed() {
pressed = true;
}
Listing 11-3Drawing a shape
with every keypress
这些形状是在我们按下一个键后创建的(图 11-1 )。
图 11-1
Output from Listing 11-3
注意一些事情。首先,我们把background
函数移到了setup
函数下面。这是为了确保我们绘制的形状保留在屏幕上。如果我们有一个在draw
函数中调用的background
函数,那么它会覆盖所有东西,覆盖每一帧,这对于这个用例来说是不理想的。此外,我们将ellipse
函数调用分布在几行代码中,同样是为了增加可读性。
我们有一个全局变量叫做pressed
。每按一次键,我们就将这个全局变量的值设置为true
。当这种情况发生时,draw
函数会在屏幕上显示一个ellipse
,因为条件语句已经执行。然后draw
函数立即将pressed
值再次设置为false
,这样我们只得到一个椭圆。
在清单 11-4 中,我们将对这个例子做一点改进,使它看起来更顺眼。目前,圆圈看起来有点太均匀,颜色有点太暗。我们将这样做,每次我们创建一个圆,它使用一个 0 到 200 之间的随机大小和一个预定义随机颜色列表中的随机颜色(图 11-2 )。
图 11-2
Output from Listing 11-4
var pressed;
var colors = [];
function setup() {
createCanvas(800, 300);
background(0);
colors = [
[245, 3, 155],
[13, 159, 215],
[148, 177, 191],
[100, 189, 167],
[242, 226, 133],
[176, 230, 110],
[123, 90, 240]
];
}
function draw() {
noStroke();
if (pressed === true) {
var randomIndex = parseInt(random(colors.length), 10); // convert the given number to an integer
var randomSize = random(200);
fill(colors[randomIndex]);
ellipse(
random(width),
random(height),
randomSize,
randomSize
);
}
pressed = false;
}
function keyPressed() {
pressed = true;
}
Listing 11-4Changing size and color
为了能够在每次按键时选择一种随机颜色,我们需要生成一个介于 0 和 colors 数组长度减 1 之间的随机integer
。我们使用负 1,因为数组索引从 0 开始计数。
要生成 0 和数组长度减 1 之间的任意随机数,我们可以简单地将random
函数写成random(
colors.length)
。这将最终生成一个介于 0 和之间的数,直到达到colors
数组中的项目数(不包括该数)。然而,问题是生成的数字是浮点数,这意味着它有小数位。然而,我们需要一个整数来访问数组中的条目。所以我们需要把十进制数转换成整数。有几种方法可以解决这个问题。一种方法是使用 p5.js floor
函数,它将给定的浮点数下舍入到最接近的整数。另一个解决方案是使用名为parseInt
的原生 JavaScript 函数,它将给定的值转换成整数——如果该值可以转换的话。我们不能指望向它抛出一个字符串名称值,然后接收一个整数。
如清单 11-5 所示,我们需要向parseInt
函数传递第二个参数来设置计算将要发生的基数。这个基数几乎总是 10。在浮点数上使用parseInt
函数看起来像这样。
var num = parseInt(0.5, 10);
console.log(num); // will be 0.
Listing 11-5Using parseInt on a float number
然而,识别被按下的键只是问题的一部分。我们应该能够做的另一件事是识别用户按下了哪个按钮。在keyPressed
函数中,理论上我们可以通过使用keyCode
变量来识别任何被按下的键。一个keyCode
变量以编码的方式保存用户按下的最后一个键,这样如果用户按下键‘a’,它将返回值‘65’,代表‘b’;66 '等…
由于 p5.js 是一个有用的库,这使得通过为它们提供预定义的变量来识别一些键变得更加容易,例如:BACKSPACE
、DELETE
、ENTER
、RETURN
、TAB
、ESCAPE
、SHIFT
、CONTROL
、OPTION
、ALT
、UP_ARROW
、DOWN_ARROW
、LEFT_ARROW
、RIGHT_ARROW
。
例如,清单 11-6 提供了一小段代码,每当‘Enter’键被按下时,它就执行一个console.log
语句。
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
}
function keyPressed() {
if (keyCode === ENTER) {
console.log('Enter Pressed');
}
}
Listing 11-6Using keyCode values
使用keyCode
变量,我们可以通过一点解码来识别哪个字母数字键被按下。但是还有另一个特别适合字母数字字符的变量,叫做key
。key
变量按原样存储被按下的字母数字键的值,这样更容易识别哪个键被按下了。
摘要
在这一章中,我们学习了一种更好的处理事件的方法,那就是事件函数。我们特别关注两个事件函数:mousePressed
和keyPressed
事件函数。
我们还学习了一些可以在keyPressed
函数中使用的变量:key
和keyCode
。使用key
使得识别字母数字按键更容易,而keyCode
对于检测其他按键是理想的,因为它可以与 p5.js 变量进行比较,如ENTER
、SPACE
等。这使得识别这些按钮更加容易。
从 JavaScript 部分,我们了解了可以用来将类似数字的值(也包括表示数字的字符串)转换成整数的parseInt
函数。
实践
在屏幕上画一个矩形,键盘箭头键可以控制矩形的位置。
十二、关于 p5.js 的更多信息
此时,我们几乎已经准备好进行我们的最终项目了:一个使用 JavaScript 和 p5.js 构建的交互式游戏!那是在下一章。在此之前,我想演示几个更有用的 p5.js 函数来扩展我们可以构建的东西的领域。
你有没有注意到我们如何利用现有的知识在屏幕上画出形状,但是我们不能真正地变换它们,比如围绕它们的中心旋转它们?这是我们可以构建的视觉效果的一大障碍,所以在这一章中,让我们学习如何在 p5.js 中进行转换来增强我们的能力。
旋转和平移
在使用过其他类型的绘图库之后,我应该说在 p5.js 中进行缩放、旋转和平移形状之类的转换可能有点不直观。清单 12-1 和 12-2 是演示如何使用 p5.js rotate
函数的示例,该函数允许我们旋转形状。
图 12-1
Output for Listing 12-1
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
}
function draw() {
background(220);
fill(237, 34, 93);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-1Drawing rectangles
without rotation
目前,我们正在绘制两个彼此对角的矩形(图 12-1 )。让我们利用rotate
函数来看看会发生什么。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
}
function draw() {
background(220);
fill(237, 34, 93);
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-2Using the
rotate function
你会注意到两个形状都从屏幕上消失了。如果您预期形状仅移动 5 度,这一定是一个令人困惑的结果。这是因为rotate
函数在 p5.js 中的默认单位是弧度。我们可以通过使用带有DEGREES
p5.js 变量的angleMode
函数来使这个函数使用度数。如清单 12-3 所示,在setup
函数中做这个声明。
angleMode(DEGREES);
Listing 12-3Using
angleMode
现在事情以或多或少符合预期的方式运行。我们现在可以观察到,当我们调用rotate
函数时,我们最终会旋转函数调用后出现的每个形状(清单 12-4 和图 12-2 )。
图 12-2
Output from Listing 12-4
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-4Using rotate with angleMode
另一件要注意的事情是,旋转发生在原点周围,即画布的左上角。然而,当我们控制形状时,我们通常喜欢让它们绕着原点旋转。所以这个功能,照现在的样子,好像不是特别有用。
为了更好地控制rotate
函数,我们应该研究一下translate
函数。translate
功能将对象从原点移动给定的 x 和 y 平移量。在清单 12-5 中,让我们在当前的设置中使用它。结果如图 12-3 所示。
图 12-3
Output from Listing 12-5
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
translate(150, 0); // using translate function
rotate(5);
rect(width/2, height/2, 50, 50);
rect(width/2+50, height/2+50, 50, 50);
}
Listing 12-5Using the
translate function
现在发生的是translate
函数将画布内的所有内容向右移动 150 像素。它移动整个坐标系,因为旋转也围绕原点右侧的 150px 发生,而不是从原点开始。
事不宜迟,列表 12-6 和图 12-4 是关于如何围绕原点旋转事物。我认为展示它是如何做的比解释它更容易。我们现在将使用单一的形状。
图 12-4
Output from Listing 12-6
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
fill(237, 34, 93);
// rotating the shape around it's origin
translate(width/2, height/2);
rotate(45);
rect(0, 0, 100, 100);
}
Listing 12-6Rotating around the origin
在这个例子中,我们像往常一样绘制一个形状,但是使用translation
函数来设置它的 x 和 y 坐标,而不是将这些值直接输入到形状绘制函数中。这样做,当结合使用rectMode
功能时,允许我们绘制中心位于原点的形状。基本上,我们从在原点绘制形状开始,因为所有的变换函数都相对于该点起作用。然后我们使用translate
和rotate
功能将形状移动到想要的位置和角度。使用这种方法,我们需要记住在translate
函数后调用rotate
,否则旋转仍然会相对于原始原点发生,这可能不是我们想要的。
一般来说,这种方法和使用转换函数的缺点是,从这一点开始,我们绘制的所有东西都将使用这个新的原点。解决这个问题的方法是使用push
和pop
函数。
推动和弹出
p5.js push
函数允许我们创建一个新的状态,而pop
函数将状态恢复到以前的状态。这允许我们对单个对象应用完全不同的设置,而不用担心这些设置是否会影响后面的形状,只要我们在push
和pop
调用之间做所有的事情。同样,在示例中更容易看出这一点(列表 12-7 和图 12-5 )。
根据我们当前的设置,我们在translate
和rotate
函数后绘制的所有东西都将应用 45 度旋转。
fgura 12-5
Output from Listing 12-7
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
}
Listing 12-7Translate function with multiple shapes
在清单 12-8 中,让我们在这里实现push
和pop
函数,这样我们就可以隔离我们正在应用于更大矩形的变换。结果见图 12-6 。
图 12-6
Output for Listing 12-8
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
// translation and rotation will be contained in between
// push and pop function calls.
push();
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
pop();
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
}
Listing 12-8Using the push and pop functions
太棒了!无论我们在push
和pop
函数之间做什么,都不会影响这些函数调用之外的任何东西。需要注意的是,我们总是一起调用push
和pop
函数。使用一个而不使用另一个没有任何意义。
在清单 12-9 中,让我们更新我们的例子,这样我们仍然可以将粉色矩形平移到中间,但是对它应用不同的旋转值。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
push();
translate(width/2, height/2);
rotate(45);
// pink rectangle
fill(237, 34, 93);
rect(0, 0, 150, 150);
pop();
push();
translate(width/2, height/2);
rotate(30);
// white rectangle
fill(255, 255, 255);
rect(0, 0, 75, 75);
pop();
}
Listing 12-9Applying different translations to different shapes
如果您发现自己希望 p5.js 转换没有这么复杂,您可以尝试构建自己的函数来处理和抽象掉复杂性。清单 12-10 提供了一个示例矩形函数,它接受第五个参数,即旋转参数。
function rectC(x, y, width, height, rotation) {
if (rotation === undefined) {
rotation = 0;
}
push();
translate(x, y);
rotate(rotation);
rect(0, 0, width, height);
pop();
}
Listing 12-10Declaring a custom function to handle transformations
这里,我们正在创建名为rectC
的矩形绘制函数,它包装了原始的rect
函数,但是在内部使用push
和pop
来保存状态和设置转换,并且它接受一个可选的旋转参数。如果没有提供旋转参数,那么它将假设值为undefined
。如果是这样的话,我可以将旋转值设置为 0。清单 12-11 是前一个例子的重构,以利用这个函数。请注意,这次它更简洁了。
function setup() {
createCanvas(800, 300);
rectMode(CENTER);
noStroke();
angleMode(DEGREES);
}
function draw() {
background(220);
// pink rectangle
fill(237, 34, 93);
rectC(width/2, height/2, 150, 150, 45);
// white rectangle
fill(255, 255, 255);
rectC(width/2, height/2, 75, 75, 30);
}
function rectC(x, y, width, height, rotation) {
// if rotation value is not provided assume it is 0
if (rotation === undefined) {
rotation = 0;
}
push();
translate(x, y);
rotate(rotation);
rect(0, 0, width, height);
pop();
}
Listing 12-11Using a custom function to handle transformations
摘要
使用绘图库时,能够变换形状变得非常重要。在本章中,我们看到了 p5.js transform
函数是如何工作的。我们学习了translate
和rotate
函数。我们还学习了angleMode
函数,它让我们设置rotate
函数使用的单位。
然后,我们学习了push
和pop
函数,并了解了如何将它们与转换函数结合使用,以隔离状态并将转换应用于各个形状。虽然这些函数对学习 JavaScript 并不重要,但我发现在使用 p5.js 时了解它们是非常必要的。
实践
在进入下一章之前,尝试自己制作一些很酷的东西,我们将一起制作一个互动游戏!
十三、最终项目
在这一章中,我们将构建一个游戏,它利用了我们到目前为止所看到的一切。在这个过程中,我们还将学习更多的技巧。事实上,我们可以使用 p5.js 库构建一个简单的游戏,这给人留下了非常深刻的印象,也很好地说明了这个库的能力。
我们的游戏会很简单。这是一个打字速度游戏,我们将迅速显示数字给玩家,并希望玩家使用键盘在屏幕上输入当前的数字。如果他们在给定的时间内输入正确的数字,他们就得分了。我们将跟踪分数,以便能够在游戏结束时显示出来。如果游戏能呈现出强烈的视觉体验就太好了,但是主要的焦点还是要围绕着正确的游戏逻辑。
让我们对我们需要创造的事物进行分类:
- 我们需要每隔 N 帧在屏幕上显示一个数字。
- 我们不希望数字在屏幕上保持不变。它应该是动画的,以便随着时间的推移更容易或更难阅读。
- 该号码需要保留在屏幕上,直到显示下一个号码,或者直到玩家按下一个键试图匹配该号码。
- 如果玩家输入与屏幕上的数字匹配,我们将显示一条成功消息。如果不是,将指示失败。
- 我们需要记录成功和失败的次数。在 X 个帧或尝试之后,向用户显示结果。
- 我们需要在游戏结束后找到重启游戏的方法。
入门指南
我们列表中的第一项是能够在屏幕上定期显示一个唯一的数字。还记得我们之前使用了remainder
操作符(%)来实现这个壮举。这里,我们将每隔 100 帧在屏幕上显示一个介于 0 和 9 之间的数字(列表 13-1 )。
var content;
function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % 100 === 0) {
content = parseInt(random(10), 10);
}
text(content, width/2, height/2);
}
Listing 13-1Displaying a random integer
every 100 frames
在这个例子中,我们首先在全局范围内初始化一个名为content
的变量。然后在draw
函数中,我们使用random
函数在第一帧或每 100 帧生成一个随机数,然后将该值保存在content
变量中。然而,random
函数的问题是它返回一个浮点数。为了这个游戏的目的,我们想要整数。所以我们使用parseInt
函数将浮点数转换成整数。记住,parseInt
函数要求您传递第二个参数来设置操作的数字系统的基数,对于常见的用例来说,数字系统几乎总是 10。
我们将生成的数字存储在一个名为content
的变量中,然后将该变量传递给一个text
函数,在屏幕中间显示出来。
我们将需要从我们将在屏幕上显示的数字自定义行为很多;所以我们将创建一个 JavaScript 对象来表示它。这样,我们创建的操作数字的函数(如变换操作、颜色配置等)。)可以保持分组在帮助组织程序的对象下。我们将称这个新对象为GuessItem
。我很清楚这是一个可怕的名字,但正如他们所说,在计算机科学中有两个难题:缓存失效、命名事物和一个接一个的错误。
如果我们在尝试创建一个包装 p5.js text
函数的 JavaScript 对象之后再来看我们的代码,看起来我们似乎毫无理由地增加了额外的复杂性,因为我们的代码几乎增长了两倍。但是在一个对象下包含文本绘制功能将有助于以后组织我们的代码。见清单 13-2 。
var guessItem;
function setup() {
createCanvas(800, 300);
}
function draw() {
if (frameCount === 1 || frameCount % 100 === 0) {
background(220);
guessItem = new GuessItem(width/2, height/2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.content = getContent();
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function () {
push();
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
pop();
}
}
Listing 13-2
Text drawing functionality
我们先来关注一下GuessItem
对象。GuessItem
是一个创建对象的构造函数,它需要三个参数:x 和 y 位置以及它在屏幕上绘制的形状的比例。它本身也有两个methods
。其中之一是getContent
,它产生一个介于 0 和 10 之间的随机数,并将其存储在一个名为content
的属性中。它包含的另一个方法是render
,在屏幕上显示一个GuessItem
对象实例的content
属性。
render
方法中的每个操作都存在于push
和pop
函数调用中。这允许我们包含在这个对象包含的这个方法中发生的设置和转换相关的状态变化。这里,我们使用translate
和scale
变换函数来改变文本对象的位置和大小。我们之前没有看到scale
函数,但它是一个与translate
和rotate
函数非常相似的变换函数。顾名思义,它控制的是绘图区域的比例,和其他变换函数的工作原理类似,所以最好将其包含在push
和pop
函数之间。
我们可以使用一个textSize
函数来调用大小,但是我通常发现使用转换函数更直观一些。
在清单 13-3 中,我们现在将使用这个GuessItem
构造函数来创建一个绘制到屏幕上的对象。我们用几个参数on line 10
实例化一个GuessItem
对象,并将它保存在一个名为guessItem
的变量中。
guessItem = new GuessItem(width/2, height/2, 1);
Listing 13-3Creating a GuessItem instance
GuessItem
将要显示的数字也是在实例化时确定的。使用这个对象拥有的render
方法将这个对象绘制到屏幕上on line 13
(清单 13-4 )。
guessItem.render();
Listing 13-4Using the
render method
在清单 13-5 中,让我们让文本在它的生命周期中不断增长,为游戏增加一些活力。
var guessItem;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % 100 === 0) {
guessItem = new GuessItem(width / 2, height / 2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 1;
this.content = getContent();
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function() {
push();
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
pop();
}
}
Listing 13-5Making the text grow in size
我们添加了一种方法,通过每次调用render
函数来递增scale
函数(清单 13-6 )。
this.scale = this.scale + this.scaleIncrement;
Listing 13-6Increment the scale function
我们还在名为scaleIncrement
的GuessItem
构造函数中添加了一个控制缩放速度的新变量。使用该值可以改变动画的速度。例如,我们可以增加这个值来增加游戏难度。
在清单 13-7 中,我们将在我们的脚本中添加更多的参数化,以便能够控制数字显示的方式和频率。
var guessItem
;
// controls the frequency that a new random number is generated.
var interval = 100;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
guessItem = new GuessItem(width / 2, height / 2, 1);
}
guessItem.render();
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.render = function() {
push();
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-7Controlling the frequency of numbers
在这里,我们有几个更小的调整。我们在render
方法中添加了一个fill
函数,我们现在动态地为显示的数字设置 alpha,以使每一帧更加透明。我认为这增加了游戏的活力。将这个数字设得小一点,看看事情变得有压力。我们还使用一个名为interval
的全局变量来参数化GuessItem
的创建频率,这样我们就可以使用该变量的值来使游戏变得更容易或更难。
顺便问一下,你能猜出我们为什么给数字生成函数取名为getContent
吗?那是因为我们做完这个游戏之后,更新游戏在屏幕上显示文字而不是数字应该是一件相当琐碎的事情。让我们的函数名保持通用有助于我们将来为这个游戏所做的扩展工作。
到目前为止,我们只完成了待办事项列表中的两个项目,即通过使用给定的时间间隔在屏幕上显示一个数字,并在屏幕上动画显示该数字,以给我们的游戏添加活力。在下一节中,我们将处理玩家交互。
用户交互
我们还有一项突出的任务,就是获取用户输入,并将其与屏幕上的数字进行比较。让我们实现它(清单 13-8 )。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width / 2, height / 2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution)
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
// generate a random integer in between 0 and 9
return parseInt(random(10), 10);
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function() {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-8
Fetching and comparing user input
我们在很多地方更新了代码。为了能够完成我们的任务,我们在名为solve
的GuessItem
对象上实现了一个新方法。solve
方法获取用户输入,并根据给定的用户输入是否与GuessItem content
变量匹配,返回true
或false
。我们最终将结果保存在一个solution
全局变量中(清单 13-9 )。
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
Listing 13-9Solve method inside the GuessItem
为了能够获得用户输入,我们创建了一个 p5.js 事件函数keyPressed
,每当用户按下一个键时都会调用这个函数。在这个keyPressed
函数中,我们调用一个guessItem
对象的solve
方法来查看被按下的键是否与guessItem
的内容相匹配。如果是,则解变量为true
,如果不是,则为false
。
function keyPressed() {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
if (guessItem !== null) {
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution)
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
Listing 13-10Handling key press
如果有一个GuessItem
存在,我们只从玩家那里读取按键。这是因为一旦玩家做出猜测,我们现在就给guessItem
变量赋予一个null
。这样做可以有效地去除当前的guessItem
对象。这使得我们可以防止玩家对一个数字进行多次猜测。由于guessItem
变量现在可以有一个null
变量,这意味着游戏中可能没有猜测项,因为用户试图猜测它的值,我们对render
方法的调用可能会失败。为了防止这种情况发生,我们将那个render
调用放在一个条件中。此外,我们在keyPressed
函数中有几个console.log
函数,通过查看控制台消息来了解发生了什么。
作为一项测试措施,我们增加了一个条件,如果玩家猜测错误,将背景颜色改为黑色,如果玩家猜测正确,则使用solution
变量将背景颜色改为白色。
说了这么多,这段代码现在不工作。甚至我们正确的猜测都是把屏幕变黑。你能猜到原因吗?
原来原因是keyPressed
函数将被按下的键捕获为字符串,而GuessItem
对象中生成的内容是一个数字。使用三重等号,===
,我们正在寻找这两个值之间是否有严格的相等,没有。这是因为数字永远不等于字符串。所以,我们的函数返回false
。为了解决这个问题,我们将使用 JavaScript 函数String
将生成的数字转换成一个字符串(清单 13-11 )。
function getContent() {
return String(parseInt(random(10), 10));
}
Listing 13-11Converting the random integer
to a string
保持用户分数
为了能够向用户反馈他们在游戏中的表现,我们将开始存储他们的分数。我们将利用这些存储的数据让游戏在一定数量的猜测或失败后停止(清单 13-12 )。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function () {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-12
Storing scores
在清单 13-13 中,我们创建了一个results
数组来存储玩家分数。每当玩家做出一个正确的猜测,我们就在那里推一个true
值;每次玩家猜错了,我们就按一个false
。
if (solution) {
results.push(true);
} else {
results.push(false);
}
Listing 13-13Creating a results array
我们还应该构建一些功能来获取results
array
的值并对其进行评估。为此,我们将构建一个名为getGameScore
的函数(清单 13-14 )。它将获得results
数组,并对其进行评估,以查看当前用户得分。
var guessItem = null;
// controls the frequency that a new random number is generated
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
function setup() {
createCanvas(800, 300);
}
function draw() {
// if there are 3 losses or 10 attempts stop the game
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {win: wins, loss: losses, total: total};
}
function keyPressed() {
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved;
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function () {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-14Building a
getGameScore function
我们的脚本越来越大,越来越复杂!在清单 13-15 中是我们最近添加的函数:getGameScore
。它获取score
变量并遍历该变量来合计输赢的次数,以及猜测的总数。
function getGameScore(score) {
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {win: wins, loss: losses, total: total};
}
Listing 13-15Calculating the game score using the getGameScore function
我们在draw
函数的开头添加了一个条件来检查getGameScore
函数的结果。如果有 3 次失败或总共 10 次猜测,条件执行基本上有一个return
语句的内容(清单 13-16 )。
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
Listing 13-16Conditionally stopping
the game
如清单 13-17 所示,在return
语句之后的任何一行都不会被执行,因为当前的循环将终止,新的循环将开始——只要玩家的分数保持不变,新的循环也将终止。
if (gameScore.loss === 3 || gameScore.total === 10) {
return;
}
Listing 13-17Using the return statement
to stop the draw loop
我们需要一个机制来重启游戏。如清单 13-18 所示,首先,我们将构建一个在游戏结束时显示的屏幕,以显示玩家的分数,并提示玩家按一个键,ENTER,以重新开始游戏(图 13-1 )。其次,我们会让它在游戏结束后,如果玩家按下回车键,它就会重新启动。
图 13-1
Output from Listing 13-18
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 100;
// an array to store solution values
var results = [];
var solution = null;
// stores if the game is over or not.
var gameOver = false;
function setup() {
createCanvas(800, 300);
}
function draw() {
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
gameOver = true;
displayGameOver(gameScore);
return;
}
background(220);
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution === true) {
background(255);
} else if (solution === false) {
background(0);
}
}
function displayGameOver(score) {
// create the Game Over screen
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width / 2, height / 2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the strings for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount / 10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins+1;
} else {
losses = losses+1;
}
}
return {
win: wins,
loss: losses,
total: total
};
}
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
function keyPressed() {
// if game is over, then restart the game on ENTER key press.
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.5;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 3;
this.solved = null;
function getContent() {
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.render = function() {
push();
if (this.solved === false) {
return;
}
fill(0, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
text(this.content, 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-18Restarting the game
让我们先看看我们用displayGameOver
函数做了什么(清单 13-19 )。这里发生了一些我们以前不知道的事情。
function displayGameOver(score) {
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width/2, height/2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the strings for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount/10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
Listing 13-19
DisplayGameOver function
您应该注意的第一件事是,translate
函数调用的结果是累积的。如果我们在width/2, height/2
之后执行(0, 100)
的translate
,那么得到的translate
将是width/2, height/2 + 100
。
这段代码中的另一个新东西是 p5.js sin
和map
函数,我们用它们来创建一个闪烁的文本。一个sin
函数计算角度的正弦值。给定顺序值,产生的sine
值将在-1 和 1 之间交替。但是-1 和 1 在我们的用例中作为数值对我们几乎没有用处。如果我们要用这个值来设置一个fill
函数的alpha
,一个在 0 到 255 之间变化的值将会非常有用。这就是map
功能发挥作用的地方(清单 13-20 )。map
函数将给定范围内的给定值(第二个和第三个参数)映射到新的给定范围(第四个和第五个参数)。
var alternatingValue = map(sin(frameCount/10), -1, 1, 0, 255);
Listing 13-20Using the
map function
我们将介于-1 和 1 之间的sin
函数的结果映射到 0 和 255。
我们可以调用这个新函数向玩家显示消息,而不是简单地执行一个return
语句。我们实现的下一件事是在游戏结束后重启游戏。为此,我们需要两样东西。首先,我们需要一种方法来响应ENTER
键。然后,我们需要重新初始化相关的游戏变量,以创建一个新游戏正在开始的印象。
清单 13-21 显示了响应ENTER
键的keyPressed
功能部分。
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
Listing 13-21Responding to the
ENTER key
我们使用keyCode
变量和ENTER
变量来响应ENTER
按键。
restartTheGame
函数的内容很简单(清单 13-22 )。它只是重新初始化全局范围内的几个变量,比如用户分数,让它重新开始工作。
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
Listing 13-22The
restartTheGame function
这就是了!我们可以继续努力,通过调整机制和增强游戏的视觉效果来让游戏体验变得更好。但是我们已经奠定了构成我们游戏骨架的基础,现在可以根据你的具体需求进一步开发。
最终代码
这是最终的代码(列表 13-23 )。我决定对我正在开发的版本做一些更新。我决定显示数字的单词,而不是显示数字。我发现这在视觉上更令人愉悦,从游戏的角度来看也更具挑战性,因为它增加了一点解析你所看到的东西的开销。我还在GuessItem
中添加了一个名为drawEllipse
的新方法,该方法可以在屏幕上绘制椭圆以及文字,使游戏更具视觉吸引力。最后,我稍微调整了一下游戏参数,以使计时正确,并添加了当玩家输入正确或错误的数字时显示的消息。图 13-2 显示了最终游戏代码的屏幕。
var guessItem = null;
// controls the frequency that a new random number is generated.
var interval = 60; // changing this to make the game feel faster.
// an array to store solution values
var results = [];
var solution = null;
// stores if the game is over or not.
var gameOver = false;
function setup() {
createCanvas(800, 300);
}
function draw() {
// if there are 3 losses or 10 attempts stop the game.
var gameScore = getGameScore(results);
if (gameScore.loss === 3 || gameScore.total === 10) {
gameOver = true;
displayGameOver(gameScore);
return;
}
background(0); // black background
if (frameCount === 1 || frameCount % interval === 0) {
solution = null;
guessItem = new GuessItem(width/2, height/2, 1);
}
if (guessItem) {
guessItem.render();
}
if (solution == true || solution === false) {
// displaying a text on screen instead of flat color.
solutionMessage(gameScore.total, solution);
}
}
function solutionMessage(seed, solution) {
// display a random message based on a true of false solution.
var trueMessages = [
'GOOD JOB!',
'DOING GREAT!',
'OMG!',
'SUCH WIN!',
'I APPRECIATE YOU',
'IMPRESSIVE'
];
var falseMessages = [
'OH NO!',
'BETTER LUCK NEXT TIME!',
'PFTTTT',
':('
];
var messages;
push();
textAlign(CENTER, CENTER);
fill(237, 34, 93);
textSize(36);
randomSeed(seed * 10000);
if (solution === true) {
background(255);
messages = trueMessages;
} else if (solution === false) {
background(0);
messages = falseMessages;
}
text(messages[parseInt(random(messages.length), 10)], width / 2, height / 2);
pop();
}
function displayGameOver(score) {
// create the Game Over screen
push();
background(255);
textSize(24);
textAlign(CENTER, CENTER);
translate(width / 2, height / 2);
fill(237, 34, 93);
text('GAME OVER!', 0, 0);
translate(0, 36);
fill(0);
// have spaces inside the string for the text to look proper.
text('You have ' + score.win + ' correct guesses', 0, 0);
translate(0, 100);
textSize(16);
var alternatingValue = map(sin(frameCount / 10), -1, 1, 0, 255);
fill(237, 34, 93, alternatingValue);
text('PRESS ENTER', 0, 0);
pop();
}
function getGameScore(score) {
// given a score array, calculate the number of wins and losses.
var wins = 0;
var losses = 0;
var total = score.length;
for (var i = 0; i < total; i++) {
var item = score[i];
if (item === true) {
wins = wins + 1;
} else {
losses = losses + 1;
}
}
return {
win: wins,
loss: losses,
total: total
};
}
function restartTheGame() {
// sets the game state to start.
results = [];
solution = null;
gameOver = false;
}
function keyPressed() {
// if game is over, then restart the game on ENTER key press.
if (gameOver === true) {
if (keyCode === ENTER) {
console.log('restart the game');
restartTheGame();
return;
}
}
if (guessItem !== null) {
// check to see if the pressed key matches to the displayed number.
// if so set the solution global variable to a corresponding value.
console.log('you pressed: ', key);
solution = guessItem.solve(key);
console.log(solution);
if (solution) {
results.push(true);
} else {
results.push(false);
}
guessItem = null;
} else {
console.log('nothing to be solved');
}
}
function GuessItem(x, y, scl) {
this.x = x;
this.y = y;
this.scale = scl;
this.scaleIncrement = 0.25;
this.clr = 255;
this.content = getContent();
this.alpha = 255;
this.alphaDecrement = 6;
this.solved = null;
this.contentMap = {
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine',
'0': 'zero'
};
this.colors = [
[63, 184, 175],
[127, 199, 175],
[218, 216, 167],
[255, 158, 157],
[255, 61, 127],
[55, 191, 211],
[159, 223, 82],
[234, 209, 43],
[250, 69, 8],
[194, 13, 0]
];
function getContent() {
// generate a random integer in between 0 and 9
return String(parseInt(random(10), 10));
}
this.solve = function(input) {
// check to see if the given input is equivalent to the content.
// set solved to the corresponding value.
var solved;
if (input === this.content) {
solved = true;
} else {
solved = false;
}
this.solved = solved;
return solved;
}
this.drawEllipse = function(size, strkWeight, speedMultiplier, seed) {
// draw an animated ellipse with a random color to the screen.
push();
randomSeed(seed);
translate(this.x, this.y);
var ellipseSize = this.scale * speedMultiplier;
scale(ellipseSize);
var clr = this.colors[parseInt(random(this.colors.length), 10)]
stroke(clr);
noFill();
strokeWeight(strkWeight);
ellipse(0, 0, size, size);
pop();
}
this.render = function() {
push();
this.drawEllipse(100, 15, 2, 1 * this.content * 1000);
this.drawEllipse(60, 7, 2, 1 * this.content * 2000);
this.drawEllipse(35, 3, 1.2, 1 * this.content * 3000);
pop();
push();
fill(this.clr, this.alpha);
textAlign(CENTER, CENTER);
translate(this.x, this.y);
scale(this.scale);
// display the word for the corresponding number
text(this.contentMap[this.content], 0, 0);
// increase the scale value by the increment value with each render
this.scale = this.scale + this.scaleIncrement;
// decrease the alpha value by the decrement value with each render
this.alpha = this.alpha - this.alphaDecrement;
pop();
}
}
Listing 13-23The final code
图 13-2
Screen from the final game code
代码最大的变化是solutionMessage
函数,所以让我们更详细地看一下(清单 13-24 )。以前我们只是使用基于solution
变量的值的if-else
语句来决定屏幕上显示什么。如果解决方案是true
,我们显示白色背景,如果解决方案是false
,我们显示黑色背景。
现在,如果解决方案是这些值(true
或false
)中的一个,我们将把它传递给一个名为solutionMessage
的函数,该函数使用gameScore.total
作为random
函数的种子,选择一个随机消息来显示。
if (solution == true || solution === false) {
solutionMessage(gameScore.total, solution);
}
Listing 13-24Displaying a message on the screen
如清单 13-25 所示,在solutionMessage
函数中,有两个数组,它们包含一堆基于solution
的值显示的消息值。
if (solution === true) {
background(255);
messages = trueMessages;
} else if (solution === false) {
background(0);
messages = falseMessages;
}
Listing 13-25Conditionally choosing a message
在清单 13-26 中,我们通过将random
函数的返回值转换成整数,从这些数组中选取一个随机值。
text(messages[parseInt(random(messages.length), 10)], width / 2, height / 2);
Listing 13-26Choosing a random message
摘要
这绝对是一个具有挑战性的例子,它检验了我们目前所学的一切。
令人印象深刻的是,我们只需使用 p5.js 就可以构建一个可以在网络上运行、可供数百万人玩的游戏。这也没那么难。整个程序只有 200 行左右。当然还有改进的空间,我们可以根据玩家的表现使游戏难度动态化,添加更多的视觉天赋,并添加一个动态评分系统,在这个系统中,我们可以根据猜测数字所需的时间为正确的猜测分配不同的分数。游戏可以转换成显示文字而不是数字。它可以显示你需要输入名字的图像或者你需要回答的计算。可能性数不胜数!
话虽如此,如果我们想构建更高级的项目,p5.js 可能不是创建游戏的最佳平台。一个合适的游戏库应该有一些特性,比如资源加载系统,精灵支持,碰撞检测,物理引擎,粒子系统…这些在构建高级游戏时经常需要。不过,这并不是说你不能用 p5.js 来构建游戏。我们刚刚证明了这是完全可能的。只是有其他的库围绕着这个解决方案更加专业,而 p5.js 更适合于在网络上创建交互式的动画体验。但是通过学习 p5.js,你不仅学习了如何使用 JavaScript 和它擅长的所有事情,而且你也发展了对在 JavaScript 生态系统中与其他第三方库合作的理解。