金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 运动学模拟与粒子系统,游戏开发基础的教程

运动学模拟与粒子系统,游戏开发基础的教程

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-10-07 22:19

HTML5 游戏开发基础的教程

2017/03/24 · HTML5 · 2 评论 · 游戏

本文由 伯乐在线 - 紫洋 翻译,艾凌风 校稿。未经许可,禁止转载!
英文出处:Mikołaj Stolarski & Tomasz Grajewski。欢迎加入翻译组。

在游戏的视觉效果定义其整体外观、感觉和游戏玩法本身。玩家被好的视觉体验所吸引,从而可达到产生更多的流量。这是创建成功的游戏和为玩家提供很多乐趣的关键。

在这篇文章中,我们基于 HTML5 游戏的不同视觉效果实现,提出几个构思方案。这些示例将依据我们自己的游戏《Skytte 》所实现的效果。我会解释支持他们的基本思想, ,并提供应用于我们项目中的效果。

系列简介
也许,三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过,物理学广泛地应用在今天许多游戏、动画中。为什么在这些应用中要使用物理学?笔者认为,自我们出生以来,一直感受着物理世界的规律,意识到物体在这世界是如何"正常移动",例如射球时球为抛物线(自旋的球可能会做成弧线球) 、石子系在一根线的末端会以固定频率摆动等等。要让游戏或动画中的物体有真实感,其移动方式就要符合我们对"正常移动"的预期。
今天的游戏动画应用了多种物理模拟技术,例如运动学模拟(kinematics simulation)、刚体动力学模拟(rigid body dynamics simulation)、绳子/布料模拟(string/cloth simulation)、柔体动力学模拟(soft body dynamics simulation)、流体动力学模拟(fluid dynamics simulation)等等。另外碰撞侦测(collision detection)是许多模拟系统里所需的。
本系列希望能介绍一些这方面最基础的知识,继续使用JavaScript做例子,以即时互动方式体验。
本文简介 作为系列第一篇,本文介绍最简单的运动学模拟,只有两条非常简单的公式。运动学模拟可以用来模拟很多物体运动(例如马里奥的跳跃、炮弹等),本文将会配合粒子系统做出一些视觉特效(粒子系统其实也可以用来做游戏的玩法,而不单是视觉特效)。
运动学模拟
运动学(kinematics)研究物体的移动,和动力学(dynamics)不同之处,在于运动学不考虑物体的质量(mass)/转动惯量(moment of inertia),以及不考虑加之于物体的力(force )和力矩(torque)。
我们先回忆牛顿第一运动定律:
当物体不受外力作用,或所受合力为零时,原先静止者恒静止,原先运动者恒沿着直线作等速度运动。该定律又称为「惯性定律」。此定律指出,每个物体除了其位置(position)外,还有一个线性速度(linear velocity)的状态。然而,只模拟不受力影响的物体并不有趣。撇开力的概念,我们可以用线性加速度(linear acceleration)去影响物体的运动。例如,要计算一个自由落体在任意时间t的y轴座标,可以使用以下的分析解(analytical solution):
金沙棋牌官方平台 1
当中,和分别是t=0时的y轴起始座标和速度,而g则是重力加速度(gravitational acceleration)。
这分析解虽然简单,但是有一些缺点,例如g是常数,在模拟过程中不能改变;另外,当物体遇到障碍物,产生碰撞时,这公式也很难处理这种不连续性(discontinuity) 。
在计算机模拟中,通常需要计算连续的物体状态。用游戏的用语,就是计算第一帧的状态、第二帧的状态等等。设物体在任意时间t的状态:位置矢量为、速度矢量为、加速度矢量为。我们希望从时间的状态,计算下一个模拟时间的状态。最简单的方法,是采用欧拉方法(Euler method)作数值积分(numerical integration):
金沙棋牌官方平台 2
欧拉方法非常简单,但有准确度和稳定性问题,本文会先忽略这些问题。本文的例子采用二维空间,我们先实现一个JavaScript二维矢量类:

系列简介
也许,三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过,物理学广泛地应用在今天许多游戏、动画中。为什么在这些应用中要使用物理学?笔者认为,自我们出生以来,一直感受着物理世界的规律,意识到物体在这世界是如何"正常移动",例如射球时球为抛物线(自旋的球可能会做成弧线球) 、石子系在一根线的末端会以固定频率摆动等等。要让游戏或动画中的物体有真实感,其移动方式就要符合我们对"正常移动"的预期。
今天的游戏动画应用了多种物理模拟技术,例如运动学模拟(kinematics simulation)、刚体动力学模拟(rigid body dynamics simulation)、绳子/布料模拟(string/cloth simulation)、柔体动力学模拟(soft body dynamics simulation)、流体动力学模拟(fluid dynamics simulation)等等。另外碰撞侦测(collision detection)是许多模拟系统里所需的。
本系列希望能介绍一些这方面最基础的知识,继续使用JavaScript做例子,以即时互动方式体验。
本文简介 作为系列第一篇,本文介绍最简单的运动学模拟,只有两条非常简单的公式。运动学模拟可以用来模拟很多物体运动(例如马里奥的跳跃、炮弹等),本文将会配合粒子系统做出一些视觉特效(粒子系统其实也可以用来做游戏的玩法,而不单是视觉特效)。
运动学模拟
运动学(kinematics)研究物体的移动,和动力学(dynamics)不同之处,在于运动学不考虑物体的质量(mass)/转动惯量(moment of inertia),以及不考虑加之于物体的力(force )和力矩(torque)。
我们先回忆牛顿第一运动定律:
当物体不受外力作用,或所受合力为零时,原先静止者恒静止,原先运动者恒沿着直线作等速度运动。该定律又称为「惯性定律」。此定律指出,每个物体除了其位置(position)外,还有一个线性速度(linear velocity)的状态。然而,只模拟不受力影响的物体并不有趣。撇开力的概念,我们可以用线性加速度(linear acceleration)去影响物体的运动。例如,要计算一个自由落体在任意时间t的y轴座标,可以使用以下的分析解(analytical solution):
金沙棋牌官方平台 3
当中,和分别是t=0时的y轴起始座标和速度,而g则是重力加速度(gravitational acceleration)。
这分析解虽然简单,但是有一些缺点,例如g是常数,在模拟过程中不能改变;另外,当物体遇到障碍物,产生碰撞时,这公式也很难处理这种不连续性(discontinuity) 。
在计算机模拟中,通常需要计算连续的物体状态。用游戏的用语,就是计算第一帧的状态、第二帧的状态等等。设物体在任意时间t的状态:位置矢量为、速度矢量为、加速度矢量为。我们希望从时间的状态,计算下一个模拟时间的状态。最简单的方法,是采用欧拉方法(Euler method)作数值积分(numerical integration):
金沙棋牌官方平台 4
欧拉方法非常简单,但有准确度和稳定性问题,本文会先忽略这些问题。本文的例子采用二维空间,我们先实现一个JavaScript二维矢量类:

unity初探之黑暗之光(2)

你会学到什么

在我们开始之前, 我想列出一些我希望你能从本文中学习的知识:

  • 基本的游戏设计
    我们来看看通常用于制造游戏和游戏效果的模式: 游戏循环、精灵、碰撞和粒子系统。
  • 视觉效果的基本实现
    我们还将探讨支持这些模式的理论和一些代码示例。

复制代码 代码如下:

复制代码 代码如下:

一、设置角色跟随鼠标点击移动

思路:使用charactercollider的SimpleMove方法来控制角色的移动。通过摄像机的射线投射到地面,通过屏幕上的一个点也就是鼠标单击的点。该射线与地面发生碰撞返回发生碰撞的点,然后让角色转向该点,开始移动。当移动到一定范围时停止移动。

使用到的方法:

常见的模式

让我们从游戏开发中常用的大一些模式和元素开始

// Vector2.js
Vector2 = function(x, y) { this.x = x; this.y = y; };

// Vector2.js
Vector2 = function(x, y) { this.x = x; this.y = y; };

Camera.ScreenPointToRay 屏幕位置转射线

 

function ScreenPointToRay (position : Vector3) : Ray

Description描述

Returns a ray going from camera through a screen point.

返回一条射线从摄像机通过一个屏幕点。

Resulting ray is in world space, starting on the near plane of the camera and going through position's (x,y) pixel coordinates on the screen (position.z is ignored).

产生的射线是在世界空间中,从相机的近裁剪面开始并穿过屏幕position(x,y)像素坐标(position.z被忽略)。

Screenspace is defined in pixels. The bottom-left of the screen is (0,0); the right-top is (pixelWidth,pixelHeight).

屏幕空间以像素定义。屏幕的左下为(0,0);右上是(pixelWidth,pixelHeight)。

 

精灵

这些只是在游戏中代表一个对象的二维图像。精灵可以用于静态对象, 也可以用于动画对象, 当每个精灵代表一个帧序列动画。它们也可用于制作用户界面元素。

通常游戏包含从几十到几百精灵图片。为了减少内存的使用和处理这些映像所需的能力, 许多游戏使用精灵表。

Vector2.prototype = {
copy : function() { return new Vector2(this.x, this.y); },
length : function() { return Math.sqrt(this.x * this.x + this.y * this.y); },
sqrLength : function() { return this.x * this.x + this.y * this.y; },
normalize : function() { var inv = 1/this.length(); return new Vector2(this.x * inv, this.y * inv); },
negate : function() { return new Vector2(-this.x, -this.y); },
add : function(v) { return new Vector2(this.x + v.x, this.y + v.y); },
subtract : function(v) { return new Vector2(this.x - v.x, this.y - v.y); },
multiply : function(f) { return new Vector2(this.x * f, this.y * f); },
divide : function(f) { var invf = 1/f; return new Vector2(this.x * invf, this.y * invf); },
dot : function(v) { return this.x * v.x + this.y * v.y; }
};

Vector2.prototype = {
copy : function() { return new Vector2(this.x, this.y); },
length : function() { return Math.sqrt(this.x *金沙棋牌官方平台, this.x + this.y * this.y); },
sqrLength : function() { return this.x * this.x + this.y * this.y; },
normalize : function() { var inv = 1/this.length(); return new Vector2(this.x * inv, this.y * inv); },
negate : function() { return new Vector2(-this.x, -this.y); },
add : function(v) { return new Vector2(this.x + v.x, this.y + v.y); },
subtract : function(v) { return new Vector2(this.x - v.x, this.y - v.y); },
multiply : function(f) { return new Vector2(this.x * f, this.y * f); },
divide : function(f) { var invf = 1/f; return new Vector2(this.x * invf, this.y * invf); },
dot : function(v) { return this.x * v.x + this.y * v.y; }
};

Physics.Raycast 光线投射

 

static function Raycast (origin : Vector3, direction : Vector3, distance : float = Mathf.Infinity, layerMask : int = kDefaultRaycastLayers) : bool

精灵表

这些都用来在一个图像中合成一套单个精灵。这减少了在游戏中文件的数量,从而减少内存和处理电源使用。精灵表包含许多单精灵堆积彼此相邻的行和列,和类似精灵的图像文件,它们包含可用于静态或动画。

金沙棋牌官方平台 5

精灵表例子。(图像来源: Kriplozoik)

下面是Code + Web的文章, 帮助您更好地理解使用精灵表的益处。

Vector2.zero = new Vector2(0, 0);

Vector2.zero = new Vector2(0, 0);

Parameters参数

  • origin

    The starting point of the ray in world coordinates.
    在世界坐标,射线的起始点。

  • direction

    The direction of the ray.
    射线的方向。

  • distance

    The length of the ray
    射线的长度。

  • layerMask

    A Layer mask that is used to selectively ignore colliders when casting a ray.
    只选定Layermask层内的碰撞器,其它层内碰撞器忽略。

• static function Raycast (origin : Vector3, direction : Vector3, out hitInfo : RaycastHit, distance : float = Mathf.Infinity, layerMask : int = kDefaultRaycastLayers) : bool

游戏循环

重要的是要认识到游戏对象并不真正在屏幕上移动。运动的假象是通过渲染一个游戏世界的屏幕快照, 随着游戏的时间的一点点推进 (通常是1/60 秒), 然后再渲染的东西。这实际上是一个停止和运动的效果, 并常在二维和三 维游戏中使用。游戏循环是一种实现此停止运动的机制。它是运行游戏所需的主要组件。它连续运行, 执行各种任务。在每个迭代中, 它处理用户输入, 移动实体, 检查碰撞, 并渲染游戏 (推荐按这个顺序)。它还控制了帧之间的游戏时间。

下面示例是用JavaScriptpgpg语言写的非常基本的游戏循环︰

JavaScript

var lastUpdate; function tick() { var now = window.Date.now(); if (lastUpdate) { var elapsed = (now-lastUpdate) / 1000; lastUpdate = now; // Update all game objects here. update(elapsed); // ...and render them somehow. render(); } else { // Skip first frame, so elapsed is not 0. lastUpdate = now; } // This makes the `tick` function run 60 frames per second (or slower, depends on monitor's refresh rate). window.requestAnimationFrame(tick); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var lastUpdate;
 
function tick() {
  var now = window.Date.now();
 
  if (lastUpdate) {
    var elapsed = (now-lastUpdate) / 1000;
    lastUpdate = now;
 
    // Update all game objects here.
    update(elapsed);
    // ...and render them somehow.
    render();
  } else {
    // Skip first frame, so elapsed is not 0.
    lastUpdate = now;
  }
 
  // This makes the `tick` function run 60 frames per second (or slower, depends on monitor's refresh rate).
  window.requestAnimationFrame(tick);
};

请注意,上面的例子中是非常简单。它使用可变时间增量 (已用的变量),并建议升级此代码以使用固定的增量时间。有关详细信息, 请参阅本文。

然后,就可以用HTML5 Canvas去描绘模拟的过程:

然后,就可以用HTML5 Canvas去描绘模拟的过程:

Parameters参数

  • origin

    The starting point of the ray in world coordinates.
    在世界坐标,射线的起始点。

  • direction

    The direction of the ray.
    射线的方向。

  • distance

    The length of the ray
    射线的长度。

  • hitInfo

    If true is returned, hitInfo will contain more information about where the collider was hit (See Also: RaycastHit).
    如果返回true,hitInfo将包含碰到器碰撞的更多信息。

  • layerMask

    A Layer mask that is used to selectively ignore colliders when casting a ray.
    只选定Layermask层内的碰撞器,其它层内碰撞器忽略。 

 

    

碰撞检测

碰撞检测是指发现物体之间的交点。这对于许多游戏是必不可少的, 因为它用来检测玩家击中墙壁或子弹击中敌人, 诸如此类等等。当检测到碰撞时, 它可以用于游戏逻辑设计中;例如, 当子弹击中玩家时, 健康分数会减少十点。

有很多碰撞检测算法, 因为它是一个性能繁重的操作, 明智的选择最好的方法是很重要的。要了解有关碰撞检测、算法以及如何实现它们的更多信息, 这里有一篇来自MDN 的文章。

复制代码 代码如下:

复制代码 代码如下:

Transform.LookAt 注视

function LookAt (target : Transform, worldUp : Vector3 = Vector3.up) : void

Description描述

Rotates the transform so the forward vector points at /target/'s current position.

旋转物体,这样向前向量指向target的当前位置。简单说,

旋转物体使z轴指向目标物体。

当该物体设置了LookAt并指定了目标物体时,该物体的z轴将始终指向目标物体,在设置了worldUp轴向时,该物体在更接近指定的轴向是旋转便的灵活,注意worldUp指的是世界空间,不论你物体在什么位置,只要接近指定的轴方向,旋转会变的更灵活。


粒子和粒子系统

粒子基本上是用粒子系统的精灵。在游戏开发中一个粒子系统是由粒子发射器和分配给该发射器的粒子组成的一个组成部分。它用来模拟各种特效,像火灾、 爆炸、 烟、 和下雨的影响。随着时间的推移微粒和每个发射器有其自身的参数来定义各种变量,用于模拟的效果,如速度、 颜色、 粒子寿命或持续时间,重力、 摩擦和风速。

var position = new Vector2(10, 200);
var velocity = new Vector2(50, -50);
var acceleration = new Vector2(0, 10);
var dt = 0.1;
function step() {
position = position.add(velocity.multiply(dt));
velocity = velocity.add(acceleration.multiply(dt));
ctx.strokeStyle = "#000000";
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
ctx.arc(position.x, position.y, 5, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
start("kinematicsCancas", step);

var position = new Vector2(10, 200);
var velocity = new Vector2(50, -50);
var acceleration = new Vector2(0, 10);
var dt = 0.1;
function step() {
position = position.add(velocity.multiply(dt));
velocity = velocity.add(acceleration.multiply(dt));
ctx.strokeStyle = "#000000";
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
ctx.arc(position.x, position.y, 5, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
start("kinematicsCancas", step);

二、绕物相机旋转,拉近拉远

 1    public float maxDis = 20f;
 2     public float minDis = 2f;
 3     public int scrollSpeed = 10;
 4     public float distance=0;
 5     public int rotateSpeed = 1;
 6 
 7     private Transform player;
 8     private Vector3 offSet;
 9     private bool isRotate = false;
10     // Use this for initialization
11     void Start () {
12         player = GameObject.FindGameObjectWithTag(Tags.Player).transform;
13         transform.LookAt(player);
14         offSet = transform.position - player.position;//偏位
15     }
16     
17     // Update is called once per frame
18     void Update () {
19         transform.position = player.position + offSet;
20         
21         Rotate();
22         scroll();
23     }
24     //右键控制相机围绕对象旋转
25     void Rotate()
26     {
27         if (Input.GetMouseButtonDown(1))
28         {
29             isRotate = true;
30         }
31         if (Input.GetMouseButtonUp(1))
32         {
33             isRotate = false;
34         }
35 
36         if (isRotate)
37         {
38             Vector3 originalPosion = transform.position;
39             Quaternion originalRotate = transform.rotation;
40 
41             transform.RotateAround(player.position, Vector3.up, rotateSpeed * Input.GetAxis("Mouse X"));
42 
43             print(rotateSpeed * Input.GetAxis("Mouse Y"));
44             transform.RotateAround(player.position, Vector3.right, rotateSpeed * Input.GetAxis("Mouse Y"));
45             float x = transform.eulerAngles.x;
46             if (x > 80||x<10)
47             {
48                 transform.position = originalPosion;
49                 transform.rotation = originalRotate;
50             }
51         }
52         offSet = transform.position - player.position;//重新得到相机与当前人物对象的向量
53     }
54     //滑轮控制镜头远近
55     void scroll()
56     {
57         distance = offSet.magnitude;
58         distance -= Input.GetAxis("Mouse ScrollWheel")*scrollSpeed;
59         distance = Mathf.Clamp(distance, minDis, maxDis);
60         offSet = offSet.normalized * distance;
61     }

使用到的方法:

欧拉积分

欧拉积分是运动的积分方程的一种方法。每个对象的位置计算基于其速度,质量和力量,并需要重新计算每个 tick 在游戏循环。欧拉方法是最基本和最有用的像侧滚动的射击游戏,但也有其它的方法,如Verlet 积分和 RK4积分,会更好地完成其他任务。下面我将展示一个简单的实现的想法。

你需要一个基本的结构以容纳对象的位置、 速度和其他运动相关的数据。我们提出两个相同的结构,但每一个都有不同的意义,在世界空间中︰ 点和矢量。游戏引擎通常使用某种类型的矢量类,但点和矢量之间的区别是非常重要的,大大提高了代码的可读性 (例如,您计算不是两个矢量,但这两个点之间的距离,这是更自然)。

<button onclick="eval(document.getElementById('kinematicsCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<button onclick="clearCanvas();" type="button">Clear</button>
<table border="0" style="width: 100%;">
<tbody>
<tr>
<td><canvas id="kinematicsCancas" width="400" height="400"></canvas></td>
<td width="10"> </td>
<td width="100%" valign="top">
<h4>修改代码试试看</h4>
<li>改变起始位置</li>
<li>改变起始速度(包括方向) </li>
<li>改变加速度</li>

<button onclick="eval(document.getElementById('kinematicsCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<button onclick="clearCanvas();" type="button">Clear</button>
<table border="0" style="width: 100%;">
<tbody>
<tr>
<td><canvas id="kinematicsCancas" width="400" height="400"></canvas></td>
<td width="10"> </td>
<td width="100%" valign="top">
<h4>修改代码试试看</h4>
<li>改变起始位置</li>
<li>改变起始速度(包括方向) </li>
<li>改变加速度</li>

Transform.RotateAround 围绕旋转

function RotateAround (point : Vector3, axis : Vector3, angle : float) : void

Description描述

Rotates the transform about axis passing through point in world coordinates by angle degrees.

按照angle度通过在世界坐标的point轴旋转物体。

简单的说,按照多少度在世界坐标的某位置轴旋转物体。

This modifies both the position and the rotation of the transform.

这个修改变换的位置和旋转角度。

 

Input.GetAxis("Mouse X")和Input.GetAxis("Mouse Y")可以分别得到鼠标在水平和垂直方向上拖动的动作信息。

Input.GetAxis("Mouse ScrollWheel")可以获得鼠标滚轮的前后滚动的动作信息。

 

 

简单地说, 它代表了二维空间空间中的一个元素, 它有 x 和 y 坐标, 它定义了该点在该空间中的位置。

JavaScript

function point2(x, y) { return {'x': x || 0, 'y': y || 0}; }

1
2
3
function point2(x, y) {
  return {'x': x || 0, 'y': y || 0};
}

</td>
</tr>
</tbody>
</table>

</td>
</tr>
</tbody>
</table>

Vector3.normalized 规范化

var normalized : Vector3

Description描述

Returns this vector with a magnitude of 1 (Read Only).

返回向量的长度为1(只读)。

When normalized, a vector keeps the same direction but its length is 1.0.

当规格化后,向量保持同样的方向,但是长度变为1.0。

Note that the current vector is unchanged and a new normalized vector is returned. If you want to normalize the current vector, use Normalize function.

注意,当前向量是不改变的并且返回一个新的规范化的向量。如果你想规范化当前向量,使用Normalize函数。

If the vector is too small to be normalized a zero vector will be returned.

如果这个向量太小而不能被规范化,一个零向量将会被返回。

 

 

矢量

一个矢量是一个具有长度 (或大小) 的几何对象和方向。2 D 游戏中矢量主要是用于描述力(例如重力、 空气阻力和风) 和速度,以及禁止运动或光线反射。矢量有许多用途。

JavaScript

function vector2(x, y) { return {'x': x || 0, 'y': y || 0}; }

1
2
3
function vector2(x, y) {
  return {'x': x || 0, 'y': y || 0};
}

上述函数创建了新的二维矢量和点。在这种情况下, 我们不会在 javascript 中使用 new 运算符来获得大量的性能。还要注意, 有一些 第三方库可用来操纵矢量 (glMatrix 是一个很好的候选对象)。

下面是在上面定义的二维结构上使用的一些非常常用的函数。首先, 计算两点之间的距离:

JavaScript

point2.distance = function(a, b) { // The x and y variables hold a vector pointing from point b to point a. var x = a.x - b.x; var y = a.y

  • b.y; // Now, distance between the points is just length (magnitude) of this vector, calculated like this: return Math.sqrt(x*x + y*y); };
1
2
3
4
5
6
7
point2.distance = function(a, b) {
  // The x and y variables hold a vector pointing from point b to point a.
  var x = a.x - b.x;
  var y = a.y - b.y;
  // Now, distance between the points is just length (magnitude) of this vector, calculated like this:
  return Math.sqrt(x*x + y*y);
};

矢量的大小 (长度) 可以直接从最后一行的上面的函数,这样计算︰

JavaScript

vector2.length = function(vector) { return Math.sqrt(vector.x*vector.x

  • vector.y*vector.y); };
1
2
3
vector2.length = function(vector) {
  return Math.sqrt(vector.x*vector.x + vector.y*vector.y);
};

金沙棋牌官方平台 6

矢量的长度。

矢量规范化也是非常方便的。下面的函数调整矢量的大小,所以它成为一个单位矢量;也就是说,它的长度是 1,但保持它的方向。

JavaScript

vector2.normalize = function(vector) { var length = vector2.length(vector); if (length > 0) { return vector2(vector.x / length, vector.y / length); } else { // zero-length vectors cannot be normalized, as they do not have direction. return vector2(); } };

1
2
3
4
5
6
7
8
9
10
vector2.normalize = function(vector) {
  var length = vector2.length(vector);
 
  if (length > 0) {
    return vector2(vector.x / length, vector.y / length);
  } else {
    // zero-length vectors cannot be normalized, as they do not have direction.
    return vector2();
  }
};

金沙棋牌官方平台 7

矢量归一化。

另一个有用的例子是,其方向指从一个位置到另一个位置︰

JavaScript

// Note that this function is different from `vector2.direction`. // Please don't confuse them. point2.direction = function(from, to) { var x = to.x - from.x; var y = to.y - from.y; var length = Math.sqrt(x*x + y*y); if (length > 0) { return vector2(x / length, y / length); } else { // `from` and `to` are identical return vector2(); } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Note that this function is different from `vector2.direction`.
// Please don't confuse them.
point2.direction = function(from, to) {
  var x = to.x - from.x;
  var y = to.y - from.y;
  var length = Math.sqrt(x*x + y*y);
 
  if (length > 0) {
    return vector2(x / length, y / length);
  } else {
    // `from` and `to` are identical
    return vector2();
  }
};

点积是对两个矢量 (通常为单位矢量) 的运算, 它返回一个标量的数字, 表示这些矢量的角度之间的关系。

JavaScript

vector2.dot = function(a, b) { return a.x*b.x + a.y*b.y; };

1
2
3
vector2.dot = function(a, b) {
  return a.x*b.x + a.y*b.y;
};

这程序的核心就是step()函数头两行代码。很简单吧?
粒子系统
粒子系统(particle system)是图形里常用的特效。粒子系统可应用运动学模拟来做到很多不同的效果。粒子系统在游戏和动画中,常常会用来做雨点、火花、烟、爆炸等等不同的视觉效果。有时候,也会做出一些游戏性相关的功能,例如敌人被打败后会发出一些闪光,主角可以把它们吸收。
粒子的定义
粒子系统模拟大量的粒子,并通常用某些方法把粒子渲染。粒子通常有以下特性:
<li>粒子是独立的,粒子之间互不影响(不碰撞、没有力) </li>
<li>粒子有生命周期,生命结束后会消失</li>
<li>粒子可以理解为空间的一个点,有时候也可以设定半径作为球体和环境碰撞</li>
<li>粒子带有运动状态,也有其他外观状态(例如颜色、影像等) </li>
<li>粒子可以只有线性运动,而不考虑旋转运动(也有例外) </li>

这程序的核心就是step()函数头两行代码。很简单吧?
粒子系统
粒子系统(particle system)是图形里常用的特效。粒子系统可应用运动学模拟来做到很多不同的效果。粒子系统在游戏和动画中,常常会用来做雨点、火花、烟、爆炸等等不同的视觉效果。有时候,也会做出一些游戏性相关的功能,例如敌人被打败后会发出一些闪光,主角可以把它们吸收。
粒子的定义
粒子系统模拟大量的粒子,并通常用某些方法把粒子渲染。粒子通常有以下特性:
<li>粒子是独立的,粒子之间互不影响(不碰撞、没有力) </li>
<li>粒子有生命周期,生命结束后会消失</li>
<li>粒子可以理解为空间的一个点,有时候也可以设定半径作为球体和环境碰撞</li>
<li>粒子带有运动状态,也有其他外观状态(例如颜色、影像等) </li>
<li>粒子可以只有线性运动,而不考虑旋转运动(也有例外) </li>

金沙棋牌官方平台 8

矢量点积

点积是一个矢量投影矢量 b 上的长度。返回的值为 1 表示两个矢量指向同一方向。值为-1 意味着矢量方向相反的矢量 b 点。值为 0 表示该矢量是垂直于矢量 b。

这里是实体类的示例,以便其他对象可以从它继承。只描述了与运动相关的基本属性。

JavaScript

function Entity() { ... // Center of mass usually. this.position = point2(); // Linear velocity. // There is also something like angular velocity, not described here. this.velocity = vector2(); // Acceleration could also be named `force`, like in the Box2D engine. this.acceleration = vector2(); this.mass = 1; ... }

1
2
3
4
5
6
7
8
9
10
11
12
function Entity() {
  ...
  // Center of mass usually.
  this.position = point2();
  // Linear velocity.
  // There is also something like angular velocity, not described here.
  this.velocity = vector2();
  // Acceleration could also be named `force`, like in the Box2D engine.
  this.acceleration = vector2();
  this.mass = 1;
  ...
}

您可以在你的游戏中使用像素或米为单位。我们鼓励您使用米,因为在开发过程中,它更容易平衡的事情。速度,应该是米每秒,而加速度应该是米每秒的平方。

当使用一个第三方物理引擎,只是将存储在您的实体类的物理主体(或主体集) 的引用。然后,物理引擎将在每个主体内存储所述的属性,如位置和速度。

基本的欧拉积分看起来像这样︰

JavaScript

acceleration = force / mass velocity += acceleration position += velocity

1
2
3
acceleration = force / mass
velocity += acceleration
position += velocity

上面的代码必须在游戏中每个对象的每个帧中执行。下面是在 JavaScript 中的基本执行代码︰

JavaScript

Entity.prototype.update = function(elapsed) { // Acceleration is usually 0 and is set from the outside. // Velocity is an amount of movement (meters or pixels) per second. this.velocity.x += this.acceleration.x * elapsed; this.velocity.y += this.acceleration.y * elapsed; this.position.x += this.velocity.x * elapsed; this.position.y += this.velocity.y * elapsed; ... this.acceleration.x = this.acceleration.y = 0; }

1
2
3
4
5
6
7
8
9
10
11
12
13
Entity.prototype.update = function(elapsed) {
  // Acceleration is usually 0 and is set from the outside.
  // Velocity is an amount of movement (meters or pixels) per second.
  this.velocity.x += this.acceleration.x * elapsed;
  this.velocity.y += this.acceleration.y * elapsed;
 
  this.position.x += this.velocity.x * elapsed;
  this.position.y += this.velocity.y * elapsed;
 
  ...
 
  this.acceleration.x = this.acceleration.y = 0;
}

经过的是自最后一个帧 (自最近一次调用此方法) 所经过的时间量 (以秒为单位)。对于运行在每秒 60 帧的游戏,经过的值通常是 1/60 秒,也就是 0.016 (6) s。

上文提到的增量时间的文章也涵盖了这个问题。

要移动对象,您可以更改其加速度或速度。为实现此目的,应使用如下所示的两个函数︰

JavaScript

Entity.prototype.applyForce = function(force, scale) { if (typeof scale === 'undefined') { scale = 1; } this.acceleration.x += force.x * scale / this.mass; this.acceleration.y += force.y * scale / this.mass; }; Entity.prototype.applyImpulse = function(impulse, scale) { if (typeof scale === 'undefined') { scale = 1; } this.velocity.x += impulse.x * scale / this.mass; this.velocity.y += impulse.y * scale / this.mass; };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Entity.prototype.applyForce = function(force, scale) {
  if (typeof scale === 'undefined') {
    scale = 1;
  }
  this.acceleration.x += force.x * scale / this.mass;
  this.acceleration.y += force.y * scale / this.mass;
};
 
Entity.prototype.applyImpulse = function(impulse, scale) {
  if (typeof scale === 'undefined') {
    scale = 1;
  }
  this.velocity.x += impulse.x * scale / this.mass;
  this.velocity.y += impulse.y * scale / this.mass;
};

要向右移动一个对象你可以这样做︰

JavaScript

// 10 meters per second in the right direction (x=10, y=0). var right = vector2(10, 0); if (keys.left.isDown) // The -1 inverts a vector, i.e. the vector will point in the opposite direction, // but maintain magnitude (length). spaceShip.applyImpulse(right, -1); if (keys.right.isDown) spaceShip.applyImpulse(right, 1);

1
2
3
4
5
6
7
8
9
// 10 meters per second in the right direction (x=10, y=0).
var right = vector2(10, 0);
 
if (keys.left.isDown)
  // The -1 inverts a vector, i.e. the vector will point in the opposite direction,
  // but maintain magnitude (length).
  spaceShip.applyImpulse(right, -1);
if (keys.right.isDown)
  spaceShip.applyImpulse(right, 1);

请注意,在运动中设置的对象保持运动。您需要实现某种减速停止移动的物体 (空气阻力或摩擦,也许)。

以下是本文例子里实现的粒子类:

以下是本文例子里实现的粒子类:

武器的影响

现在我要解释一下, 在我们的 HTML5 游戏中, 某些武器效果是如何射击的

复制代码 代码如下:

复制代码 代码如下:

等离子

在 Skytte中的等离子武器。

这是我们游戏中最基本的武器, 每次都是一枪。没有用于这种武器的特殊算法。当等离子子弹发射时, 游戏只需绘制一个随着时间推移而旋转的精灵。

简单的等离子子弹可以催生像这样︰

JavaScript

// PlasmaProjectile inherits from Entity class var plasma = new PlasmaProjectile(); // Move right (assuming that X axis is pointing right). var direction = vector2(1, 0); // 20 meters per second. plasma.applyImpulse(direction, 20);

1
2
3
4
5
6
7
8
// PlasmaProjectile inherits from Entity class
var plasma = new PlasmaProjectile();
 
// Move right (assuming that X axis is pointing right).
var direction = vector2(1, 0);
 
// 20 meters per second.
plasma.applyImpulse(direction, 20);

// Particle.js
Particle = function(position, velocity, life, color, size) {
this.position = position;
this.velocity = velocity;
this.acceleration = Vector2.zero;
this.age = 0;
this.life = life;
this.color = color;
this.size = size;
};

// Particle.js
Particle = function(position, velocity, life, color, size) {
this.position = position;
this.velocity = velocity;
this.acceleration = Vector2.zero;
this.age = 0;
this.life = life;
this.color = color;
this.size = size;
};

冲击波

在 Skytte 的冲击波武器。

这种武器是更复杂一点。它也绘制简单精灵作为子弹,但却有一些代码,一点点传播开,并应用随机速度。这给这个武器带来了更具破坏性的感觉,,所以玩家觉得他们可以施加比血浆武器更大的伤害, 并且在敌人中间有更好的控制人群。

该代码工作方式类似于血浆武器代码,但是它生成三发子弹,每个子弹都有一个稍微不同的方向。

JavaScript

// BlaserProjectile inherits from Entity class var topBullet = new BlasterProjectile(); // This bullet will move slightly up. var middleBullet = new BlasterProjectile(); // This bullet will move horizontally. var bottomBullet = new BlasterProjectile(); // This bullet will move slightly down. var direction; // Angle 0 is pointing directly to the right. // We start with the bullet moving slightly upwards. direction = vector2.direction(radians(-5)); // Convert angle to an unit vector topBullet.applyImpulse(direction, 30); direction = vector2.direction(radians(0)); middleBullet.applyImpulse(direction, 30); direction = vector2.direction(radians(5)); middleBullet.applyImpulse(direction, 30);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BlaserProjectile inherits from Entity class
var topBullet = new BlasterProjectile();  // This bullet will move slightly up.
var middleBullet = new BlasterProjectile();  // This bullet will move horizontally.
var bottomBullet = new BlasterProjectile();  // This bullet will move slightly down.
var direction;
 
// Angle 0 is pointing directly to the right.
// We start with the bullet moving slightly upwards.
direction = vector2.direction(radians(-5));  // Convert angle to an unit vector
topBullet.applyImpulse(direction, 30);
 
direction = vector2.direction(radians(0));
middleBullet.applyImpulse(direction, 30);
 
direction = vector2.direction(radians(5));
middleBullet.applyImpulse(direction, 30);

上面的代码需要一些数学函数来实现:

JavaScript

function radians(angle) { return angle * Math.PI / 180; } // Note that this function is different from `point2.direction`. // Please don't confuse them. vector2.direction = function(angle) { /* * Converts an angle in radians to a unit vector. Angle of 0 gives vector x=1, y=0. */ var x = Math.cos(angle); var y = Math.sin(angle); return vector2(x, y); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function radians(angle) {
  return angle * Math.PI / 180;
}
 
// Note that this function is different from `point2.direction`.
// Please don't confuse them.
vector2.direction = function(angle) {
  /*
   * Converts an angle in radians to a unit vector. Angle of 0 gives vector x=1, y=0.
   */
  var x = Math.cos(angle);
  var y = Math.sin(angle);
  return vector2(x, y);
};

游戏循环
粒子系统通常可分为三个周期:
发射粒子
模拟粒子(粒子老化、碰撞、运动学模拟等等)
渲染粒子
在游戏循环(game loop)中,需要对每个粒子系统执行以上的三个步骤。
生与死
在本文的例子里,用一个JavaScript数组particles储存所有活的粒子。产生一个粒子只是把它加到数组末端。代码片段如下:

游戏循环
粒子系统通常可分为三个周期:
发射粒子
模拟粒子(粒子老化、碰撞、运动学模拟等等)
渲染粒子
在游戏循环(game loop)中,需要对每个粒子系统执行以上的三个步骤。
生与死
在本文的例子里,用一个JavaScript数组particles储存所有活的粒子。产生一个粒子只是把它加到数组末端。代码片段如下:

在 Skytte中雷武器。

这很有趣。武器射激光射线,但它在每个帧的程序生成 (这将在稍后解释)。为了探测命中, 它会创建一个矩形对撞机, 它会在与敌人碰撞时每秒钟造成伤害。

复制代码 代码如下:

复制代码 代码如下:

火箭

图 8︰ 在 Skytte中火箭武器。

这种武器射导弹。火箭是一个精灵, 一个粒子发射器附着在它的末端。还有一些更复杂的逻辑,比如搜寻最近的敌人或限制火箭的转弯值, 使其更少机动性。。此外,火箭就不会立即寻找敌方目标 — — 他们直接飞行一段时间, 以避免不切实际的行为。

火箭走向他们的相邻的目标。这是通过计算弹丸在给定的方向移动所需的适当力量来实现的。为了避免只在直线上移动, 计算的力在 skytte不应该太大。

假设,火箭从前面所述的实体类继承的类。

JavaScript

Rocket.prototype.update = function(elapsed) { var direction; if (this.target) { // Assuming that `this.target` points to the nearest enemy ship. direction = point2.direction(this.position, this.target.position); } else { // No target, so fly ahead. // This will fail for objects that are still, so remember to apply some initial velocity when spawning rockets. direction = vector2.normalize(this.velocity); } // You can use any number here, depends on the speed of the rocket, target and units used. this.applyForce(direction, 10); // Simple inheritance here, calling parent's `update()`, so rocket actually moves. Entity.prototype.update.apply(this, arguments); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Rocket.prototype.update = function(elapsed) {
  var direction;
 
  if (this.target) {
    // Assuming that `this.target` points to the nearest enemy ship.
    direction = point2.direction(this.position, this.target.position);
  } else {
    // No target, so fly ahead.
    // This will fail for objects that are still, so remember to apply some initial velocity when spawning rockets.
    direction = vector2.normalize(this.velocity);
  }
 
  // You can use any number here, depends on the speed of the rocket, target and units used.
  this.applyForce(direction, 10);
 
  // Simple inheritance here, calling parent's `update()`, so rocket actually moves.
  Entity.prototype.update.apply(this, arguments);
};

//ParticleSystem.js
function ParticleSystem() {
// Private fields
var that = this;
var particles = new Array();
// Public fields
this.gravity = new Vector2(0, 100);
this.effectors = new Array();
// Public methods
this.emit = function(particle) {
particles.push(particle);
};
// ...
}

//ParticleSystem.js
function ParticleSystem() {
// Private fields
var that = this;
var particles = new Array();
// Public fields
this.gravity = new Vector2(0, 100);
this.effectors = new Array();
// Public methods
this.emit = function(particle) {
particles.push(particle);
};
// ...
}

高射炮

在 Skytte 中高射炮武器。

高射炮被设计为射击许多小子弹 (象猎枪), 是小斑点精灵。它有一些在锥形区域内的点的位置用特定的逻辑来随机生成这些。

金沙棋牌官方平台 9

高射炮武器子弹锥区。

在一个圆锥形的区域中生成随机点︰

JavaScript

// Firstly get random angle in degrees in the allowed span. Note that the span below always points to the right. var angle = radians(random.uniform(-40, 40)); // Now get how far from the barrel the projectile should spawn. var distance = random.uniform(5, 150); // Join angle and distance to create an offset from the gun's barrel. var direction = vector2.direction(angle); var offset = vector2(direction.x * distance, direction.y * distance); // Now calculate absolute position in the game world (you need a position of the barrel for this purpose): var position = point2.move(barrel, offset);

1
2
3
4
5
6
7
8
9
10
11
12
// Firstly get random angle in degrees in the allowed span. Note that the span below always points to the right.
var angle = radians(random.uniform(-40, 40));
 
// Now get how far from the barrel the projectile should spawn.
var distance = random.uniform(5, 150);
 
// Join angle and distance to create an offset from the gun's barrel.
var direction = vector2.direction(angle);
var offset = vector2(direction.x * distance, direction.y * distance);
 
// Now calculate absolute position in the game world (you need a position of the barrel for this purpose):
var position = point2.move(barrel, offset);

函数返回两个值之间的一个随机浮点数。一个简单的实现就像这个样子︰

JavaScript

random.uniform = function(min, max) { return min + (max-min) * Math.random(); };

1
2
3
random.uniform = function(min, max) {
  return min + (max-min) * Math.random();
};

粒子在初始化时,年龄(age)设为零,生命(life)则是固定的。年龄和生命的单位都是秒。每个模拟步,都会把粒子老化,即是把年龄增加<span class="math">Delta t</span>,年龄超过生命,就会死亡。代码片段如下:

粒子在初始化时,年龄(age)设为零,生命(life)则是固定的。年龄和生命的单位都是秒。每个模拟步,都会把粒子老化,即是把年龄增加<span class="math">Delta t</span>,年龄超过生命,就会死亡。代码片段如下:

在 Skytte 中的电武器。

电是射击在特定半径范围内的敌人的武器。它有一个有限的范围, 但可以射击在几个敌人, 并总是射击成功。它使用相同的算法绘制曲线, 以模拟闪电作为射线武器, 但具有更高的曲线因子。

复制代码 代码如下:

复制代码 代码如下:

使用技术

function ParticleSystem() {
// ...
this.simulate = function(dt) {
aging(dt);
applyGravity();
applyEffectors();
kinematics(dt);
};
// ...
// Private methods
function aging(dt) {
for (var i = 0; i < particles.length; ) {
var p = particles[i];
p.age += dt;
if (p.age >= p.life)
kill(i);
else
i++;
}
}
function kill(index) {
if (particles.length > 1)
particles[index] = particles[particles.length - 1];
particles.pop();
}
// ...
}

function ParticleSystem() {
// ...
this.simulate = function(dt) {
aging(dt);
applyGravity();
applyEffectors();
kinematics(dt);
};
// ...
// Private methods
function aging(dt) {
for (var i = 0; i < particles.length; ) {
var p = particles[i];
p.age += dt;
if (p.age >= p.life)
kill(i);
else
i++;
}
}
function kill(index) {
if (particles.length > 1)
particles[index] = particles[particles.length - 1];
particles.pop();
}
// ...
}

产生弯曲的线条

为了制造激光束效应和电子武器, 我们开发了一种计算和变换玩家的舰船和敌人之间的直线距离的算法。换句话说,我们测量的两个对象之间的距离,找到中间点,并在这一段距离随机移动它。我们为每个新场景创建重复此操作。

若要绘制这些部分我们使用 HTML5 绘制函数 lineTo()。为了实现发光颜色我们使用多行绘制到另一个更不透明的颜色和更高的描边宽度。

金沙棋牌官方平台 10

程序上弯曲的线条。

要查找并偏移其他两个点之间的点︰

JavaScript

var offset, midpoint; midpoint = point2.midpoint(A, B); // Calculate an unit-length vector pointing from A to B. offset = point2.direction(A, B); // Rotate this vector 90 degrees clockwise. offset = vector2.perpendicular(offset); // We want our offset to work in two directions perpendicular to the segment AB: up and down. if (random.sign() === -1) { // Rotate offset by 180 degrees. offset.x = -offset.x; offset.y = -offset.y; } // Move the midpoint by an offset. var offsetLength = Math.random() * 10; // Offset by 10 pixels for example. midpoint.x += offset.x * offsetLength; midpoint.y += offset.y * offsetLength; Below are functions used in the above code: point2.midpoint = function(a, b) { var x = (a.x+b.x) / 2; var y = (a.y+b.y) / 2; return point2(x, y); }; vector2.perpendicular = function(v) { /* * Rotates a vector by 90 degrees clockwise. */ return vector2(-v.y, v.x); }; random.sign = function() { return Math.random() < 0.5 ? -1 : 1; };

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
var offset, midpoint;
 
midpoint = point2.midpoint(A, B);
 
// Calculate an unit-length vector pointing from A to B.
offset = point2.direction(A, B);
 
// Rotate this vector 90 degrees clockwise.
offset = vector2.perpendicular(offset);
 
// We want our offset to work in two directions perpendicular to the segment AB: up and down.
if (random.sign() === -1) {
  // Rotate offset by 180 degrees.
  offset.x = -offset.x;
  offset.y = -offset.y;
}
 
// Move the midpoint by an offset.
var offsetLength = Math.random() * 10;  // Offset by 10 pixels for example.
midpoint.x += offset.x * offsetLength;
midpoint.y += offset.y * offsetLength;
 
Below are functions used in the above code:
point2.midpoint = function(a, b) {
  var x = (a.x+b.x) / 2;
  var y = (a.y+b.y) / 2;
  return point2(x, y);
};
 
vector2.perpendicular = function(v) {
  /*
   * Rotates a vector by 90 degrees clockwise.
   */
  return vector2(-v.y, v.x);
};
 
random.sign = function() {
  return Math.random() < 0.5 ? -1 : 1;
};

在函数kill()里,用了一个技巧。因为粒子在数组里的次序并不重要,要删除中间一个粒子,只需要复制最末的粒子到那个元素,并用pop()移除最末的粒子就可以。这通常比直接删除数组中间的元素快(在C++中使用数组或std::vector亦是)。
运动学模拟
把本文最重要的两句运动学模拟代码套用至所有粒子就可以。另外,每次模拟会先把引力加速度写入粒子的加速度。这样做是为了将来可以每次改变加速度(续篇会谈这方面)。

在函数kill()里,用了一个技巧。因为粒子在数组里的次序并不重要,要删除中间一个粒子,只需要复制最末的粒子到那个元素,并用pop()移除最末的粒子就可以。这通常比直接删除数组中间的元素快(在C++中使用数组或std::vector亦是)。
运动学模拟
把本文最重要的两句运动学模拟代码套用至所有粒子就可以。另外,每次模拟会先把引力加速度写入粒子的加速度。这样做是为了将来可以每次改变加速度(续篇会谈这方面)。

找到最近的相邻目标

火箭和电武器找到最近的敌人,我们遍历一群活跃的敌人并比较他们的位置与火箭的位置,或此项目中电武器射击点。当火箭锁定其目标,并会飞向目标时,直到它击中目标或飞出屏幕。电武器,它会等待目标出现在范围内。

一个基本的实现可能如下所示︰

JavaScript

function nearest(position, entities) { /* * Given position and an array of entites, this function finds which entity is closest * to `position` and distance. */ var distance, nearest = null, nearestDistance = Infinity; for (var i = 0; i < entities.length; i++) { // Allow list of entities to contain the compared entity and ignore it silently. if (position !== entities[i].position) { // Calculate distance between two points, usually centers of mass of each entity. distance = point2.distance(position, entities[i].position); if (distance < nearestDistance) { nearestDistance = distance; nearest = entities[i]; } } } // Return the closest entity and distance to it, as it may come handy in some situations. return {'entity': nearest, 'distance': nearestDistance}; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function nearest(position, entities) {
  /*
   * Given position and an array of entites, this function finds which entity is closest
   * to `position` and distance.
   */
  var distance, nearest = null, nearestDistance = Infinity;
 
  for (var i = 0; i < entities.length; i++) {
    // Allow list of entities to contain the compared entity and ignore it silently.
    if (position !== entities[i].position) {
      // Calculate distance between two points, usually centers of mass of each entity.
      distance = point2.distance(position, entities[i].position);
 
      if (distance < nearestDistance) {
        nearestDistance = distance;
        nearest = entities[i];
      }
    }
  }
 
  // Return the closest entity and distance to it, as it may come handy in some situations.
  return {'entity': nearest, 'distance': nearestDistance};
}

复制代码 代码如下:

复制代码 代码如下:

结论

这些主题涵盖只支持它们的基本思路。我希望读这篇文章后,你对如何开始并持续发展游戏项目会有更好的主意。查阅下面的参考,你可以自己试着做类似的游戏项目。

打赏支持我翻译更多好文章,谢谢!

打赏译者

function ParticleSystem() {
// ...
function applyGravity() {
for (var i in particles)
particles[i].acceleration = that.gravity;
}
function kinematics(dt) {
for (var i in particles) {
var p = particles[i];
p.position = p.position.add(p.velocity.multiply(dt));
p.velocity = p.velocity.add(p.acceleration.multiply(dt));
}
}
// ...
}

function ParticleSystem() {
// ...
function applyGravity() {
for (var i in particles)
particles[i].acceleration = that.gravity;
}
function kinematics(dt) {
for (var i in particles) {
var p = particles[i];
p.position = p.position.add(p.velocity.multiply(dt));
p.velocity = p.velocity.add(p.acceleration.multiply(dt));
}
}
// ...
}

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

金沙棋牌官方平台 11 金沙棋牌官方平台 12

2 赞 2 收藏 2 评论

渲染
粒子可以用很多不同方式渲染,例如用圆形、线段(当前位置和之前位置)、影像、精灵等等。本文采用圆形,并按年龄生命比来控制圆形的透明度,代码片段如下:

渲染
粒子可以用很多不同方式渲染,例如用圆形、线段(当前位置和之前位置)、影像、精灵等等。本文采用圆形,并按年龄生命比来控制圆形的透明度,代码片段如下:

关于作者:紫洋

金沙棋牌官方平台 13

除非这世界如我所愿,开启更好的应用开发定制之旅:设计:用户旅程故事板,线性原型图,信息架构,交互流程设计,高保真原型确认研发:产品调研、竞品分析、可用性测试、渐进式迭代设计工具:Sketch 3, Photoshop, Illustrator, Keynote,Axure开发语言:HTML5, CS... 个人主页 · 我的文章 · 13 ·      

金沙棋牌官方平台 14

复制代码 代码如下:

复制代码 代码如下:

function ParticleSystem() {
// ...
this.render = function(ctx) {
for (var i in particles) {
var p = particles[i];
var alpha = 1 - p.age / p.life;
ctx.fillStyle = "rgba("

function ParticleSystem() {
// ...
this.render = function(ctx) {
for (var i in particles) {
var p = particles[i];
var alpha = 1 - p.age / p.life;
ctx.fillStyle = "rgba("

  • Math.floor(p.color.r * 255) + ","
  • Math.floor(p.color.g * 255) + ","
  • Math.floor(p.color.b * 255) + ","
  • alpha.toFixed(2) + ")";
    ctx.beginPath();
    ctx.arc(p.position.x, p.position.y, p.size, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
    }
    }
    // ...
    }
  • Math.floor(p.color.r * 255) + ","
  • Math.floor(p.color.g * 255) + ","
  • Math.floor(p.color.b * 255) + ","
  • alpha.toFixed(2) + ")";
    ctx.beginPath();
    ctx.arc(p.position.x, p.position.y, p.size, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
    }
    }
    // ...
    }

基本粒子系统完成
以下的例子里,每帧会发射一个粒子,其位置在画布中间(200,200),发射方向是360度,速率为100,生命为1秒,红色、半径为5象素。

基本粒子系统完成
以下的例子里,每帧会发射一个粒子,其位置在画布中间(200,200),发射方向是360度,速率为100,生命为1秒,红色、半径为5象素。

复制代码 代码如下:

复制代码 代码如下:

var ps = new ParticleSystem();
var dt = 0.01;
function sampleDirection() {
var theta = Math.random() * 2 * Math.PI;
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function step() {
ps.emit(new Particle(new Vector2(200, 200), sampleDirection().multiply(100), 1, Color.red, 5));
ps.simulate(dt);
clearCanvas();
ps.render(ctx);
}
start("basicParticleSystemCanvas", step);

var ps = new ParticleSystem();
var dt = 0.01;
function sampleDirection() {
var theta = Math.random() * 2 * Math.PI;
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function step() {
ps.emit(new Particle(new Vector2(200, 200), sampleDirection().multiply(100), 1, Color.red, 5));
ps.simulate(dt);
clearCanvas();
ps.render(ctx);
}
start("basicParticleSystemCanvas", step);

<button onclick="eval(document.getElementById('basicParticleSystemCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<table border="0" style="width: 100%;">
<tbody>
<tr>
<td><canvas id="basicParticleSystemCanvas" width="400" height="400"></canvas></td>
<td width="10"> </td>
<td width="100%" valign="top">
<h4>修改代码试试看</h4>
<li>改变发射位置</li>
<li>向上发射,发射范围在90度内</li>
<li>改变生命</li>
<li>改变半径</li>
<li>每帧发射5个粒子</li>

<button onclick="eval(document.getElementById('basicParticleSystemCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<table border="0" style="width: 100%;">
<tbody>
<tr>
<td><canvas id="basicParticleSystemCanvas" width="400" height="400"></canvas></td>
<td width="10"> </td>
<td width="100%" valign="top">
<h4>修改代码试试看</h4>
<li>改变发射位置</li>
<li>向上发射,发射范围在90度内</li>
<li>改变生命</li>
<li>改变半径</li>
<li>每帧发射5个粒子</li>

</td>
</tr>
</tbody>
</table>

</td>
</tr>
</tbody>
</table>

简单碰撞
为了说明用数值积分相对于分析解的优点,本文在粒子系统上加简单的碰撞。我们想加入一个需求,当粒子碰到长方形室(可设为整个Canvas大小)的内壁,就会碰撞反弹,碰撞是完全弹性的(perfectly elastic collision)。
在程序设计上,我把这功能用回调方式进行。 ParticleSystem类有一个effectors数组,在进行运动学模拟之前,先执行每个effectors对象的apply()函数:
而长方形室就这样实现:

简单碰撞
为了说明用数值积分相对于分析解的优点,本文在粒子系统上加简单的碰撞。我们想加入一个需求,当粒子碰到长方形室(可设为整个Canvas大小)的内壁,就会碰撞反弹,碰撞是完全弹性的(perfectly elastic collision)。
在程序设计上,我把这功能用回调方式进行。 ParticleSystem类有一个effectors数组,在进行运动学模拟之前,先执行每个effectors对象的apply()函数:
而长方形室就这样实现:

复制代码 代码如下:

复制代码 代码如下:

// ChamberBox.js
function ChamberBox(x1, y1, x2, y2) {
this.apply = function(particle) {
if (particle.position.x - particle.size < x1 || particle.position.x + particle.size > x2)
particle.velocity.x = -particle.velocity.x;
if (particle.position.y - particle.size < y1 || particle.position.y + particle.size > y2)
particle.velocity.y = -particle.velocity.y;
};
}

// ChamberBox.js
function ChamberBox(x1, y1, x2, y2) {
this.apply = function(particle) {
if (particle.position.x - particle.size < x1 || particle.position.x + particle.size > x2)
particle.velocity.x = -particle.velocity.x;
if (particle.position.y - particle.size < y1 || particle.position.y + particle.size > y2)
particle.velocity.y = -particle.velocity.y;
};
}

这其实就是当侦测到粒子超出内壁的范围,就反转该方向的速度分量。
此外,这例子的主循环不再每次把整个Canvas清空,而是每帧画一个半透明的黑色长方形,就可以模拟动态模糊(motion blur)的效果。粒子的颜色也是随机从两个颜色中取样。

这其实就是当侦测到粒子超出内壁的范围,就反转该方向的速度分量。
此外,这例子的主循环不再每次把整个Canvas清空,而是每帧画一个半透明的黑色长方形,就可以模拟动态模糊(motion blur)的效果。粒子的颜色也是随机从两个颜色中取样。

复制代码 代码如下:

复制代码 代码如下:

var ps = new ParticleSystem();
ps.effectors.push(new ChamberBox(0, 0, 400, 400)); // 最重要是多了这语句
var dt = 0.01;
function sampleDirection(angle1, angle2) {
var t = Math.random();
var theta = angle1 * t + angle2 * (1 - t);
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function sampleColor(color1, color2) {
var t = Math.random();
return color1.multiply(t).add(color2.multiply(1 - t));
}
function step() {
ps.emit(new Particle(new Vector2(200, 200), sampleDirection(Math.PI * 1.75, Math.PI * 2).multiply(250), 3, sampleColor(Color.blue, Color.purple), 5));
ps.simulate(dt);
ctx.fillStyle="rgba(0, 0, 0, 0.1)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ps.render(ctx);
}
start("collisionChamberCanvas", step);

var ps = new ParticleSystem();
ps.effectors.push(new ChamberBox(0, 0, 400, 400)); // 最重要是多了这语句
var dt = 0.01;
function sampleDirection(angle1, angle2) {
var t = Math.random();
var theta = angle1 * t + angle2 * (1 - t);
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function sampleColor(color1, color2) {
var t = Math.random();
return color1.multiply(t).add(color2.multiply(1 - t));
}
function step() {
ps.emit(new Particle(new Vector2(200, 200), sampleDirection(Math.PI * 1.75, Math.PI * 2).multiply(250), 3, sampleColor(Color.blue, Color.purple), 5));
ps.simulate(dt);
ctx.fillStyle="rgba(0, 0, 0, 0.1)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ps.render(ctx);
}
start("collisionChamberCanvas", step);

<button onclick="eval(document.getElementById('collisionChamberCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<canvas id="collisionChamberCanvas" width="400" height="400"></canvas>

<button onclick="eval(document.getElementById('collisionChamberCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<canvas id="collisionChamberCanvas" width="400" height="400"></canvas>

互动发射
最后一个例子加入互动功能,在鼠标位置发射粒子,粒子方向是按鼠标移动速度再加上一点噪音(noise)。粒子的大小和生命都加入了随机性。

互动发射
最后一个例子加入互动功能,在鼠标位置发射粒子,粒子方向是按鼠标移动速度再加上一点噪音(noise)。粒子的大小和生命都加入了随机性。

复制代码 代码如下:

复制代码 代码如下:

var ps = new ParticleSystem();
ps.effectors.push(new ChamberBox(0, 0, 400, 400));
var dt = 0.01;
var oldMousePosition = Vector2.zero, newMousePosition = Vector2.zero;
function sampleDirection(angle1, angle2) {
var t = Math.random();
var theta = angle1 * t + angle2 * (1 - t);
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function sampleColor(color1, color2) {
var t = Math.random();
return color1.multiply(t).add(color2.multiply(1 - t));
}
function sampleNumber(value1, value2) {
var t = Math.random();
return value1 * t + value2 * (1 - t);
}
function step() {
var velocity = newMousePosition.subtract(oldMousePosition).multiply(10);
velocity = velocity.add(sampleDirection(0, Math.PI * 2).multiply(20));
var color = sampleColor(Color.red, Color.yellow);
var life = sampleNumber(1, 2);
var size = sampleNumber(2, 4);
ps.emit(new Particle(newMousePosition, velocity, life, color, size));
oldMousePosition = newMousePosition;
ps.simulate(dt);
ctx.fillStyle="rgba(0, 0, 0, 0.1)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ps.render(ctx);
}
start("interactiveEmitCanvas", step);
canvas.onmousemove = function(e) {
if (e.layerX || e.layerX == 0) { // Firefox
e.target.style.position='relative';
newMousePosition = new Vector2(e.layerX, e.layerY);
}
else
newMousePosition = new Vector2(e.offsetX, e.offsetY);
};
<button onclick="eval(document.getElementById('interactiveEmitCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<canvas id="interactiveEmitCanvas" width="400" height="400"></canvas>

var ps = new ParticleSystem();
ps.effectors.push(new ChamberBox(0, 0, 400, 400));
var dt = 0.01;
var oldMousePosition = Vector2.zero, newMousePosition = Vector2.zero;
function sampleDirection(angle1, angle2) {
var t = Math.random();
var theta = angle1 * t + angle2 * (1 - t);
return new Vector2(Math.cos(theta), Math.sin(theta));
}
function sampleColor(color1, color2) {
var t = Math.random();
return color1.multiply(t).add(color2.multiply(1 - t));
}
function sampleNumber(value1, value2) {
var t = Math.random();
return value1 * t + value2 * (1 - t);
}
function step() {
var velocity = newMousePosition.subtract(oldMousePosition).multiply(10);
velocity = velocity.add(sampleDirection(0, Math.PI * 2).multiply(20));
var color = sampleColor(Color.red, Color.yellow);
var life = sampleNumber(1, 2);
var size = sampleNumber(2, 4);
ps.emit(new Particle(newMousePosition, velocity, life, color, size));
oldMousePosition = newMousePosition;
ps.simulate(dt);
ctx.fillStyle="rgba(0, 0, 0, 0.1)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ps.render(ctx);
}
start("interactiveEmitCanvas", step);
canvas.onmousemove = function(e) {
if (e.layerX || e.layerX == 0) { // Firefox
e.target.style.position='relative';
newMousePosition = new Vector2(e.layerX, e.layerY);
}
else
newMousePosition = new Vector2(e.offsetX, e.offsetY);
};
<button onclick="eval(document.getElementById('interactiveEmitCode').value)" type="button">Run</button>
<button onclick="stop();" type="button">Stop</button>
<canvas id="interactiveEmitCanvas" width="400" height="400"></canvas>

总结
本文介绍了最简单的运动学模拟,使用欧拉方法作数值积分,并以此法去实现一个有简单碰撞的粒子系统。本文的精华其实只有两条简单公式(只有两个加数和两个乘数),希望让读者明白,其实物理模拟可以很简单。虽然本文的例子是在二维空间,但这例子能扩展至三维空间,只须把Vector2换成Vector3。本文完整源代码可下载。
续篇会谈及在此基础上加入其他物理现象,有机会再加入其他物理模拟课题。希望各位支持,并给本人更多意见。

总结
本文介绍了最简单的运动学模拟,使用欧拉方法作数值积分,并以此法去实现一个有简单碰撞的粒子系统。本文的精华其实只有两条简单公式(只有两个加数和两个乘数),希望让读者明白,其实物理模拟可以很简单。虽然本文的例子是在二维空间,但这例子能扩展至三维空间,只须把Vector2换成Vector3。本文完整源代码可下载。
续篇会谈及在此基础上加入其他物理现象,有机会再加入其他物理模拟课题。希望各位支持,并给本人更多意见。

< body>

< body>

您可能感兴趣的文章:

  • canvas实现粒子时钟效果
  • javascript转换静态图片,增加粒子动画效果
  • THREE.JS入门教程(4)创建粒子系统
  • Canvas + JavaScript 制作图片粒子效果

也许,三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过,物理学广泛地应用在今天许多游戏、动画中。为什么在这...

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:运动学模拟与粒子系统,游戏开发基础的教程

关键词:

上一篇:没有了

下一篇:没有了