65.9K
CodeProject 正在变化。 阅读更多。
Home

学习 JavaScript:第 1 部分 - 创建星空

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (96投票s)

2013年8月24日

CPOL

10分钟阅读

viewsIcon

179600

downloadIcon

1985

在这篇文章中,我们将使用 JavaScript 创建一个星空。我们将了解核心语言功能如何工作、如何创建类以及如何使用 HTML5 Canvas。

引言

查看我们将制作的示例:dwmkerr.com/experiments/starfield

JavaScript 的普及度正在爆炸式增长。在本系列文章中,我们将通过实践来学习 JavaScript。我不会深入探讨语法和设计模式的理论讨论——我将制作项目,我们将在实践中学习理论。

本系列文章的理念是您可以随时加入。查看下面的目录,找到一篇您感兴趣的文章。在每篇文章中,我们都会实际创建一些东西,并在此过程中学习一些技巧。

第 1 步 - 创建一个网页

我们需要一个超简单的网页来运行星空。让我们创建最精简的 HTML 模板

<!DOCTYPE html>
<html>
    <head>
        <title>Starfield</title>  
    </head> 
    <body>
        <div id="container"></div>
    </body>
</html>  

我们创建了一个轻量级的 HTML 页面,其中包含一个 div,这个 div 将承载我们的星空。我们首先要确保它填满屏幕,可以通过在 head 中添加一些 CSS 样式来实现

<head>
    <title>Starfield</title>
    <style>
        #container {
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0px;
            top: 0px;
        }
    </style>
</head> 

现在让我们创建一个新的 JavaScript 文件,我们称之为 starfield.js。将其放在与 HTML 文件(我们可以称之为 index.html)相同的目录中。让我们确保在 HTML 中包含 starfield

<body>
   <!-- snip -->
   <script src="starfield.js"></script>
</body>   

第 2 步 - 创建 Starfield 类

如果您不熟悉 JavaScript,那么这里的事情将变得有趣。我们将创建一个类来表示 Starfield。在 starfield.js 文件中,添加以下代码

//  Define the starfield class.
function Starfield() {
    this.fps = 30;
    this.canvas = null;
    this.width = 0;
    this.height = 0;
    this.minVelocity = 15;
    this.maxVelocity = 30;
    this.stars = 100;
    this.intervalId = 0;
} 

如果您之前在 JavaScript 中创建过类,可以跳过这部分解释,否则……

JavaScript 类

我们刚刚写的内容对于 C++ 或 C# 开发者来说,看起来不像一个类。它看起来像一个函数。嗯,它就是。在 JavaScript 中,没有类(尽管在 ECMAScript 6 中我们会得到它们)。但这实际上并不能阻止我们创建类,或者至少是类似类的对象。函数是对象——我们可以创建它们的实例,并且可以设置它们的属性。我们将像这样创建一个 Starfield

//  Create a starfield
var starfield = new Starfield();  

这非常重要。我们创建了该函数的一个新实例并调用了它。通过调用它,我们已经在 function 对象上设置了一些属性。我们只在创建的 function 对象上设置了它们——而不是每个实例。

现在就像创建属性一样,我们也可以创建 function,例如

//  Define the starfield class
function Starfield() {
    /* snip */
    this.start = function() { /* do something*/ };
} 

我们可以像这样调用这个 function

//  Create a starfield.  
var starfield = new Starfield();
starfield.start(); 

但我们实际上可以做得更好一点。如果我们的函数很复杂,那么为每个实例重新创建它实际上效率不高,我们真正想要做的是只创建一次 function,让每个实例自动获取它。这就是原型发挥作用的地方。

当您在由 function 创建的类型实例后面加上点号时,引擎将尝试在该类型上查找属性。如果找不到,它会查看该类型的“prototype”。当我们使用函数创建类型实例时,它每次都会继承相同的原型。我的意思是这样

//  Define the starfield class.
function Starfield() {
    /* snip */
}

//  Add a function to the starfield class
Starfield.prototype.start = function() {
    /* here's the function */
}; 

我们修改了 Starfield 函数的 prototype,这意味着每次创建 Starfield 时,它都会获得 start 函数,即使我们只声明了一次。

还是不清楚?想想这个

var starfield = new Starfield();
starfield.stop = function() { /* do something */  }; 

我们已经在 starfield 的一个实例上创建了一个 stop 函数——所以我们只能在那个 starfield 上调用 stop。那么这个呢?

var starfield = new Starfield();
Starfield.prototype.pause = function() { /* do something */  }; 

现在我们在 Starfieldprototype 上创建了一个名为 'pause' 的 function。这意味着我们可以从任何 starfield 实例调用它。为什么?因为引擎首先在实例上查找 'pause',但没有找到。然后它在实例的 prototype 上查找。prototype 是所有 Starfield 共享的,所以它找到了!

这在开始时确实很难适应,我能推荐的最有用的事情是多玩玩它。创建一些类,阅读关于 prototype 的资料并尝试一下。

第 3 步 - 初始化星空

我们有点跑题了,但这就是文章的目的。我们创建了带有一些属性的 Starfield 类,现在让我们实际做点什么。

我们的目标是实现两个函数——第一个将初始化 starfield,使其准备好使用;第二个将启动它,实际运行动画。让我们先编写 initialise

还记得 prototype 吗?我们将在那里放置 initialise 函数——因为每个 starfield 都需要它。

//  The initialise function initialises a starfield object so that
//  it's ready to be started. We must provide a container div, that's
//  what the starfield will live in.
Starfield.prototype.initialise = function(div) {
    
    var self = this;
 
    //  Store the div
    this.containerDiv = div;
    self.width = window.innerWidth;
    self.height = window.innerHeight;
 
    window.addEventListener('resize', function resize(event) {
        self.width = window.innerWidth;
        self.height = window.innerHeight;
        self.canvas.width = self.width;
        self.canvas.height = self.height;
        self.draw();
    });
 
    //  Create the canvas
    var canvas = document.createElement('canvas');
    div.appendChild(canvas);
    this.canvas = canvas;
    this.canvas.width = this.width;
    this.canvas.height = this.height;
}; 

这是一篇学习 JavaScript 的文章,所以我将逐一讲解。如果你觉得它很容易理解,可以跳过。

Starfield.prototype.initialise = function(div) { 

我们正在向 starfield prototype 添加一个 function,这意味着每个 starfield 都能够使用它。该 function 接受一个参数,它是一个 div。它没有类型,因为 JavaScript 没有类型(尽管如果你想要类型,可以查看 TypeScript)。

var self = this;

//  Store the div.
this.containerDiv = div;
self.width = window.innerWidth;
self.height = window.innerHeight; 

我们正在将“this”变量的副本存储在一个局部变量中。原因很快就会变得明显……接下来,我们存储对提供给我们的 div 的引用(注意到我们没有在构造函数中创建“containerDiv”吗?没关系,您可以根据需要创建属性。我通常在构造函数中创建它们,以便我可以快速查看应该有哪些属性。)

我们还存储了浏览器窗口的客户端区域。“window”对象由浏览器提供,它允许您对浏览器执行许多操作。

window.addEventListener('resize', function resize(event) {
    self.width = window.innerWidth;
    self.height = window.innerHeight;
    self.canvas.width = self.width;
    self.canvas.height = self.height;
    self.draw(); 
});  

现在我们将处理窗口的“resize”事件。有两种方法可以做到这一点。第一种是设置“onresize”函数,第二种是使用“addEventListener”。

通常,我们应该使用“addEventListener”函数,因为这不会阻止任何已经添加的其他事件的工作。如果我们直接设置“onresize”,我们会替换之前可能存在的任何内容。因此,通过使用“addEventListener”,我们确保不会干扰其他库。当函数被调用时,我们将更新我们的 widthheight,更新 canvas widthheight(我们稍后将创建 canvas),并调用“draw”函数,我们也将很快创建它。

为什么我们使用“self”而不是 this

好的,我们在 'initialise' 函数中编写这段代码,在 initialise 函数的上下文中,'this' 是 Starfield。但是当窗口为我们调用 'resize' 函数时,当我们在那个 function 中时,'this' 实际上是窗口。所以为了编辑 starfield 实例,我们使用我们之前声明的 'self' 变量,它是对 starfield 的引用。

这实际上是相当高级的——function 被调用了,我们却以某种方式使用了在函数外部创建的变量。这被称为闭包,它让我们的生活轻松多了!闭包允许我们从另一个上下文访问状态。在编写回调函数等等时,这是一件非常有用的事情。

    //  create the canvas
    var canvas = document.createElement('canvas');
    div.appendChild(canvas);
    this.canvas = canvas;
    this.canvas.width = this.width;
    this.canvas.height = this.height;
}; 

这是 initialise 函数的最后一部分。我们使用“document”对象创建一个新的 HTML 元素。

document 对象在基于 Web 的 JavaScript 开发中极其重要——为什么呢?它代表了“DOM”(文档对象模型)。这实际上是 HTML 页面的树形结构——节点、元素、属性等等。我们在客户端 JavaScript 中所做的大量工作都与 DOM 相关,我们改变元素的样式,添加新项目等等。在这种情况下,我们使用 document 创建一个新的 HTML Canvas,然后将其添加到我们的 container div 中——然后我们设置它的 widthheight

这是基础——我们刚刚通过编程创建了 HTML,这是我们将在 JavaScript 中经常做的事情之一。

这就是初始化!

我们来回顾一下

  1. 存储 div
  2. 存储有用的属性,widthheight
  3. 监听窗口大小调整,当它发生时,更新 widthheight 并重新绘制。
  4. 创建一个 canvas 用于绘制,并使其成为 container div 的子元素。

第 4 步 - 启动星空

这是有趣的部分,我们现在可以实际创建主要的 starfield 逻辑了。让我们创建 starfieldstart 函数,它将启动 starfield 运行。

Starfield.prototype.start = function() {
 
    //  Create the stars
    var stars = [];
    for(var i=0; i<this.stars; i++) {
        stars[i] = new Star(Math.random()*this.width, Math.random()*this.height, 
                   Math.random()*3+1,
        (Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
	}
    this.stars = stars; 

我们正在添加一个 function,就像之前通过使用 prototype 一样。我们做的第一件事是创建一个数组——就是以 'var stars' 开头的那一行。通过将其设置为 '[]',我们将其设置为一个数组。数组在 JavaScript 中功能非常强大,我们也可以将其用作队列或列表。现在我们遍历星星的数量(我们在构造函数中设置了它),每次都在数组中创建一个 Star——等等,什么是 star?将这段代码放在文件的末尾——而不是我们正在编写的函数中!

function Star(x, y, size, velocity) {
    this.x = x;
    this.y = y; 
    this.size = size;
    this.velocity = velocity;
} 

Function 作为类是不是很奇怪?但它们也相当容易使用!我想表示 star 对象,所以我有一个函数可以设置一些属性。在函数上调用 'new' 会根据它实例化一个类型,并带上我提供的属性。这就是我将 'star' 对象添加到数组的方式。我使用 Math.random,这是一个标准的 JavaScript 函数,它返回一个介于 01 之间的值,用于随机化星星的初始位置、大小和速度。

现在回到函数——我们创建了数组,现在我们将其存储在 'this' 中——即 Starfield 对象。接下来,我们将使用 JavaScript 的 setInterval 函数。该函数设置一个回调,每当一个间隔过去时就会调用该回调。我们的间隔由 fps(每秒帧数)指定。每次我们到达该函数时,我们都会调用 'update' 和 'draw'。我们使用 'self' 闭包来确保我们在 starfield 对象上调用它们!

    var self = this;
    //	Start the timer.
    this.intervalId = setInterval(function() {
        self.update();
        self.draw();	
    }, 1000 / this.fps);
};  

我们还存储了由 'setInterval' 返回的 id——我们可以用它来稍后停止它。

现在我们将创建 update 函数,它将 update starfield 的状态。

Starfield.prototype.update = function() {
    var dt = 1 / this.fps;
    for(var i=0; i<this.stars.length; i++) {
        var star = this.stars[i];
        star.y += dt * star.velocity;
        //  If the star has moved from the bottom of the screen, spawn it at the top.
        if(star.y > this.height) {
            this.stars[i] = new Star(Math.random()*this.width, 0, Math.random()*3+1, 
               (Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
        }
    }
};

这是移动 star 的核心逻辑——我们计算已经过去的时间(dtdelta t)。然后我们遍历每个 star,并根据其速度和已过去的时间更新其位置。

最后,如果 star 已经移到屏幕底部之外,我们会在顶部创建一个新的 star

接下来是 draw 函数

Starfield.prototype.draw = function() {
    
    //  Get the drawing context
    var ctx = this.canvas.getContext("2d");
 
    // Draw the background
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, this.width, this.height);
 
    //  Draw stars
    ctx.fillStyle = '#ffffff';
    for(var i=0; i<this.stars.length;i++) {
        var star = this.stars[i];
        ctx.fillRect(star.x, star.y, star.size, star.size);
    }
};  

信不信由你,这是一个新的 HTML5 特性——CanvasCanvas 是一个对象,您可以使用它在 JavaScript 中进行基于位图的绘制。您可以绘制线条、多边形等等。事实上,您几乎可以用它绘制任何东西。

我们所需要做的就是用黑色填充背景,将填充颜色设置为白色,并为每个 star 绘制一个小的矩形。

第 5 步 - 试一试!

我们做到了!将代码添加到您的 HTML 页面中

<body>
    <!-- snip -->
    <script>
        var container = document.getElementById('container');
        var starfield = new Starfield();
        starfield.initialise(container);
        starfield.start();
    </script>
</body>

运行页面,您就看到了。

我们学到了什么?

以下是我们学到知识的总结

  • 在 JavaScript 中,类使用“constructor”函数创建。
  • 调用“constructor”函数时使用“new”来创建该类型的新实例。
  • constructor”函数有一个名为“prototype”的属性,并且在所有类型实例之间共享。
  • 通常,类成员函数在 prototype 上定义。
  • JavaScript 'window' 对象由浏览器提供,代表代码运行的环境。
  • JavaScript 'document' 对象由引擎提供,表示 HTML 文档。
  • 在 JavaScript 中可以使用 'setInterval' 创建定时器。
  • JavaScript 中的 'this' 关键字应谨慎使用——在回调函数中,'this' 可能不是您所期望的。
  • 你可以使用 var array = []; 在 JavaScript 中创建一个数组。
  • 您可以在回调函数中使用在回调函数外部定义的变量,这是一个闭包。

告诉我你的想法

如果您觉得这篇文章有用,请告诉我——如果有什么我可以做得更清楚的地方,也请告诉我。

下一篇文章将是“用 JavaScript 创建太空侵略者”。

历史

  • 2014年1月31日:初始版本
© . All rights reserved.