游戏开发中的数学、向量的应用

数学中的坐标系统(笛卡尔坐标系统)

Figure1.1

这里A(0,0), B(1,2), C(4,3), D(–1,2), E(–2,–1), F(3,–2).

计算机中的坐标系统(屏幕坐标系统)

Figure1.2

注意这里的y轴在两个坐标系统是相反的。在后面将会研究将笛卡尔坐标系统转换为屏幕坐标系统。

在3D的坐标系中,没有一个工业标准,有的采用y轴向上(y-up)的坐标系,有的采用z轴向上(z-up)的坐标系,有的采用右手坐标系统(right-handed coordinate system),有的采用左手系统(left-handed coordinate system)。

这里我们采用y轴向上的右手坐标系统。

Figure1.3

这里有一个p点的小例子:
Figure1.4

在传统过的数学方法表示中通常是括号加注的方式,如P(1,2,3),但是对于程序员来说,他们更加喜欢这样的表示方式,如p<1,2,3>或者p[1,2,3],因为这样表示可以进行矢量运算,矢量运算后面将会介绍。

点定义:

?
1
2
3
4
5
6
7
8
9
10
function Point2D(x,y){
     this .x=x;
     this .y = y;
}
 
function Point3D(x,y,z){
     this .x=x;
     this .y = y;
     this .z = z;
}

两点间的距离
在人工智能程序中,智能体可以通过判断敌人与自己的距离来决定发动攻击的时机。

公式:a2+b2=c2(勾股定理)

Figure2.2

2D场景中的距离公式:
设点P1(x1, y1)和P2(x2, y2)分别为线上的点,他们的距离d的计算方法如下:
201105121203069961

计算机实现:

?
1
2
3
4
5
6
7
function distance2D(p1,p2)
{
     var dx=p1.x-p2.x;
     var dy=p1.y-p2.y;
     var distance=Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2));
     return distance;
}

3D场景中的距离公式:
201105121203061107

 

计算机实现:

?
1
2
3
4
5
6
7
8
function distance3D(p1,p2)
{
     var dx=p1.x-p2.x;
     var dy=p1.y-p2.y;
     var dz=p1.z-p2.z;
     var distance=Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2)+Math.pow(dz,2));
     return distance;
}

线

直线公式:
Ax + By = C (A和B不能同时为0)
讨论:
A和B不为0:为斜线(不垂直于x轴和y轴);
A为0,B不为0:直线为垂直于y轴的直线,且于y轴交与点(0,C/B);
A不为0,B为0:直线为垂直于x轴的直线,且于x轴交与点(C/A,0);
例:
Figure1.6

Figure1.5
斜率:
Figure1.7

定义斜率:

?
1
2
3
4
5
6
7
8
9
function getSlope(p1,  p2)
{
     var slope;
     if (p1.x-p2.x!=0)
     {
          slope=(p1.y-p2.y)/(p1.x-p2.x);
          return slope;
     }
}

已知:直线A:y1=k1*x1+b1 直线B:y2=k2*x2+b2
两直线相互垂直的条件是:斜率乘积为-1,即k1*k2=-1。
两直线平行的条件是:斜率相等,即k1=k2;当k1=k2且b1=b2直线重合。
两直线相交的条件是:斜率不相等,即k1!=k2;两直线垂直是直线相交的一种特殊情况。

抛物线

如果你投掷一个铅球或者踢足球,它们将做一个抛物线运动。因为它们总是受一个垂直于地面的重力加速度g的作用。下图为四个方向上的抛物线。

Figure2.7

公式:
垂直轴向上的抛物线:y=a(x–h)2+k, 顶点为(h,k),关于x=h对称.
水平轴向上的抛物线:x=a(y–k)2+h, 顶点为(h,k),关于y=k对称.

举例:
Figure2.9

圆的两个关键因素:圆心和半径。

公式:
(x–h)2 + (y–k)2 = r2  圆心为(h,k)  半径为r.
举例:圆心为(1,2),半径为2的圆。
Figure2.10

计算机中的实现:

?
1
2
3
4
5
function Circle(point,r)
{
    this .center = point ;
    this .radius = r;
}

圆的碰撞条件:
当两个圆相切或者相交时,我们认为两个圆应该开始发生碰撞。

Figure2.11
数学表示:
Figure2.12

计算机中的实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fuction collisionBetweenCircles(circle1,circle2)
{
    var hit;
    var  powerOfDistanceOfCenter=Math.pow(circle1.center.x-circle2.center.x)+ Math.pow((circle1.center.y-circle2.center.y),2));
    var powerOfDistanceOfRadius=Math.pow(circle1.radius+circle2.radius,2);
     if (powerOfDistanceOfCenter<=powerOfDistanceOfRadius)
     {
         hit= true ;
     }
     else
     {
         hit= false ;
     }
     return hit;
}

弧度

数学表示:
角度:degree=radian*180/π
弧度:radian=degree*π/180
π≈3.141592654

计算机实现

?
1
2
var RadToDeg=57.29577951,
     DegToRad=0.017453293f;

矢量

矢量既有大小又有方向。

常用举例:

物理上常用的矢量与标量的对应概念有:速度和速率;距离和位移等。如图:

Figure4.1

距离:
假设A到B的距离为a,B到C的距离为b,C到D的距离为c;
那么A到D的距离就为a+b+c。

位移:
假设A到B的位移为a,B到C的位移为b,C到D的位移为c;
那么A到D的位移大小为|a+b+c|(数学上也叫a+b+c的模),方向是从A指向D。

计算机中矢量的表示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//2D Vector
function Vector2D(x,y)
{
    this .x = x;
    this .y = y;
}
 
//3D Vector
function Vector3D(x,y,z)
{
    this .x = x;
    this .y = y;
    this .z = z;
}

1矢量加法

设A=a1i+a2j , B=b1i+b2j那么A+B=(a1+b1)i+(a2+b2)j

Figure4.11

2.矢量的减法,我们可以把A-B看成A+(-B)。

设A=a1i+a2j , B=b1i+b2j那么A-B=(a1-b1)i+(a2-b2)j

Figure4.13

3.点积
A·B=|A| |B| cosq
2D:定义矢量A[a1,a2],矢量B[b1,b2] ;那么点积A·B=a1b1+a2b2
3D:定义矢量A[a1,a2,a3],矢量B[b1,b2,b3] ;那么点积A·B=a1b1+a2b2+a3b3

性质:

如果A·B = 0, 那么A┴B。
A·B = B· A。
如果 A·B < 0 (负), 那么q > 90°
如果 A·B > 0 (正), 那么q < 90°

Figure4.16
C · D=5(6) + 3(–2) = 30 – 6 = 24

计算机实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
//向量加法
function addVector(v1,v2){
   return new Vector(v1.x+v2.x,v1.y+v2.y);
}
//向量减法
function subVector(v1,v2){
   return new Vector(v1.x-v2.x,v1.y-v2.y);
}
//点积
function dotProduct(v1,v2){
    return v1.x*v2.x+v1.y*v2.y;
}

绝大多数程序员认为编程是远比数学有意思得多,但是掌握一些数学基本知识可以帮助你解决很多的问题。其中,向量知识是一个“超值”的数学主题。

向量通常被描述为一个既有长度,又有方向的量。如很多物理量一般都是用向量来表示的:动量、速度、加速度、位移······

在以下内容中,我们将沿用CSS/位图的屏幕坐标系统,即左上角为原点、x轴沿右侧增加、y轴沿下方增加(亦称做笛卡尔坐标系)。

向量运算

我们可以把一些有用的运算应用于向量,下面列出了一些简单的向量运算以及可能的应用程序。

加法和减法

我们可以通过加减向量的x,y坐标来实现向量的相加或相减。这就和常规的计算是一样的,所以,一个向量和自己相加可以使它的长度扩大两倍,一个向量减掉它自己会得到零向量。一些简单的例子:

  • 给飞行的皮球加上一个重力向量,让它真实地下落
  • 把两个碰撞物体的向量加在一起,计算逼真的碰撞反馈
  • 给宇宙飞船增加一个火箭推力的向量,让飞船移动

缩放

按照一定比例缩放向量的x、y坐标,你就可以缩小或放大向量的长度。一些实例如下:

  • 反复以略小于1的向量缩放移动向量,让对应物体缓慢地停下来
  • 将大炮的方向向量扩大,作为一个发射炮弹的初始向量

标准化

有时需要将一个向量变为单位长度,或者说将向量长度变为一个单位。这个过程叫做标准化,单位长度的向量叫做单位向量。当我们感兴趣的是向量的方向而不是长度时,我们经常会这么做。单位向量可以表示:

  • 定向喷射的方向
  • 斜坡的倾斜方向
  • 大炮的发射方向

一旦有了单位向量,我们可以将其缩放,使其代表喷射推力或者炮弹的初始化速度。

旋转

以任意角度来旋转一个向量是非常有用的,因为这可以让你指向任意一个你想要的方向。实例包括:

  • 使一个对象始终指向另一个
  • 更改一个虚拟的喷射引擎的推力方向
  • 根据发射器的方向改变投射体的初始发射方向

在JavaScript中,角是以弧度)(radian)为单位,而不是我们熟悉的角度(degree)指向的。因为弧度看起来不是很直观,我们可以利用JavaScript中的函数很容易将弧度和角度进行转化。

?
1
2
3
4
5
6
7
8
//角度转为弧度
degToRad = function (deg){
     return deg * (Math.PI/180);
};
//弧度转为角度
radToDeg = function (rad){
     return rad * (180/Math.PI);
};

向量的点乘

单位向量的点乘给出两个向量之间角度的余弦,或者说单位向量点乘告诉我们两个向量的方向相近程度。点乘的结果在-1到1之间变化。这是一些特殊点乘含义的例子:

  • 向量的方向相同:点积 = 1
  • 向量的夹角为45o:点积 = 0.5
  • 向量的夹角为90o:点积 = 0
  • 向量的夹角为180o:点积 = -1

点乘可以让我们知道两个物体面向对方的程度。比如:在游戏中,我们可以根据点乘判断两个角色是否可以”看到“对方,或者某形状的某一边是否指向某个方向。

创建一个JavaScript向量对角

为了在JavaScript中充分利用向量,我们可以把前面描述的函数总结成一个可以利用的向量对角,这样在程序中使用向量就会非常方便。需要时我们可以加入更多与向量相关的功能。

向量的x和y分量在对象中被称做为vxvy。这使得我们以后在处理一些含有其他x、y变量的代码时,能够清楚地区分开:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var vector2d = function (x ,y){
        var vec = {
            //把x、y保存在对象的vx、vy中
            vx: x,
            vy: y,
            //scale方法可以让我们来放大或缩小向量
            scale: function (scale){
                vec.vx *= scale;
                vec.vy *= scale;
            },
            //向量的加法运算
            add: function (vec2){
                vec.vx += vec2.vx;
                vec.vy += vec2.vy;
            },
            //向量的减法运算
            sub: function (vec2){
                vec.vx -= vec2.vx;
                vec.vy -= vec2.vy;
            },
            //方向取反
            negate: function (){
                vec.vx = -vec.vx;
                vec.vy = -vec.vy;
            },
            //获取向量长度
            length: function (){
                return Math.sqrt(vec.vx * vec.vx + vec.vy * vec.vy);
            },
            //获取向量长度的平方
            lengthSquared: function (){
                return vec.vx * vec.vx + vec.vy * vec.vy;
            },
            //将向量转化为一个单位向量
            normalize: function (){
                var len = Math.sqrt(vec.vx * vec.vx + vec.vy * vec.vy);
                if (len){
                    vec.vx /=len;
                    vec.vy /=len;
                }
                //把向量的长度返回
                return len;
            },
            //向量的旋转
            rotate: function (angle){
                var vx = vec.vx,
                        vy = vec.vy,
                        cosVal = Math.cos(angle),
                        sinVal = Math.sin(angle);
                vec.vx = vx * cosVal - vy * sinVal;
                vec.vy = vx * sinVal + vy * cosVal;
            },
            //toString方法可以把向量以文本形式输出,方便程序调试
            toString: function (){
                return '(' + vec.vx.toFixed(3) + ',' + vec.vy.toFixed(3) + ')' ;
            }
        };
        return vec;
    };

使用向量的大炮模拟

我们已经定义了向量对角,可以利用它创建一个简单的大炮模拟。首先,我们应该限定一下”模拟“一词的概念:我们的目标不是尝试绝对真实地复制大炮,而是要创建一个足够现实的模拟,用于程序如游戏。即使游戏中最高级的物理模拟也得在一定程度上去掉一些现实。例如:游戏中的人物角色不模拟直立行走的物理过程,游戏中的飞机也不模拟保持悬浮的物理过程。

QQ截图20131226210004

严格地说,为了达到精确的模拟效果,你应该在计算中考虑每一帧画面的经过时间。不过在这个演示中,我们假设了一个30毫秒的帧速率。实际上,某些浏览器上的计时器反正不是特别准确,所以缺乏时间的计算是没有多大损失的(相关解决方法你可以参见:游戏开发中的定时器、速度和帧速率相关问题探讨)。
这个模拟使用HTML5 的画布来绘制图形,你也可以加入浏览器中其它的渲染(SVG、CSS3等)。为了使代码集中于对向量的使用和相关计算,这里仅使用了基本的图形。

大炮模拟中使用向量来表示:

  • 表示大炮的瞄准方向
  • 表示炮弹的运动(最初来源于大炮的瞄准方向)

模拟范围的变量

在主模拟函数的顶部我们定义少量的模拟范围的变量。尽管这些变量在模拟的所有函数中都可用,但是它们被封装在主模拟函数中而并不是全局变量。

?
1
2
3
var gameObjects = [],                             //用来保存游戏对象的数组
         canvas = document.getElementById( 'canvas' ),   //canvas的引用
         ctx = canvas.getContext( '2d' );

我们将模拟中的每一个对象(除了背景外)都加入gameObjects[]数组。然后模拟的主循环遍历整个数组来实现移动和绘制所有对象。

炮弹

我们初始化炮弹时需要传入初始x和y位置以及移动向量。在每一个周期中,我们将向量添加到当前位置,并且给移动向量的y分量加gravity,使炮弹在向前运动的同时向下坠落。在每一个周期内,我们给gravity增加一个固定的量,以模拟重力加速效果。炮弹用一个实心圆来表示。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var cannonBall = function (x ,y , vectory){
         var gravity = 0,
                 that = {
                     x: x,                           //初始时X坐标
                     y: y,                           //初始时Y坐标
                     removeMe: false ,                //移除标识
                     //move方法用来更新炮弹位置和检测炮弹是否落地
                     move: function (){
                         vectory.vy += gravity;
                         gravity += 0.1;
                         that.x += vectory.vx;
                         that.y += vectory.vy;
                         if (that.y > canvas.height -150){
                             that.removeMe = true ;
                         }
                     },
                     draw: function (){
                         ctx.beginPath();
                         ctx.arc(that.x, that.y, 5, 0, Math.PI * 2, true );
                         ctx.fill();
                         ctx.closePath();
                     }
                 };
         return that;
     };

大炮

大炮用安装在车轮上的一个简单的矩形炮管来表示,它的轴始终对准鼠标指针。要计算对鼠标指针的角度,我们使用Math.atan2(y,x)函数。Math.atan2(y,x)函数返回水平轴与点(x , y)之间的弧度。假设水平轴通过大炮的支点,输入应该是鼠标指针位置相对于大炮的支点位置:

?
1
angle = Math.atan2(mouseY - cannonY, mouseX - cannonX);

当你点击鼠标的时候,大炮会发射一颗炮弹。炮弹会有一个初始位置(大炮的支点)和一个移动向量。我们通过鼠标指针的位置相对于大炮的位置来计算移动的向量。

?
1
vector = vector2d(mouseX - cannonX, mouseY - cannonY);

尽管这个向量的方向是正确的,但是它的长度是炮弹到鼠标指针的距离。这是没有用处的,因为这个距离是变化的;不能被简单地放大或者缩小一个固定的量。解决办法是将其标准化为一个单位向量,然后缩放到想要的长度。

?
1
2
vec.normalize();    //标准化为单位向量
vec.scale(25);      //放大到25个单位长度

下面是一个完整的大炮对象:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var cannon = function (x , y){
         var mx = 0,
                 my = 0,
                 angle = 0,
                 that = {
                     x: x,
                     y: y,
                     angle: 0,
                     removeMe: false ,
                     //move方法计算大炮到鼠标的角度
                     move: function (){
                         //计算到鼠标的角度
                         angle = Math.atan2(my - that.y , mx - that.x);
                     },
                     draw: function (){
                         ctx.save();
                         ctx.lineWidth = 2;
                         //原点移动到炮管的中下方
                         ctx.translate(that.x , that.y);
  
                         //按照move方法中计算的角度进行旋转
                         ctx.rotate(angle);
                         //画一个矩形的炮管
                         ctx.strokeRect(0, -5, 50 ,10);
  
                         //在大炮的底部画一个轮子
                         ctx.moveTo(0, 0);
                         ctx.beginPath();
                         ctx.arc(0, 0, 15, 0, Math.PI * 2, true );
                         ctx.fill();
                         ctx.closePath();
                         ctx.restore();
                     }
                 };
         //当鼠标点击时,发射炮弹
         canvas.onmousedown = function (event){
             //创建一个方向从大炮指向鼠标的向量
             var vec = vector2d(mx - that.x, my - that.y);
             vec.normalize();                //转化为单位向量
             vec.scale(25);                  //把向量放大25倍
             //创建一个子弹,并添加到gameObjects数组中
             gameObjects.push(cannonBall(that.x, that.y, vec));
         };
         //实时获取鼠标在画布上的位置
         canvas.onmousemove = function (event){
             var bb = canvas.getBoundingClientRect();
             mx = (event.clientX - bb.left);
             my = (event.clientY - bb.top);
         };
         return that;
     }

背景

眼光锐利的人很可能已经注意到,炮弹在空中飞的时候有一个尾巴。我们能获得这个效果是因为在蓝天绿地背景上有趣地使用了画布的globalAlpha属性。通常情况下,当我们用画布处理动画时,我们每一帧都需要重绘整个画面来擦除前一帧的图像。如果我们不这样做,画布上会留下之前的痕迹,所有移动图像都会有拖尾。通过指定背景的alpha值,我们只是在一定程度上清除了前一帧。由于这些半透明背景是一层层叠加的,它们最终完全清除了前一帧图像。背景的alpha值越小,尾巴消失所用的时间就越长。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//在画布是绘制蓝天和草地
     var drawSkyAndGrass = function (){
         ctx.save();
         //设定透明度
         ctx.globalAlpha = 0.4;
         //创建一个画布渐变的对象
         var linGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
         //顶部从天空蓝开始
         linGrad.addColorStop(0, '#00BFFF' );
         //中间为白色
         linGrad.addColorStop(0.5, 'white' );
         //草地绿色
         linGrad.addColorStop(0.5, '#55dd00' );
         //底部过度到白色
         linGrad.addColorStop(1, 'white' );
  
         ctx.fillStyle = linGrad;
         ctx.fillRect(0, 0, canvas.width, canvas.height);
         ctx.restore();
     };

主循环

我们将主循环封闭在setInterval()的一个函数中。主循环每30毫秒被处理一次,它调用了模拟对象中的move()函数和draw()函数,同时还创建一个没有设置removeMe标志的对象列表。所有设置removeMe标志的物体将不再被包括在新列表中,因此就从模拟对象中消失了。炮弹到地平线以下后将用此方法被移除。

页面的布局

这里是大炮模拟的最终页面布局,注意,为了避免重复,这里精简了一些函数代码。只需要用把前面的代码填进来代替就可以了。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!doctype html>
< html lang = "zh-cn" >
< head >
     < meta charset = "UTF-8" >
     < title >向量的应用实例</ title >
     < script type = "text/javascript" >
         window.onload = function(){
             var gameObjects = [],
                     canvas = document.getElementById('canvas'),
                     ctx = canvas.getContext('2d');
             var vector2d = function(x, y){
                 /***向量部分代码***/
             };
  
             var cannonBall = function(x, y, vector){
                 /***代码被精简,请见文章前面部分***/
             };
  
             var cannon = function(x, y){
                 /***代码被精简,请见文章前面部分***/
             };
  
             var drawSkyAndGrass = function(){
                 /***代码被精简,请见文章前面部分***/
             };
  
             //添加一个初始的大炮到游戏对象列表中
             gameObjects.push(cannon(50, canvas.height-150));
  
             //下面是移动和绘制的主循环
             setInterval(function(){
                 drawSkyAndGrass();
  
                 gameObjectsFresh = [];
                 for(var i=0; i< gameObjects.length ; i++){
                     gameObjects[i].move();
                     gameObjects[i].draw();
                     if(gameObjects[i].removeMe === false){
                         gameObjectsFresh.push(gameObjects[i]);
                     }
                 }
                 gameObjects = gameObjectsFresh ;
             },30);
         };
     </script>
</ head >
< body >
     < canvas id = "canvas" width = "640" height = "480" style = "border: 1px solid #ccc" >
         您的浏览器不支持canvas绘图,请更新浏览器。
     </ canvas >
</ body >
</ html >

本文内容来自《JavaScript高效图形编程》。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值