汕头高端建站:HTML5游戏设计原理

2019.08.12 mf_web

196

游戏中的视觉效果定义了它们的整体外观和游戏体验。玩家被高质量的视觉吸引,从而产生更多的流量和覆盖范围。这对于创造成功的游戏并为玩家提供很多乐趣至关重要。汕头高端建站

在本文中,我们想提出一些如何在<canvas>基于HTML5的游戏中实现不同视觉效果的想法。这些例子将基于我们在游戏Skytte中所做的效果。我们将解释支持它们的基本思想,并提供我们工作中使用的效果。

你将学到什么

在我们开始之前,让我们列出我们希望您从本文中学到的东西:

  • 基本的游戏设计
    我们将看看常用于制作游戏和游戏效果的模式,如:游戏循环,精灵,碰撞和粒子系统。

  • 视觉效果的基本实现
    我们还将探索支持这些模式的理论和一些代码示例。

常见模式

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

精灵

这些只是代表游戏中物体的二维图像。当每个精灵代表一系列连续动画时,精灵可用于静态对象,也可用于动画对象。它们还可用于制作用户界面元素。

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

精灵表

它们用于在一个图像中对一组单个精灵进行分组。这减少了游戏中的文件数量,从而减少了内存和处理能耗。Sprite表包含许多以行和列彼此相邻堆叠的单个精灵,并且它们包含的精灵可以静态使用或用于动画。

Spritesheet示例
Sprite表示例。(图片来源:Kriplozoik)

这是Code + Web上的一篇文章,可以帮助您更好地理解使用精灵表的好处。

游戏循环

重要的是要意识到游戏对象并没有真正在屏幕上移动。通过将游戏世界的快照渲染到屏幕,将游戏时间提前少量(通常为1/60秒),然后再次渲染事物来实现运动的幻觉。这实际上是一种停止动作效果,可用于2-D和3D游戏。游戏循环是实现此停止动作的机制。它是运行游戏所需的主要组件。它随着时间的推移不断运行,执行各种任务。在每次迭代时,它处理用户输入,移动实体,检查冲突,并呈现游戏(最好按此顺序)。它还控制帧之间经过的游戏时间。

以下是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);};

请注意,上面的示例非常简单。它使用变量delta时间(elapsed变量),建议升级此代码以使用固定增量时间。有关详细信息,请参阅此文章。

碰撞检测

碰撞检测是指找到物体之间的交叉点。这对于许多游戏来说都是必不可少的,因为它用于检测玩家是否击中墙壁或子弹击中敌人,等等。当检测到碰撞时,它可以用于游戏逻辑; 例如,当子弹击中玩家时,健康分数会降低10分。

有很多碰撞检测算法,因为它是一个性能繁重的操作,所以明智地选择最好的方法是很重要的。要了解有关碰撞检测,算法及其实现方式的更多信息,请参阅MDN的一篇文章。

粒子和粒子系统

粒子基本上是粒子系统使用的精灵。在游戏开发中,粒子系统是由粒子发射器和分配给该发射器的粒子组成的组件。它用于模拟各种效果,如火焰,爆炸,烟雾和雨水效果。粒子随时间发射,每个发射器都有自己的参数来定义用于模拟效果的各种变量,例如速度,颜色,粒子的寿命或持续时间,重力,摩擦力和风速。

欧拉整合

汕头高端建站欧拉积分是一种数值积分运动方程的方法。每个物体的位置都是根据其速度,质量和力量计算的,并且需要在游戏循环中为每个滴答重新计算。Euler方法对于像滚动式射击游戏这样的游戏来说是最基本和最有用的,但也有其他方法,如Verlet集成和RK4集成,这些方法更适合其他任务。下面我将展示这个想法的简单实现。

您需要一个基本结构来保存对象的位置,速度和其他与运动相关的数据。我们提出了两个相同的结构,但每个结构在世界的空间中具有不同的含义:点和向量。通常游戏引擎使用某种矢量类,但点和矢量之间的区别非常重要,并且极大地提高了代码的可读性(例如,您计算的距离不是两个矢量之间,而是两个点,这更自然)。

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

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

矢量是具有长度(或幅度)和方向的几何对象。在二维游戏中,矢量主要用于描述力(例如重力,空气阻力和风)和速度,以及禁止运动或光如何从物体反射。矢量有很多用途。

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

上述函数创建新的二维向量和点。在内部,我们new在JavaScript中不使用运算符来获得大量性能。另请注意,您可以使用一些第三方库来操作向量(glMatrix是一个很好的候选者)。

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

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);};

向量的大小(长度)可以直接从上面函数的最后一行计算,如下所示:

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

矢量长度
矢量长度。(查看大图)

向量的归一化也非常方便。下面的函数调整向量的大小,使其成为单位向量; 也就是说,它的长度是1,但它的方向是保持不变的。

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();
  }};

矢量归一化
矢量归一化。(查看大图)

另一个有用的例子是有一个单位向量,其方向指向一个位置到另一个位置:

// 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();
  }};

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

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

矢量点积
矢量点积。

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

这是一个实体类的示例,因此其他对象可以从中继承。仅描述了与运动相关的基本属性。

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;
  ...}

您可以在游戏中使用像素或米数作为单位。我们鼓励您使用仪表,因为在开发过程中更容易平衡。那么,速度应该是每秒米,加速度应该是每秒平方米。

使用第三方物理引擎时,只需在实体类中存储对物理实体(或一组实体)的引用。然后,物理引擎为您提供每个体内所提到的属性,如位置和速度。

基本的Euler集成看起来像这样:

acceleration = force / massvelocity += accelerationposition += velocity

必须在游戏中的每个对象的每个帧中执行上面的代码。以下是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;}

elapsed是自上一帧(自上次调用此方法以来)以来经过的时间量(以秒为单位)。在每秒60帧运行的游戏,该elapsed值通常为1 / 60的第二,它是0.016(6)类的。

前面提到的关于增量时间的文章也涵盖了这个问题。

要移动对象,您可以更改其加速度或速度。下面显示的两个函数应该用于此目的:

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;};

要将对象移动到右侧,您可以执行以下操作:

// 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中如何制作武器效果。

等离子体

这是我们游戏中最基本的武器,每次只射击一次。这种武器没有特殊的算法。当等离子子弹射击时,游戏只会绘制一个随时间旋转的精灵。

可以产生这样一个简单的等离子子弹:

// PlasmaProjectile inherits from Entity classvar 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);

冲击波

这种武器有点复杂。它还将简单的精灵绘制为子弹,但是有一些代码会将它们展开一点并应用随机速度。这给这种武器带来了更具毁灭性的感觉,因此玩家觉得它们可以比使用等离子武器造成更大的伤害,并且在敌人中拥有更好的控制能力。

该代码与等离子武器代码类似,但它产生三个子弹,每个子弹的方向略有不同。

// BlaserProjectile inherits from Entity classvar 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 vectortopBullet.applyImpulse(direction, 30);direction = vector2.direction(radians(0));middleBullet.applyImpulse(direction, 30);direction = vector2.direction(radians(5));middleBullet.applyImpulse(direction, 30);

上述代码需要一些数学函数才能工作:

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);};

射线

这个很有意思。武器射击激光射线,但它在每帧中都是程序生成的(这将在后面解释)。为了探测命中,它会创建一个矩形对撞机,只要它与敌人发生碰撞就会每秒造成一次伤害。

火箭

这种武器射击导弹。火箭是一个精灵,它的末端附有一个粒子发射器。还有一些更复杂的逻辑,例如搜索最近的敌人或限制火箭的转弯值以减少其可操作性。此外,火箭队不会立即开始寻找敌方目标 - 他们直接飞行一段时间以避免不切实际的行为。

Skytte的火箭队向最近的邻居移动。这是通过计算射弹在任何给定方向上移动所需的适当力来实现的。为避免仅以直线移动,计算的力不应太大。

假设这Rocket是一个继承自Entity前面描述的类的类。

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);};

高射炮

Flak被设计用于射击许多小子弹(像霰弹枪一样),它们是小点精灵。它具有一些特定的逻辑,可以在锥形区域内随机生成这些点的位置。

高射炮武器子弹锥区域
高射炮武器子弹锥区域。

要在锥形区域中生成随机点:

// 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);

该random.uniform()函数返回两个值之间的随机浮点数。一个简单的实现可能如下所示:

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

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

使用的技术

程序曲线

为了创建激光束效果和电子武器,我们开发了一种算法来计算和转换玩家的船与敌人之间的线性距离。换句话说,我们测量了两个物体之间的距离,找到了中间点并随机移动它。我们为每个创建的新部分重复此操作。

要绘制这些部分,我们使用HTML5 <canvas>绘制功能lineTo()。为了实现发光的颜色,我们使用了多条线条相互绘制,颜色更加不透明,笔划宽度更高。

程序曲线
程序曲线。(查看大图)

要查找并抵消另外两点之间的点:

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;};

寻找最近邻居

为了找到最接近火箭和电子武器的敌人,我们迭代一群活跃的敌人并将他们的位置与火箭的位置或电子武器的射击点进行比较。当火箭锁定目标时,它会飞向它,直到它击中或飞离屏幕。对于电子武器,它等待目标在范围内。

基本实现可能如下所示:

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};}

结论

这些主题仅涵盖支持它们的基本思想。我们希望在阅读完文章之后,您现在可以更好地了解如何开始开发此类内容。

汕头高端建站

最新案例

寒枫总监

来电咨询

400-6065-301

微信咨询

寒枫总监

TOP