p5.js 学习手册(二)

原文:Learn JavaScript with p5.js

协议:CC BY-NC-SA 4.0

八、函数

函数是 JavaScript 的主要构件。它们允许我们以更有效和可伸缩的方式编写程序。函数通过在单个可执行名称下包含和分组操作来帮助我们管理复杂性。我们已经知道如何使用 p5.js 预定义的函数如ellipsebackground来调用函数。我们甚至将自己的函数声明为 p5.js 迫使我们将代码放入两个函数声明中:setupdraw。如果我们想要创建我们自己的函数,我们将遵循我们一直用于创建或声明这些函数的相同约定。

创建函数

为了创建(或声明)一个新的函数,我们将从使用function关键字开始,然后给函数起一个我们选择的名字,这个名字理想地描述了函数的行为或目的。见清单 8-1 。

function functionName() {
        // function body
}
Listing 8-1Creating a function

在函数名的旁边,我们会打开括号。如果我们想要构建一个处理用户输入的函数,我们可以在括号内定义参数,作为将来用户输入的占位符变量名。我们稍后会看到这是如何工作的。

然后我们有花括号。在花括号内可以称为函数体。在那里,我们编写构建函数逻辑的代码。我们还可以使用参数,即我们在函数名旁边的括号中定义的变量名,作为我们希望在函数体内执行的操作的一部分。

让我们看一个简单的例子。注意 p5.js 有一个ellipse函数,但没有一个circle函数。这不是一个真正的问题,因为我们可以通过提供具有相同宽度和高度值的ellipse函数来创建一个圆。不过,为了便于讨论,让我们创建一个使用三个值的circle函数:我们想要画圆的xy位置以及圆的直径。

清单 8-2 展示了如何去做。在括号内,我们将写下变量名,这些变量名最终将在调用该函数时提供。这些名称被称为参数,因为它们参数化了我们正在创建的操作的函数。我们将在函数中使用这些参数,以允许用户控制函数的内部工作。

function circle(x, y, diameter) {
        ellipse(x, y, diameter, diameter);
}
Listing 8-2Declaring a circle function

我们可以选择任何东西作为参数名,但是使用能清楚表达意图的名称通常是有意义的。所以在我们的例子中,使用名字xydiameter是有意义的。

在定义了这个函数之后,我们可以通过使用它的名字并为它提供值来调用它。提供给函数的值称为函数的参数。请注意,如果没有提供所有必需的参数,函数可能会失败或无法按预期工作(清单 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

但是如果我们想在另一个计算中使用这个结果,我们可能会遇到障碍。这个函数不会将数字返回给我们;它只是把它显示在屏幕上。调用这个函数会影响我们所处的环境,但是它不会返回一个值来用于进一步的计算。到目前为止,我们看到的一些函数,如ellipserect等,以类似的方式运行,它们做一些事情,但实际上并不返回值作为计算的结果。然而,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 中工作时有一点有点奇怪。我们从来没有真正调用过setupdraw函数,然而它们还是被执行了!这是由 p5.js 的架构决定的。p5.js 为我们处理setupdraw函数的执行,因为它们的执行遵循一些简单的规则,如下所示:

  • setup函数在draw函数之前执行。
  • setup函数只执行一次,而draw函数以一定的默认速率连续执行。

摘要

从我们开始使用 p5.js 的那一刻起,我们就在使用函数。它自己的架构依赖于我们程序中两个函数的存在,这两个函数的名字必须是setupdraw。而且,我们一直在使用 p5.js 库自带的函数,比如ellipserect等。

我们已经看到,函数可以被构建为使用或不使用外部用户输入。我们也可以构建使用或者不使用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

在这个例子中,我们在setupdraw函数范围之外初始化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对象有一个定义其坐标的xy属性以及一个定义其大小的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 )。我们在创建函数的对象内部所做的声明与我们在使用对象初始化器时所做的非常相似。注意我们是如何在函数中使用widthheight 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_1circle_2circle_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 次!).

让我们在drawsetup函数之外创建一个名为words的变量(清单 10-14 )。因为变量是在setupdraw函数之外初始化的,所以它可以在这两个函数中使用。

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 中有许多事件函数,我们可以声明它们来利用事件系统。这里我们将关注两个事件函数:mousePressedkeyPressed事件函数。

使用鼠标按下

这个想法类似于drawsetup函数,我们用这个特殊的名字声明这个函数,p5.js 以一种特殊的方式处理它(就像setupdraw函数一样)。

在 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 是一个有用的库,这使得通过为它们提供预定义的变量来识别一些键变得更加容易,例如:BACKSPACEDELETEENTERRETURNTABESCAPESHIFTCONTROLOPTIONALTUP_ARROWDOWN_ARROWLEFT_ARROWRIGHT_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变量,我们可以通过一点解码来识别哪个字母数字键被按下。但是还有另一个特别适合字母数字字符的变量,叫做keykey变量按原样存储被按下的字母数字键的值,这样更容易识别哪个键被按下了。

摘要

在这一章中,我们学习了一种更好的处理事件的方法,那就是事件函数。我们特别关注两个事件函数:mousePressedkeyPressed事件函数。

我们还学习了一些可以在keyPressed函数中使用的变量:keykeyCode。使用key使得识别字母数字按键更容易,而keyCode对于检测其他按键是理想的,因为它可以与 p5.js 变量进行比较,如ENTERSPACE等。这使得识别这些按钮更加容易。

从 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功能时,允许我们绘制中心位于原点的形状。基本上,我们从在原点绘制形状开始,因为所有的变换函数都相对于该点起作用。然后我们使用translaterotate功能将形状移动到想要的位置和角度。使用这种方法,我们需要记住在translate函数后调用rotate,否则旋转仍然会相对于原始原点发生,这可能不是我们想要的。

一般来说,这种方法和使用转换函数的缺点是,从这一点开始,我们绘制的所有东西都将使用这个新的原点。解决这个问题的方法是使用pushpop函数。

推动和弹出

p5.js push函数允许我们创建一个新的状态,而pop函数将状态恢复到以前的状态。这允许我们对单个对象应用完全不同的设置,而不用担心这些设置是否会影响后面的形状,只要我们在pushpop调用之间做所有的事情。同样,在示例中更容易看出这一点(列表 12-7 和图 12-5 )。

根据我们当前的设置,我们在translaterotate函数后绘制的所有东西都将应用 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 中,让我们在这里实现pushpop函数,这样我们就可以隔离我们正在应用于更大矩形的变换。结果见图 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

太棒了!无论我们在pushpop函数之间做什么,都不会影响这些函数调用之外的任何东西。需要注意的是,我们总是一起调用pushpop函数。使用一个而不使用另一个没有任何意义。

在清单 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函数,但是在内部使用pushpop来保存状态和设置转换,并且它接受一个可选的旋转参数。如果没有提供旋转参数,那么它将假设值为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函数是如何工作的。我们学习了translaterotate函数。我们还学习了angleMode函数,它让我们设置rotate函数使用的单位。

然后,我们学习了pushpop函数,并了解了如何将它们与转换函数结合使用,以隔离状态并将转换应用于各个形状。虽然这些函数对学习 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方法中的每个操作都存在于pushpop函数调用中。这允许我们包含在这个对象包含的这个方法中发生的设置和转换相关的状态变化。这里,我们使用translatescale变换函数来改变文本对象的位置和大小。我们之前没有看到scale函数,但它是一个与translaterotate函数非常相似的变换函数。顾名思义,它控制的是绘图区域的比例,和其他变换函数的工作原理类似,所以最好将其包含在pushpop函数之间。

我们可以使用一个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

我们还在名为scaleIncrementGuessItem构造函数中添加了一个控制缩放速度的新变量。使用该值可以改变动画的速度。例如,我们可以增加这个值来增加游戏难度。

在清单 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

我们在很多地方更新了代码。为了能够完成我们的任务,我们在名为solveGuessItem对象上实现了一个新方法。solve方法获取用户输入,并根据给定的用户输入是否与GuessItem content变量匹配,返回truefalse。我们最终将结果保存在一个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 sinmap函数,我们用它们来创建一个闪烁的文本。一个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,我们显示黑色背景。

现在,如果解决方案是这些值(truefalse)中的一个,我们将把它传递给一个名为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 生态系统中与其他第三方库合作的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值