博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Building JavaScript Games for Phones Tablets and Desktop(3)-创造一个游戏世界
阅读量:5950 次
发布时间:2019-06-19

本文共 9324 字,大约阅读时间需要 31 分钟。

hot3.png

创造一个游戏世界

这章教会你如何通过内存中储存的信息创造一个游戏世界。介绍了基本类型和变量并且这些变量是如何储存和改变信息的。接下来,你会看到如何用对象储存更复杂的信息,里面包含成员变量和方法。

基本类型和变量

先前的章节几次讨论到了内存。你已经看到了如何执行一个简单的指令譬如canvasContext.fillStyle = “blue”;为画布设置一个颜色。在这章例子里,你使用内存储存临时信息,是为了记住一些简单运算的结果。在这个例子里,你使用经过的时间来改变背景色。

类型

类型,或者数据类型,代表不同类型的信息。先前的例子使用了不同类型的信息,当做参数传递给了函数。例如,fillRect函数,需要4个整数,start函数需要一个文本标识引用自canvas,update和draw函数不需要任何信息。浏览器/解释器可以区分这些不同类型的信息并且很多情况下可以把一个类型的信息转换成另一个类型的信息。比如,在JavaScript里,你可以使用单引号或者双引号来标识文本。如下的两句指令是一样的:

canvas = document.getElementById("myCanvas");canvas = document.getElementById('myCanvas');

浏览器可以自动的在不同类型的信息间进行转换。比如,下面的语句不会导致语法错误:

canvas = document.getElementById(12);

这个当做参数传递的整数会被转换成文本。在这种情况下,当然,这里没有ID叫做12的canvas,所以这个程序是不正确的。如果你像下面一样替换canvas的ID,程序就会正常运行:

浏览器自动的在文本和数字之间进行转换。

现代编程语言都比JavaScript严格的多。比如Java和C#,不同类型之间转换是有限制的。大多时候,你必须明确的告诉编译器这里有一个类型转换。

为什么对类型转换需要这么严格的限制呢?首先,一个函数或方法中明确的类型参数可以让程序员简单的使用函数。比如下面这样:

function playAudio (audioFileId)

简单的看一下,你并不确定audioFileId是一个数字还是文本。在C#里,这个函数会类似这样:

void playAudio(string audioFileId)

可以看出,不仅有参数名,而且还附有参数类型。类型是string,也就是字符串。此外,在函数名前面还有void这个单词,表示这个函数没有返回值。

声明和变量赋值

在JavaScript里面储存信息和使用信息都是很简单的。你需要做的只是为你要引用的信息提供一个名字。这个名字就叫做变量。当你想在程序里使用变量时,在使用之前声明它是一个好习惯。下面是如何声明一个变量:

var red;

在这里,red就是变量名。你可以在你随后的程序用它储存信息。

当你声明了一个变量,你不需要提供你需要储存信息的类型。变量仅仅是内存里有一个名字。有些编程语言要求在变量声明时确定变量的类型。比如在Java或者C++里。然而,很多的脚本语言(包括JavaScript)允许你声明一个变量而不需要指定其类型。当编程语言声明变量时不要指定类型,这个编程语言就有松散类型。在JavaScript里,你可以声明不止一个变量,比如:

var red, green, fridge, grandMa, applePie;

这里你声明了5个不同的变量。当你声明这些变量时,你没有赋值。在这种情况下,变量是未定义的。你可以用赋值指令为变量赋值。比如,给red赋值:

red = 3;

赋值指令包含下面这些:

  • 变量名
  • ”=“符号
  • 变量值
  • 一个分号

你可以发现赋值指令中间有个等号。然而,在JavaScript里,最好把”=“看成”变为“而不是”等于“。毕竟,变量还没有等于右边的值,它在指令执行之后才变成那个值。下面的语法图描述了赋值指令。(图3-1)

(省略图3-1)

现在知道如何声明一个变量和给变量赋值了。如果你在声明变量时就知道给变量赋什么值,你可以声明的同时进行赋值。比如:

var red = 3;

当执行这句后,内存中就会有3这个值,如图3-2所示。

(省略图3-2)

这里有更多关于数值变量的声明和赋值:

var age = 16;var numberOfBananas;numberOfBananas = 2;var a, b;a = 4;var c = 4, d = 15, e = -3;c = d;numberOfBananas = age + 12;

在第四行,你可以发现一次可以声明多个变量。也可以像第六行一样有多个变量进行了声明赋值。在赋值的右边,你可以放置其它变量或者数学表达式,正如最后两行所示。指令 c = d;让储存在d中的值也储存在了c里面。因为d是15,所以执行这条指令后,c也是15.最后一条指令让age加上12,把结果储存在numberOfBananas里面。概括来说,执行这些指令后,这些内存值看起来就像图3-3所示:

(省略图3-3)

全局变量和严格模式

除了在使用变量之前进行声明,javascript也可以直接使用变量而不用声明它。比如下面这样:

var a = 3;var b;b = 4;x = a + b;

如上所示,前两行指令通过var关键字声明了变量a和b。变量x从来没有声明,但是它用来储存a和b的和。javascript允许这样。这很糟糕,而且这也是它为什么糟糕的原因。问题在于没有声明的变量javascript解释器会自动的把它进行声明,而你却完全不知道。如果你在其它地方使用同样名字的变量,你的程序可能发生你意想不到的结果。另外,如果你使用了很多不同的变量,你最好同时记录这些变量。但是最大的问题是下面这个:

var myDaughtersAge = 12;var myAge = 36;var ourAgeDifference = myAge - mydaughtersAge;

当编写这些指令时,你希望的是ourAgeDifference的值是24.但是,这个值是未定义的。这是因为第三行这里有个打字错误。变量的名字不是mydaughtersAge,而是myDaughtersAge。这种情况下,浏览器/解释器是悄悄的声明一个叫做mydaughtersAge的变量而不是抛出一个错误停止运行脚本。因为变量没有定义,所有与此变量有关的计算都是未定义的。因此,变量ourAgeDifference也是未定义的。

这个问题真的很棘手。幸运的是,新的EMCAScript5标准有个叫做严格模式的东西。当脚本用严格模式来解释时,它不允许在没有声明变量之前使用变量。如果你想你的脚本在严格模式下执行,你需要做的仅仅只是在脚本开始加上下面这行,例如:

"use strict";var myDaughtersAge = 12;var myAge = 36;var ourAgeDifference = myAge - mydaughtersAge;

“use strict”告诉了解释器在严格模式下解释脚本。如果你现在执行这段代码,浏览器会停止运行且抛出错误告知有变量没有进行声明。

除了能检查变量在使用之前是否声明了,严格模式也包含了其他一些能让书写正确javascript代码更简单的东西。

我非常推荐你在严格模式下书写你所有的javascript代码。所以本书的所有javascript代码都是严格模式下书写的,这能帮程序员省掉很多麻烦且这样的代码在未来版本的javascript也是无可挑剔的。

指令和表达式

如果你看到语法图里面的元素,你可能已经注意到一些在赋值语句右边的值或者程序片段,它们被叫做表达式。那么表达式和指令有什么不同呢?两者不同的是指令某种方式上改变内存,而表达式有一个值。指令通常使用表达式。这里有几个表达式的例子:

16numberOfBananas2a + 4numberOfBananas + 12 - a-3"myCanvas"

所有的这些表达式代表了一个确定的类型。除了最后一行,所有的表达式都是数值。最后一行是字符串。除了数值和字符串,还有其他类型的表达式。我讨论的是本书最重要的表达式。比如,下节我会讨论运算符的表达式,第7章会讲使用函数或者方法作为一个表达式。

运算符和更复杂的表达式

这节讨论javascript中不同的运算符。你会了解到运算符的优先级。你也可以了解到有些时候,在javascript中表达式也能相当的复杂。比如,一个变量可以包含多个值,或者可以代表一个函数。

算术运算符

(省略)

运算符优先级

(省略)

把一个函数赋值给变量

在javascript中,函数被储存在内存里。正因为如此,函数也是表达式。所以,可以给一个变量赋值为函数。例如:

var someFunction = function () {// do something}

这个例子声明了一个变量并进行了赋值。这个变量的值是一个无名函数。如果你想执行这个函数,你可以通过变量名字调用,如下:

someFunction();

那么像下面这样定义一个函数和你之前看到的有什么不同呢?

function someFunction () {// do something}

实际上,这并没有什么不同。主要是如果不像传统方式那样定义一个函数,那么这个函数在定义之前不能使用。当浏览器执行一个javascript文件,有两个步骤。第一个步骤,浏览器构造一个函数的列表。第二步,浏览器解释剩下的脚本。这对正确执行脚本很有必要,浏览器需要知道哪个函数是有效的。比如,下面的这段代码可以运行,即使这个在后面被定义:

someFunction();function someFunction () {// do something}

然而,如果一个函数被赋值给一个变量,那么就只剩上述的第二步了。意味着下面的代码会报错。

someFunction();var someFunction = function () {// do something}

浏览器会告知有一个变量没有进行声明。在定义之后进行调用就可以了,比如:

someFunction();var someFunction = function () {// do something}

多个值组成的变量

一个变量可以由多个值组成而不是只能单一值。这就像函数里面做的一样,把指令组合在一起。比如:

function mainLoop () {canvasContext.fillStyle = "blue";canvasContext.fillRect(0, 0, canvas.width, canvas.height);update();draw();window.setTimeout(mainLoop, 1000 / 60);}

跟函数一样,你可以把一些变量放在一个更大的变量里面。这个更大的变量就有了更多的值。就像下面这个例子:

var gameCharacter = {name : "Merlin",skill : "Magician",health : 100,power : 230};

这是一个复合变量的例子。变量gameCharacter有一些变量。这些变量有名字和值。因此,在某种意义上说,变量gameCharacter由其它一些变量组成。每个子变量都有一个名字,冒号后面是值。这个包含名字的表达式和花括号之间的值被叫做对象字变量。图3-6显示了对象字变量的的语法图:

(省略图3-6)

在声明和实例化变量gameCharacter之后,这块内存看起来如图3-7:

(省略3-7)

你可以像下面这样获取一个复合变量的值。

gameCharacter.name = "Arjan";var damage = gameCharacter.power * 10;

正如你看到的那样,你可以获取gameCharacter变量中的某个变量,通过在gameCharacter后面加上一个点和子变量名。javascript甚至允许在声明和实例化复合变量后修改这个复合变量的值。举个例子,看下面这段代码:

var anotherGameCharacter = {name : "Arthur",skill : "King",health : 25,power : 35000};anotherGameCharacter.familyName = "Pendragon";

现在anotherGameCharacter有5部分了,name, skill, health, power, familyName。

因为变量也可以指向函数,所以你可以包含一个指向函数的子变量。如下所示:

var anotherGameCharacter = {name : "Arthur",familyName : "Pendragon",skill : "King",health : 25,power : 35000,healMe : function () {anotherGameCharacter.health = 100;}};

像之前一样,你可以在这之后也能为其子变量添加一个函数。

anotherGameCharacter.killMe = function () {anotherGameCharacter.health = 0;};

你可以调用其他变量一样调用这些函数。下面的指令恢复了游戏角色的生命:

anotherGameCharacter.healMe();

如果你想杀死角色,可以调用anotherGameCharacter.killMe();组合变量和函数最棒的地方在于你可以把相关的变量和函数放在一起。这个例子就是把与同一个游戏角色相关的变量放在了一起,同时增加了几个相关的函数。从现在开始,如果一个函数属于一个变量,我把这个函数叫做方法。把一个由其他变量组成的变量叫做对象。如果一个变量是对象的一部分,这个变量叫做成员变量。

你可以想象对象和方法的能量有多强大。它们提供了一个进入复杂游戏世界的方式。如果javascript没有这种能力,那么在程序的开头,你会声明一长串的变量,并且不知道这些变量之间如何相关且能做什么。把变量装进对象里且给对象提供方法,你可以写出更容易理解的程序。在下节,你就要使用这种强大的能力来写一个简单的移动方块程序。

方块移动游戏

这节实现了一个方块在画布上移动的简单程序。主要有两个目的:

  • 关于游戏循环里Update和draw函数的更多细节
  • 如何使用对象来结构化程序

在写这个程序之前,让我们再一次看看BasicGame例子的代码:

var canvas = undefined;var canvasContext = undefined;function start () {canvas = document.getElementById("myCanvas");canvasContext = canvas.getContext("2d");mainLoop();}document.addEventListener('DOMContentLoaded', start);function update () {}function draw () {canvasContext.fillStyle = "blue";canvasContext.fillRect(0, 0, canvas.width, canvas.height);}function mainLoop () {update();draw();window.setTimeout(mainLoop, 1000 / 60);}

我们现在用上面学到的关于对象的知识来把这段代码重新整理成一个游戏程序。如下:

"use strict";var Game = {canvas : undefined,canvasContext : undefined};Game.start = function () {Game.canvas = document.getElementById("myCanvas");Game.canvasContext = Game.canvas.getContext("2d");Game.mainLoop();};document.addEventListener('DOMContentLoaded', Game.start);Game.update = function () {};Game.draw = function () {Game.canvasContext.fillStyle = "blue";Game.canvasContext.fillRect(0, 0, Game.canvas.width, Game.canvas.height);};Game.mainLoop = function () {Game.update();Game.draw();window.setTimeout(mainLoop, 1000 / 60);};

你创建了一个叫做Game的复合变量。这个对象有两个成员变量,canvas和canvasContext。此外,你给这个对象添加了几个方法,包括构成这个游戏循环的方法。你分开定义了这个对象的方法。这样做的原因是你可以清楚的分辨出这些数据和方法组成的对象可以如何与数据打交道。需要注意的是,像我推荐的一样,添加了“use strict”。

现在,让我们对这个程序进行扩展。它需要显示一个在屏幕上移动的方块。你想随着时间改变方块的X坐标值。为了做到这些,你必须把现在的X坐标值储存在变量里。那样你就可以在update里面给变量进行赋值并且使用这个值在draw方法里画出这个方块。放置这个变量的地方是在Game对象里,你可以像下面一样声明和实例化:

var Game = {canvas : undefined,canvasContext : undefined,rectanglePosition : 0};

使用变量rectanglePosition来储存在方块的X坐标值。在draw方法里,你可以使用这个值在屏幕上画出方块。这个例子里,绘画出一个不超过画布大小的方块,下面是draw方法的内容:

Game.draw = function () {Game.canvasContext.fillStyle = "blue";Game.canvasContext.fillRect(Game.rectanglePosition, 100, 50, 50);}

现在你需要做的就是计算X的坐标值。在update方法里计算X值,因为改变X值以为着更新着游戏世界。在这个例子里,我们基于时间的流逝来改变方块的坐标值。在javascript获取系统的时间值:

var d = new Date();var currentSystemTime = d.getTime();

在此之前,你还没看过像第一行那样的符号。现在,假设new Date()创造了一个复合变量(对象),里面有跟时间相关的信息,还有一些有用的方法。其中一个方法是getTime。你通过对象d调用此方法并储存在currentSystemTime里。现在这个变量就有了从1970年1月1日开始的时间。你可以想象到这个变量值有点大。如果你想设置X坐标值,那么就要一个很大的屏幕。当然,不可能这样做,你用时间对画布的宽度求余,余数作为X坐标值。那样,你始终得到的是一个在0到画布宽度之间的值。update方法如下:

Game.update = function () {var d = new Date();Game.rectanglePosition = d.getTime() % Game.canvas.width;};

如你所知,update和draw方法顺序调用,大约每秒60帧。每一次时间改变,系统时间也改变,意味着方块的坐标值也改变,那么方块显示的地方就与之前不一样。

在这个例子运行的如你所想之前你还要做一件事情。如果你像这样运行程序,一个蓝色的条将出现在屏幕上。因为你在旧的方块上绘画了新的方块。为了解决这个问题,每次画方块之前你需要清除画布。清除画布用clearRect方法。这个方法清除掉指定大小的画布。举例:

Game.canvasContext.clearRect(0, 0, Game.canvas.width, Game.canvas.height);

为了方便,把这条指令放在一个叫做clearCanvas的方法里,如下:

Game.clearCanvas = function () {Game.canvasContext.clearRect(0, 0, Game.canvas.width, Game.canvas.height);};

你需要做的就是在游戏循环里,在update和draw方法之前调用上面的方法。

Game.mainLoop = function() {Game.clearCanvas();Game.update();Game.draw();window.setTimeout(Game.mainLoop, 1000 / 60);};

这个例子就完成了。运行结果如图3-8所示:

(省略图3-8)

变量的范围

你声明变量的地方决定了你能在哪些地方使用这些变量。看上面程序的d变量。它声明在update方法里,所以它只能在update方法里面使用。就是说,你不能在draw方法里面使用这个变量。当然,你可以在draw方法里面重新申请一个d变量,但是需要意识到的是这里的d变量和update方法里面的d变量是不一样的。

相对的,你在一个对象的水平上声明一个变量,你就可以在任何地方使用这个变量。你需要在update和draw方法里面使用方块的X坐标,因为在update里面需要更新坐标值,draw里面需要绘画出方块。因此需要在对象这个水平上声明变量,那样对象的所有方法都可以使用这个变量。

变量可以在哪里使用叫做变量范围。在这个例子里面,d的变量范围是update方法,Game.recentPostion是全局范围。

你学到了什么

在这章里,你学到了:

  • 怎样使用变量在内存里储存信息
  • 怎样创建含有变量和方法的对象
  • 怎样通过变量和update方法改变游戏世界和draw方法在屏幕上显示游戏世界

转载于:https://my.oschina.net/u/1587304/blog/399862

你可能感兴趣的文章
AlertDialog对话框
查看>>
我的友情链接
查看>>
linux安全---cacti+ntop监控
查看>>
鸟哥的linux私房菜-shell简单学习-1
查看>>
nagios配置监控的一些思路和工作流程
查看>>
通讯组基本管理任务三
查看>>
Centos下基于Hadoop安装Spark(分布式)
查看>>
3D地图的定时高亮和点击事件(基于echarts)
查看>>
mysql开启binlog
查看>>
设置Eclipse编码方式
查看>>
分布式系统唯一ID生成方案汇总【转】
查看>>
并查集hdu1232
查看>>
Mysql 监视工具
查看>>
从前后端分离到GraphQL,携程如何用Node实现?\n
查看>>
Linux Namespace系列(09):利用Namespace创建一个简单可用的容器
查看>>
nginc+memcache
查看>>
linux下crontab实现定时服务详解
查看>>
Numpy中的random模块中的seed方法的作用
查看>>
关于jsb中js与c++的相互调用
查看>>
UVA 122 Trees on the level 二叉树 广搜
查看>>