Статьи

Постройте Stage3D Shoot-‘Em-Up: взрывы, параллакс и столкновения

В этой серии учебных пособий (частично бесплатно, частично Premium) мы создаем высокопроизводительную 2D-съемку с использованием нового аппаратно-ускоренного Stage3D рендеринга Stage3D . В этой части мы добавим приятную глазу систему частиц, эффект параллакса, независимые от частоты кадров таймеры игрового цикла и обнаружение столкновений.


Давайте посмотрим на конечный результат, к которому мы будем стремиться: демо-ролик с аппаратным ускорением, который включает в себя все, начиная с первой и второй частей этой серии, а также эффективную систему частиц для множества приятных глаз, частоту кадров. независимые таймеры для последовательного движения, тонкий фоновый эффект параллакса, способность объектов вращаться вокруг друг друга и система обнаружения столкновений, способная обрабатывать тонны объектов.

Проверьте это: каждый взрыв немного отличается!


Давайте продолжим делать шутер с боковой прокруткой, вдохновленный ретро аркадными играми, такими как R-Type или Gradius в ActionScript.

В первой части этой серии мы реализовали базовый движок 2D-спрайтов, который обеспечивает отличную производительность за счет использования аппаратного рендеринга Stage3D, а также нескольких оптимизаций.

Во второй части мы реализовали титульный экран, главное меню, звуковые эффекты и музыку, а также систему ввода, чтобы игрок мог управлять своим космическим кораблем с помощью клавиатуры.

В этой части мы собираемся добавить все приятное на глаз: систему частиц, полную искр, летящих обломков, ударных волн, следов пожара двигателя и множества взрывов.

В предыдущих версиях наша игра имела блокировку частоты кадров и работала медленнее на старых компьютерах. Чтобы обеспечить одинаковую синхронизацию для всего, независимо от частоты кадров, мы собираемся изменить все единицы моделирования движения и анимации для учета точного количества миллисекунд, прошедших с предыдущего кадра. Таким образом, независимо от того, работаете ли вы со скоростью 60 кадров в секунду на современном игровом оборудовании или на старом нетбуке вашей бабушки, сам игровой опыт будет идентичным.

Наконец, мы собираемся запрограммировать обнаружение столкновений, которое требуется практически во всех играх, которые вы можете себе представить. Для того, чтобы вызвать взрывы, мы должны быть в состоянии определить, когда пуля попала в противника. Пока мы на этом, мы собираемся добавить немного дополнительного пизацза, просто для удовольствия, в том числе эффект вертикального параллакса на фоне звездного поля и спутник «power orb», вдохновленный R-Type, который окружает корабль игрока ,


Если у вас его еще нет, обязательно загрузите исходный код из второй части . Откройте файл проекта в FlashDevelop и будьте готовы обновить игру!

Этот исходный код будет работать в любом другом компиляторе AS3, от CS6 до Flash Builder. Если вы используете FB, обязательно -default-frame-rate 60 « -default-frame-rate 60 » в параметры компилятора, чтобы обеспечить максимальную производительность.


Мы собираемся воспользоваться хорошо оптимизированными внутренними компонентами вашего класса менеджера сущностей с прошлого раза, добавив в него простую систему частиц, которая все еще использует все те же базовые функции сущностей и таблиц спрайтов.

Таким образом, мы по-прежнему визуализируем спрайты всей игры (корабли, пули и все) в одном пакете геометрии, используя одну текстуру. Поэтому большая часть симуляции частиц будет обрабатываться так же, как мы в настоящее время управляем движением врагов. Самое главное, мы собираемся свести к минимуму количество вызовов отрисовки, вставляя частицы в нашу существующую партию спрайтов.

Первое, что нам нужно сделать, это определить несколько интересных эффектов. Мы собираемся немного повеселиться и создать несколько круто выглядящих эффектов, таких как расширяющееся кольцо голубоватой энергии («ударная волна»), куча разных огненных шаров, которые вращаются и исчезают, некоторые быстро движущиеся искры, которые остаются красивыми и яркий и какой-то металлический обломки корпуса космического корабля.

Создайте новый файл в вашем проекте под названием GameParticles.as и реализуйте основные функции помощника частиц и взрывов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila — www.mcfunkypants.com
 
// GameParticles.as
// A simple particle system class that is
// used by the EntityManager for explosions, etc.
 
package
{
  import flash.geom.Point;
 
  public class GameParticles
  {
    public var allParticles : Vector.<Entity>;
    public var gfx:EntityManager;
   
    public function GameParticles(entityMan:EntityManager)
    {
      allParticles = new Vector.<Entity>();
      gfx = entityMan;
    }
     
    // a cool looking explosion effect with a big fireball,
    // a blue fast shockwave, smaller bursts of fire,
    // a bunch of small sparks and pieces of hull debris
    public function addExplosion(pos:Point):void
    {
      addShockwave(pos);
      addDebris(pos,6,12);
      addFireball(pos);
      addBursts(pos,10,20);
      addSparks(pos,8,16);
    }

В приведенном выше коде мы создали новый класс, который требует ссылку на наш существующий EntityManager . В конструкторе класса мы инициализируем список всех известных частиц, что может быть полезно в следующем уроке, чтобы избавить от необходимости перебирать все известные сущности, если все, что мы хотим, это частицы.

Функция addExplosion() будет вызываться в ответ на обнаружение столкновения между пулей и противником во время игры. Он породит ударную волну, немного мусора, огромный огненный шар, несколько маленьких вращающихся огненных шаров и кучу летящих искр. Давайте определим эти различные эффекты дальше — но прежде чем мы сможем, нам нужна общая функция создания частиц.


Продолжайте добавлять в GameParticles.as , реализуя функцию инициализации для общей частицы. Он будет использовать наш менеджер сущностей, чтобы порождать (или восстанавливать из списка неактивных сущностей) спрайт с некоторыми подходящими свойствами.

Некоторые значения по умолчанию не могут быть вставлены в само объявление функции, так как они будут использовать некоторую случайность, поэтому мы просто используем NaN («не число») в качестве необязательного параметра по умолчанию и выполняем некоторый код, если значение не было определяется, когда эта функция запускается. Таким образом, нам не нужно указывать все о конкретной частице, если будут использоваться значения по умолчанию. Если мы использовали ноль в качестве значения по умолчанию, то мы не могли бы принудительно установить нулевое значение в качестве действительного значения.

01
02
03
04
05
06
07
08
09
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
public function addParticle(
  spr:uint, // sprite ID
  x:int, y:int, // starting location
  startScale:Number = 0.01, // initial scale
  spdX:Number = 0, // horizontal speed in px/sec
  spdY:Number = 0, // vertical speed in px/sec
  startAlpha:Number = 1, // initial transparency (1=opaque)
  rot:Number = NaN, // starting rotation in degrees/sec
  rotSpd:Number = NaN, // rotational speed in degrees/sec
  fadeSpd:Number = NaN, // fade in/out speed per second
  zoomSpd:Number = NaN // growth speed per second
  ):Entity
{
  // Defaults tell us to to randomize some properties
  // Why NaN?
  if (isNaN(rot)) rot = gfx.fastRandom() * 360;
  if (isNaN(rotSpd)) rotSpd = gfx.fastRandom() * 360 — 180;
  if (isNaN(fadeSpd)) fadeSpd = -1 * (gfx.fastRandom() * 1 + 1);
  if (isNaN(zoomSpd)) zoomSpd = gfx.fastRandom() * 2 + 1;
   
  var anEntity:Entity;
  anEntity = gfx.respawn(spr);
  anEntity.sprite.position.x = x;
  anEntity.sprite.position.y = y;
  anEntity.speedX = spdX;
  anEntity.speedY = spdY;
  anEntity.sprite.rotation = rot * gfx.DEGREES_TO_RADIANS;
  anEntity.rotationSpeed = rotSpd * gfx.DEGREES_TO_RADIANS;
  anEntity.collidemode = 0;
  anEntity.fadeAnim = fadeSpd;
  anEntity.zoomAnim = zoomSpd;
  anEntity.sprite.scaleX = startScale;
  anEntity.sprite.scaleY = startScale;
  anEntity.sprite.alpha = startAlpha;
  if (!anEntity.recycled)
    allParticles.push(anEntity);
  return anEntity;
}

Последний шаг в создании нашего удивительного нового класса систем частиц — создание функций для различных специальных эффектов, используемых нашими взрывами в игре, как указано выше.

01
02
03
04
05
06
07
08
09
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
60
    // one big spinning ball of fire
    public function addFireball(pos:Point):void
    {
      addParticle(gfx.spritenumFireball, pos.x, pos.y, 0.01, 0, 0, 1, NaN, NaN, NaN, 4);
    }
     
    // a shockwave ring that expands quickly
    public function addShockwave(pos:Point):void
    {
      addParticle(gfx.spritenumShockwave, pos.x, pos.y, 0.01, 0, 0, 1, NaN, NaN, -3, 20);
    }
 
    // several small fireballs that move and spin
    public function addBursts(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        addParticle(gfx.spritenumFireburst,
          pos.x + gfx.fastRandom() * 16 — 8,
          pos.y + + gfx.fastRandom() * 16 — 8,
          0.02,
          gfx.fastRandom() * 200 — 100,
          gfx.fastRandom() * 200 — 100,
          0.75);
      }
    }
 
    // several small bright glowing sparks that move quickly
    public function addSparks(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        // small sparks that stay bright but get smaller
        addParticle(gfx.spritenumSpark, pos.x, pos.y, 1,
          gfx.fastRandom() * 320 — 160,
          gfx.fastRandom() * 320 — 160,
          1, NaN, NaN, 0, -1.5);
      }
    }
       
    // small pieces of destroyed spaceship debris, moving on average slightly forward
    public function addDebris(pos:Point, mincount:uint, maxcount:uint):void
    {
      var nextparticle:int = 0;
      var numparticles:int = gfx.fastRandom() * mincount + (maxcount-mincount);
      for (nextparticle = 0; nextparticle < numparticles; nextparticle++)
      {
        addParticle(gfx.spritenumDebris, pos.x, pos.y, 1,
        gfx.fastRandom() * 180 — 120,
        gfx.fastRandom() * 180 — 90,
        1, NaN, NaN, -1, 0);
      }
    }
 
  } // end class
} // end package

Вот и все для нашей упрощенной системы частиц. Как вы можете видеть, мы определяем только поведение каждого типа визуального эффекта в этом классе: работа по анимации каждой частицы выполняется одновременно с анимацией всех других сущностей с помощью нашего класса менеджера сущностей, который мы обновим следующим. Во-первых, однако, нам нужно добавить несколько новых свойств в наш базовый класс сущностей для поддержки этого нового поведения.


Нам нужно добавить несколько новых свойств в наш базовый класс сущностей. Так как мы собираемся сделать так, чтобы корабль игрока выводил непрерывный поток огня от двигателей, например, мы хотим иметь возможность хранить эту новую информацию для каждой сущности. Кроме того, некоторые новые свойства, которые относятся к моделированию частиц и обнаружению столкновений, должны быть определены здесь. Откройте существующий Entity.as и внесите несколько изменений следующим образом.

01
02
03
04
05
06
07
08
09
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila — www.mcfunkypants.com
 
// Entity.as
// The Entity class will eventually hold all game-specific entity stats
// for the spaceships, bullets and effects in our game.
// it simply holds a reference to a gpu sprite and a few demo properties.
// This is where you would add hit points, weapons, ability scores, etc.
 
package
{
  import flash.geom.Point;
  import flash.geom.Rectangle;
   
  public class Entity
  {
    private var _speedX : Number;
    private var _speedY : Number;
    private var _sprite : LiteSprite;
    public var active : Boolean = true;
 
    // if this is set, custom behaviors are run
    public var aiFunction : Function;
     
    // collision detection
    public var isBullet:Boolean = false;
    public var leavesTrail:Boolean = false;
    public var collidemode:uint = 0;
    public var collideradius:uint = 32;
    // box collision is not implemented (yet)
    public var collidebox:Rectangle = new Rectangle(-16, -16, 32, 32);
    public var collidepoints:uint = 25;
    public var touching:Entity;
    public var owner:Entity;
    public var orbiting:Entity;
    public var orbitingDistance:Number;
     
    // used for particle animation (in units per second)
    public var fadeAnim:Number = 0;
    public var zoomAnim:Number = 0;
    public var rotationSpeed:Number = 0;
 
    // used to mark whether or not this entity was
    // freshly created or reused from an inactive one
    public var recycled:Boolean = false;
     
    public function Entity(gs:LiteSprite = null)
    {
      _sprite = gs;
      _speedX = 0.0;
      _speedY = 0.0;
    }
     
    public function die() : void
    {
      // allow this entity to be reused by the entitymanager
      active = false;
      // skip all drawing and updating
      sprite.visible = false;
      // reset some things that might affect future reuses:
      leavesTrail = false;
      isBullet = false;
      touching = null;
      owner = null;
      collidemode = 0;
    }
 
    public function get speedX() : Number
    {
      return _speedX;
    }
    public function set speedX(sx:Number) : void
    {
      _speedX = sx;
    }
    public function get speedY() : Number
    {
      return _speedY;
    }
    public function set speedY(sy:Number) : void
    {
      _speedY = sy;
    }
    public function get sprite():LiteSprite
    {
      return _sprite;
    }
    public function set sprite(gs:LiteSprite):void
    {
      _sprite = gs;
    }

Как вы можете видеть, большинство из них такие же, как в предыдущем уроке. Когда сущность «умирает» (то есть становится невидимой и доступной для повторного использования нашим оптимизированным пулом повторного использования сущностей), мы отключаем некоторые из этих новых значений, чтобы следующая сущность, повторно использующая этот спрайт, не принимала нежелательных поведения.


Это самая важная часть этого урока. Мы собираемся превратить нашу техническую демонстрацию из простой графической демонстрации в нечто, похожее на настоящую игру, путем реализации нашей процедуры обнаружения столкновений.

Для простоты (и быстрой) пока мы не собираемся реализовывать столкновение ограничивающего прямоугольника, столкновение прямоугольника с сферой или луч, которое часто используется в сложных физических движках. Мы сконцентрируемся на том, что нужно нашей игре, и это простой способ определить, находится ли что-то «достаточно близко» к чему-то другому, чтобы вызвать столкновение (и последующий взрыв).

Обнаружение столкновения сфер просто проверяет, находится ли один круг в радиусе другого. Таким образом, мы можем дать разным объектам размер «радиуса» и проверить, как далеко находятся их центральные точки, чтобы определить, перекрываются ли эти два круга. Чтобы заставить нашу игру работать еще быстрее, мы будем выполнять эту математику только в том случае, если обе сущности настроены на столкновение.

В качестве дальнейшей оптимизации вместо использования встроенной функции Point.distance мы собираемся выполнить математику тригонометрии вручную, так как это было показано в тестах производительности примерно в шесть раз быстрее. Похоже, больше кода, но все, что мы на самом деле делаем, это теорема Пифагора без каких-либо квадратных корней.

Избегая использования «правильного» расстояния и сравнивая «квадраты» расстояний, используя только умножение, мы фактически проверяем расстояние до степени двойки. Однако все это не имеет значения. Конечный результат — это очень быстрый и простой способ проверить, перекрываются ли два круга, и при этом не нужно использовать вычисления sin, cos, деления, степеней или квадратного корня. Неряшливый, но эффективный!

01
02
03
04
05
06
07
08
09
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
    // used for collision callback performed in GameActorpool
    public function colliding(checkme:Entity):Entity
    {
      if (collidemode == 0)
      {
        return null;
      }
      else if (collidemode == 1) // sphere
      {
        if (isCollidingSphere(checkme))
          return checkme;
        else
          return null;
      }
    }
   
  // simple sphere to sphere collision
  public function isCollidingSphere(checkme:Entity):Boolean
  {
    // never collide with yourself
    if (this == checkme) return false;
    // only check if these shapes are collidable
    if (!collidemode || !checkme.collidemode) return false;
    // don’t check your own bullets
    if (checkme.owner == this) return false;
    // don’t check things on the same «team»
    if (checkme.owner == owner) return false;
    // don’t check if no radius
    if (collideradius == 0 || checkme.collideradius == 0) return false;
     
    // this is the simpler way to do it, but it runs really slow
    // var dist:Number = Point.distance(sprite.position, checkme.sprite.position);
    // if (dist <= (collideradius+checkme.collideradius))
     
    // this looks weird but is 6x faster than the above
    // see: http://www.emanueleferonato.com/2010/10/13/as3-geom-point-vs-trigonometry/
    if (((sprite.position.x — checkme.sprite.position.x) *
      (sprite.position.x — checkme.sprite.position.x) +
      (sprite.position.y — checkme.sprite.position.y) *
      (sprite.position.y — checkme.sprite.position.y))
      <=
      (collideradius+checkme.collideradius)*(collideradius+checkme.collideradius))
    {
      touching = checkme;
      return true;
    }
     
    // default: too far away
    // trace(«No collision. Dist = «+dist);
    return false;
     
  }
 
  } // end class
} // end package

Это для нашего недавно обновленного класса Entity.as . Теперь в нашей игре есть сущности, которые хранят статистику, необходимую для этого урока, и могут рассчитывать столкновения.


В диспетчере сущностей появилось много новых дополнений, которые добавляют возможность запрашивать обнаружение столкновений, запускать звуковые эффекты, добавлять частицы и многое другое. Откройте существующий EntityManager.as и внесите несколько изменений следующим образом. Со времени последнего изменения было так много мелких изменений, что весь файл указан здесь, чтобы избежать путаницы, поэтому вы можете просто заменить весь класс этой новой третьей версией.

01
02
03
04
05
06
07
08
09
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila — www.mcfunkypants.com
 
// EntityManager.as
// The entity manager handles a list of all known game entities.
// This object pool will allow for reuse (respawning) of
// sprites: for example, when enemy ships are destroyed,
// they will be re-spawned when needed as an optimization
// that increases fps and decreases ram use.
 
package
{
  import flash.display.Bitmap;
  import flash.display3D.*;
  import flash.geom.Point;
  import flash.geom.Rectangle;
   
  public class EntityManager
  {
    // a particle system class that updates our sprites
    public var particles:GameParticles;
     
    // so that explosions can be played
    public var sfx:GameSound;
     
    // the sprite sheet image
    public var spriteSheet : LiteSpriteSheet;
    private const SpritesPerRow:int = 8;
    private const SpritesPerCol:int = 8;
    [Embed(source=»../assets/sprites.png»)]
    private var SourceImage : Class;
 
    // the general size of the player and enemies
    private const shipScale:Number = 1.5;
    // how fast player bullets go per second
    public var bulletSpeed:Number = 250;
 
    // for framerate-independent timings
    public var currentFrameSeconds:Number = 0;
     
    // sprite IDs (indexing the spritesheet)
    public const spritenumFireball:uint = 63;
    public const spritenumFireburst:uint = 62;
    public const spritenumShockwave:uint = 61;
    public const spritenumDebris:uint = 60;
    public const spritenumSpark:uint = 59;
    public const spritenumBullet3:uint = 58;
    public const spritenumBullet2:uint = 57;
    public const spritenumBullet1:uint = 56;
    public const spritenumPlayer:uint = 10;
    public const spritenumOrb:uint = 17;
 
    // reused for calculation speed
    public const DEGREES_TO_RADIANS:Number = Math.PI / 180;
    public const RADIANS_TO_DEGREES:Number = 180 / Math.PI;
     
    // the player entity — a special case
    public var thePlayer:Entity;
    // a «power orb» that orbits the player
    public var theOrb:Entity;
     
    // a reusable pool of entities
    // this contains every known Entity
    // including the contents of the lists below
    public var entityPool : Vector.<Entity>;
    // these pools contain only certain types
    // of entity as an optimization for smaller loops
    public var allBullets : Vector.<Entity>;
    public var allEnemies : Vector.<Entity>;
     
    // all the polygons that make up the scene
    public var batch : LiteSpriteBatch;
     
    // for statistics
    public var numCreated : int = 0;
    public var numReused : int = 0;
     
    public var maxX:int;
    public var minX:int;
    public var maxY:int;
    public var minY:int;

В приведенном выше коде мы определяем большое количество переменных класса, многие из которых являются новыми для этого учебника. Следует отметить те, которые относятся к времени и скорости. Вместо того, чтобы относительные скорости каждого движущегося корабля были привязаны непосредственно к частоте кадров, мы хотим, чтобы игра шла с одинаковой скоростью независимо от того, какую машину использует игрок.

Отслеживая время, прошедшее с предыдущего кадра, мы можем умножить различные константы «скорость в секунду» на количество миллисекунд, которые текущий кадр потратил для достижения плавного движения, даже если FPS колеблется.


В приведенном выше коде мы сохраняем постоянные значения для позиций различных спрайтов в нашем изображении таблицы спрайтов. Мы добавили несколько новых видов спрайтов, которые относятся к частицам, и реорганизовали таблицу спрайтов, чтобы наши незваные враги появлялись только из первых нескольких рядов. Эти изменения привели к небольшим изменениям в нашей таблице спрайтов:

Наш обновленный спрайт-лист.
Щелкните правой кнопкой мыши, чтобы загрузить.

Как видите, теперь у нас есть несколько дополнительных спрайтов для взрывов, ударных волн, искр и осколков.


Продолжая EntityManager.as , обновите основные процедуры инициализации, чтобы создать списки для каждого типа объекта. Каждый из этих списков отслеживает определенный вид сущности / спрайта. Мы можем использовать эти списки для повышения производительности, когда нам нужно пройтись по всем объектам только определенного типа, сэкономив время, которое потребуется для просмотра всех известных объектов любого вида. Однако частицы будут храниться в своем собственном экземпляре класса, используя новый класс GameParticles мы реализовали выше.

01
02
03
04
05
06
07
08
09
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
60
61
62
public function EntityManager(view:Rectangle)
{
  entityPool = new Vector.<Entity>();
  allBullets = new Vector.<Entity>();
  allEnemies = new Vector.<Entity>();
  particles = new GameParticles(this);
  setPosition(view);
}
 
public function setPosition(view:Rectangle):void
{
  // allow moving fully offscreen before
  // automatically being culled (and reused)
  maxX = view.width + 64;
  minX = view.x — 64;
  maxY = view.height + 64;
  minY = view.y — 64;
}
 
public function createBatch(context3D:Context3D) : LiteSpriteBatch
{
  var sourceBitmap:Bitmap = new SourceImage();
 
  // create a spritesheet with 8×8 (64) sprites on it
  spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, SpritesPerRow, SpritesPerCol);
   
  // Create new render batch
  batch = new LiteSpriteBatch(context3D, spriteSheet);
   
  return batch;
}
 
// search the entity pool for unused entities and reuse one
// if they are all in use, create a brand new one
public function respawn(sprID:uint=0):Entity
{
  var currentEntityCount:int = entityPool.length;
  var anEntity:Entity;
  var i:int = 0;
  // search for an inactive entity
  for (i = 0; i < currentEntityCount; i++ )
  {
    anEntity = entityPool[i];
    if (!anEntity.active && (anEntity.sprite.spriteId == sprID))
    {
      //trace(‘Reusing Entity #’ + i);
      anEntity.active = true;
      anEntity.sprite.visible = true;
      anEntity.recycled = true;
      numReused++;
      return anEntity;
    }
  }
  // none were found so we need to make a new one
  //trace(‘Need to create a new Entity #’ + i);
  var sprite:LiteSprite;
  sprite = batch.createChild(sprID);
  anEntity = new Entity(sprite);
  entityPool.push(anEntity);
  numCreated++;
  return anEntity;
}

Выше мы также обновили функцию setPosition чтобы позволить объектам выходить за пределы любого из четырех краев экрана перед тем, как их setPosition , поскольку сейчас все движется во всех направлениях. В уроке прошлой недели сущности были уничтожены только в том случае, если они отошли от левого края и отскочили от остальных трех краев экрана. Функции createBatch и respawn практически не изменились с прошлого раза.

Чтобы избежать добавления сущностей в различные enEntity.recycled которые мы реализовали в конструкторе классов более одного раза, мы добавляем в сущность новый флаг enEntity.recycled который сообщает менеджеру сущности, является ли возвращаемый им спрайт совершенно новым. или не. Таким образом, в наших списках хранится только одна ссылка на каждый спрайт.


В качестве еще одной небольшой оптимизации вместо того, чтобы использовать Math.random() снова и снова во время игры, мы собираемся реализовать немного более быструю псевдослучайную функцию на основе XOR.

Эта функция имеет дополнительное преимущество, за исключением того факта, что она работает в четыре раза быстрее, чем встроенная случайная функция. При желании его можно fastrandomseed с постоянным значением в качестве начального fastrandomseed , чтобы каждый раз производить один и тот же набор случайных чисел в последовательности.

Это может пригодиться в будущих версиях вашей игры для хранения повторов или для сохранения игр. На данный момент, однако, единственная причина, по которой мы так поступаем, — это повысить производительность.

01
02
03
04
05
06
07
08
09
10
11
12
// this XOR based fast random number generator runs 4x faster
// than Math.random() and also returns a number from 0 to 1
// see http://www.calypso88.com/?cat=7
private const FASTRANDOMTOFLOAT:Number = 1 / uint.MAX_VALUE;
private var fastrandomseed:uint = Math.random() * uint.MAX_VALUE;
public function fastRandom():Number
{
  fastrandomseed ^= (fastrandomseed << 21);
  fastrandomseed ^= (fastrandomseed >>> 35);
  fastrandomseed ^= (fastrandomseed << 4);
  return (fastrandomseed * FASTRANDOMTOFLOAT);
}

Продолжая EntityManager.as , EntityManager.as следующие незначительные улучшения в игровые устройства. В частности, мы собираемся установить для булевого флага thePlayer.leavesTrail значение true, чтобы двигатели игрока излучали устойчивый поток огненных шаров, которые быстро сжимаются и исчезают. Это даст хороший эффект.

Кроме того, просто ради интереса давайте реализуем «шар силы», который вращается вокруг корабля игрока. Этот «компаньон» вдохновлен ретро-шутерами, такими как R-Type, и придаст нашей игре чуть больше пизацца. «Шар», как мы его назовем, будет вращаться вокруг игрока, испуская меньший собственный след, и сможет уничтожать входящих врагов.

В будущих версиях нашей игры может быть интересно, чтобы «убийства сфер» давали игроку больше очков, чем те, которые были получены при стрельбе из пуль. Этакий «навык выстрела», так сказать. Вы могли бы даже получить специальную награду за достижение за весь пройденный уровень, не выполняя ни единого выстрела — используя шар в качестве единственного средства для защиты себя.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// this entity is the PLAYER
public function addPlayer(playerController:Function):Entity
{
  thePlayer = respawn(spritenumPlayer);
  thePlayer.sprite.position.x = 32;
  thePlayer.sprite.position.y = maxY / 2;
  thePlayer.sprite.rotation = 180 * DEGREES_TO_RADIANS;
  thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = shipScale;
  thePlayer.speedX = 0;
  thePlayer.speedY = 0;
  thePlayer.active = true;
  thePlayer.aiFunction = playerController;
  thePlayer.leavesTrail = true;
   
  // just for fun, spawn an orbiting «power orb»
  theOrb = respawn(spritenumOrb);
  theOrb.rotationSpeed = 720 * DEGREES_TO_RADIANS;
  theOrb.leavesTrail = true;
  theOrb.collidemode = 1;
  theOrb.collideradius = 12;
  theOrb.isBullet = true;
  theOrb.owner = thePlayer;
  theOrb.orbiting = thePlayer;
  theOrb.orbitingDistance = 180;
   
  return thePlayer;
}

Функции shootBullet и addEntity последнего времени практически не addEntity , но включены здесь, чтобы упростить вашу жизнь. Обратите внимание, что мы теперь используем новые свойства сущностей для столкновений, и случайные сущности теперь устанавливаются на правильное вращение, чтобы быть направленным в любом направлении, в котором они летят.

01
02
03
04
05
06
07
08
09
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
// shoot a bullet (from the player for now)
public function shootBullet(powa:uint=1):Entity
{
  var anEntity:Entity;
  // three possible bullets, progressively larger
  if (powa == 1)
    anEntity = respawn(spritenumBullet1);
  else if (powa == 2)
    anEntity = respawn(spritenumBullet2);
  else
    anEntity = respawn(spritenumBullet3);
  anEntity.sprite.position.x = thePlayer.sprite.position.x + 8;
  anEntity.sprite.position.y = thePlayer.sprite.position.y + 2;
  anEntity.sprite.rotation = 180 * DEGREES_TO_RADIANS;
  anEntity.sprite.scaleX = anEntity.sprite.scaleY = 1;
  anEntity.speedX = bulletSpeed;
  anEntity.speedY = 0;
  anEntity.owner = thePlayer;
  anEntity.collideradius = 10;
  anEntity.collidemode = 1;
  anEntity.isBullet = true;
  if (!anEntity.recycled)
    allBullets.push(anEntity);
  return anEntity;
}
 
// for this test, create random entities that move
// from right to left with random speeds and scales
public function addEntity():void
{
  var anEntity:Entity;
  var randomSpriteID:uint = Math.floor(fastRandom() * 55);
  // try to reuse an inactive entity (or create a new one)
  anEntity = respawn(randomSpriteID);
  // give it a new position and velocity
  anEntity.sprite.position.x = maxX;
  anEntity.sprite.position.y = fastRandom() * maxY;
  anEntity.speedX = 15 * ((-1 * fastRandom() * 10) — 2);
  anEntity.speedY = 15 * ((fastRandom() * 5) — 2.5);
  anEntity.sprite.scaleX = shipScale;
  anEntity.sprite.scaleY = shipScale;
  anEntity.sprite.rotation = pointAtRad(anEntity.speedX,anEntity.speedY)
    — (90*DEGREES_TO_RADIANS);
  anEntity.collidemode = 1;
  anEntity.collideradius = 16;
  if (!anEntity.recycled)
    allEnemies.push(anEntity);
}

Далее нам нужно реализовать некоторые вспомогательные математические функции, используемые в подпрограммах порождения выше. Они очень удобны и могут быть использованы во многих отношениях в будущем. Поскольку Flash (и большинство игровых движков, по крайней мере, в процедурах низкого уровня) хранит вращение объекта, используя радианы (вместо градусов), мы определили постоянную выше, что ускоряет эти вычисления.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// returns the angle in radians of two points
public function pointAngle(point1:Point, point2:Point):Number
{
  var dx:Number = point2.x — point1.x;
  var dy:Number = point2.y — point1.y;
  return -Math.atan2(dx,dy);
}
 
// returns the angle in degrees of 0,0 to x,y
public function pointAtDeg(x:Number, y:Number):Number
{
  return -Math.atan2(x,y) * RADIANS_TO_DEGREES;
}
 
// returns the angle in radians of 0,0 to x,y
public function pointAtRad(x:Number, y:Number):Number
{
  return -Math.atan2(x,y);
}

Каждый кадр, когда каждый объект перемещается в новое местоположение, те, у которых свойство collidemode свойства collidemode установлено в ненулевое значение, будут отправлены в процедуру checkCollisions ниже.

Вместо того, чтобы иметь каждый отдельный объектный цикл через любой другой известный объект и проверять наличие коллизий, мы можем оптимизировать подавляющее большинство этих проверок. Это потому что только пули должны проверять столкновения, и в нашей демонстрационной игре они могут сталкиваться только с вражескими кораблями (не другими пулями, частицами или игроком).

Следовательно, теперь мы можем воспользоваться одним из « allEnemies », которые мы заполнили выше, для поиска только тех объектов, которые могут потребовать обнаружения коллизий, списка allEnemies . Функция обнаружения столкновений, которую мы написали ранее (в классе сущностей), проверит, чтобы пуля и враг находились достаточно близко друг к другу (и не имели одного и того же «владельца», что будет полезно для будущих версий, где пули противника не нужно сталкиваться с друзьями).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function checkCollisions(checkMe:Entity):Entity
{
  var anEntity:Entity;
  for(var i:int=0; i< allEnemies.length;i++)
  {
    //anEntity = entityPool[i];
    anEntity = allEnemies[i];
    if (anEntity.active && anEntity.collidemode)
    {
      if (checkMe.colliding(anEntity))
      {
        if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5));
 
        particles.addExplosion(checkMe.sprite.position);
        if ((checkMe != theOrb) && (checkMe != thePlayer))
          checkMe.die();
        if ((anEntity != theOrb) && ((anEntity != thePlayer)))
          anEntity.die();
        return anEntity;
      }
    }
  }
  return null;
}

Последний шаг в обновлении нашего скоростного EntityManager.as — это обновление всего цикла обновления симуляции. Эта функция просматривает весь список активных объектов (врагов, пуль и частиц) и обновляет их позиции, прозрачность, размер, вращение и многое другое.

Мы должны убедиться, что все анимируется с одинаковой скоростью, независимо от текущей частоты кадров игры, поэтому в этой новой версии мы отслеживаем прошедшее время с предыдущего кадра и умножаем все скорости на это значение.

Таким образом, если частота кадров равна шелковистой плавности 60 кадров в секунду, враг может двигаться только на один пиксель в определенном направлении, но если игрок использовал старый компьютер с плохой графической производительностью, а частота кадров составляла всего 15 кадров в секунду, тот же спрайт был бы перемещен 4 пиксели.

Подобные действия гарантируют бесперебойную игру независимо от того, какую машину вы используете; Кроме того, во время игры даже на быстрой машине FPS будет колебаться, и мы не хотим, чтобы скорость полета игрока колебалась вместе с ним.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
    // called every frame: used to update the simulation
    // this is where you would perform AI, physics, etc.
    // in this version, currentTime is seconds since the previous frame
    public function update(currentTime:Number) : void
    {
      var anEntity:Entity;
      var i:int;
      var max:int;
       
      // what portion of a full second has passed since the previous update?
      currentFrameSeconds = currentTime / 1000;
       
      // handle all other entities
      max = entityPool.length;
      for (i = 0; i < max; i++)
      {
        anEntity = entityPool[i];
        if (anEntity.active)
        {
          anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds;
          anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds;
           
          // the player follows different rules
          if (anEntity.aiFunction != null)
          {
            anEntity.aiFunction(anEntity);
          }
          else // all other entities use the "demo" logic
          {
           
            // collision detection
            if (anEntity.isBullet && anEntity.collidemode)
            {
              checkCollisions(anEntity);
            }
             
            // entities can orbit other entities
            // (uses their rotation as the position)
            if (anEntity.orbiting != null)
            {
              anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x +
                ((Math.sin(anEntity.sprite.rotation/4)/Math.PI) *
                anEntity.orbitingDistance);
              anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y -
                ((Math.cos(anEntity.sprite.rotation/4)/Math.PI) *
                anEntity.orbitingDistance);
            }
 
            // entities can leave an engine emitter trail
            if (anEntity.leavesTrail)
            {
              // leave a trail of particles
              if (anEntity == theOrb)
                particles.addParticle(63,
                  anEntity.sprite.position.x, anEntity.sprite.position.y,
                  0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1);
              else // player
                particles.addParticle(63,
                  anEntity.sprite.position.x + 12, anEntity.sprite.position.y + 2,
                  0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1);
               
            }
             
            if ((anEntity.sprite.position.x > maxX) ||
              (anEntity.sprite.position.x < minX) ||
              (anEntity.sprite.position.y > maxY) ||
              (anEntity.sprite.position.y < minY))            
            {
              // if we go past any edge, become inactive
              // so the sprite can be respawned
              if ((anEntity != thePlayer) && (anEntity != theOrb))
                anEntity.die();
            }
          }
           
          if (anEntity.rotationSpeed != 0)
            anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds;
             
          if (anEntity.fadeAnim != 0)
          {
            anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds;
            if (anEntity.sprite.alpha <= 0.001)
            {
              anEntity.die();
            }
            else if (anEntity.sprite.alpha > 1)
            {
              anEntity.sprite.alpha = 1;
            }
          }
          if (anEntity.zoomAnim != 0)
          {
            anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds;
            anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds;
            if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0)
              anEntity.die();
          }
        }
      }
    }
  } // end class
} // end package

В приведенном выше коде мы не только обновляем позиции и повороты каждого спрайта, но также необязательно проверяем столкновения, обращаемся по орбите к другим объектам и «умираем» (становимся доступными для повторного использования в нашем пуле объектов), когда мы перемещаемся за пределы экрана постепенно исчезают до 100% невидимости или уменьшаются в масштабе до небытия.

Вот и все для обновления менеджера сущностей. Все, что остается в третьей версии нашей игры — это добавить несколько маленьких дополнений и обновить класс основной игры.


Просто для удовольствия, давайте реализуем простой и тонкий эффект параллакса с вертикальной прокруткой на нашем фоне. Мы изменим наш класс фона, чтобы отслеживать текущую вертикальную позицию игрока в процентах от высоты экрана.

Когда корабль движется, мы немного прокрутим фоновые спрайты в противоположном направлении, что придаст игре немного больше трехмерного ощущения. Откройте свой существующий GameBackground.asи внесите несколько изменений следующим образом.

Чтобы избежать путаницы, весь класс представлен здесь. Этот первый раздел со всеми начальными значениями остается неизменным с прошлого раза:

01
02
03
04
05
06
07
08
09
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com
 
// GameBackground.as
// A very simple batch of background stars that scroll
// with a subtle vertical parallax effect
 
package
{
  import flash.display.Bitmap;
  import flash.display3D.*;
  import flash.geom.Point;
  import flash.geom.Rectangle;
   
  public class GameBackground extends EntityManager
  {
    // how fast the stars move
    public var bgSpeed:int = -1;
    // the sprite sheet image
    public const bgSpritesPerRow:int = 1;
    public const bgSpritesPerCol:int = 1;
    [Embed(source="../assets/stars.gif")]
    public var bgSourceImage : Class;
     
    // since the image is larger thanthe screen we have some extra pixels to play with
    public var yParallaxAmount:Number = (512 - 400);
    public var yOffset:Number = 0;
 
    public function GameBackground(view:Rectangle)
    {
      // run the init functions of the EntityManager class
      super(view);
    }
     
    override public function createBatch(context3D:Context3D) : LiteSpriteBatch
    {
      var bgsourceBitmap:Bitmap = new bgSourceImage();
 
      // create a spritesheet with single giant sprite
      spriteSheet = new LiteSpriteSheet(bgsourceBitmap.bitmapData,
        bgSpritesPerRow, bgSpritesPerCol);
       
      // Create new render batch
      batch = new LiteSpriteBatch(context3D, spriteSheet);
       
      return batch;
    }
 
    override public function setPosition(view:Rectangle):void
    {
      // allow moving fully offscreen before looping around
      maxX = 256+512+512;
      minX = -256;
      maxY = view.height;
      minY = view.y;
      yParallaxAmount = (512 - maxY) / 2;
      yOffset = maxY / 2;
    }
     
    // for this test, create random entities that move
    // from right to left with random speeds and scales
    public function initBackground():void
    {
      trace("Init background...");
      // we need three 512x512 sprites
      var anEntity1:Entity = respawn(0)
      anEntity1 = respawn(0);
      anEntity1.sprite.position.x = 256;
      anEntity1.sprite.position.y = maxY / 2;
      anEntity1.speedX = bgSpeed;
      var anEntity2:Entity = respawn(0)
      anEntity2.sprite.position.x = 256+512;
      anEntity2.sprite.position.y = maxY / 2;
      anEntity2.speedX = bgSpeed;
      var anEntity3:Entity = respawn(0)
      anEntity3.sprite.position.x = 256+512+512;
      anEntity3.sprite.position.y = maxY / 2;
      anEntity3.speedX = bgSpeed;
    }

Параллакс — это слово, которое используется для описания того, как вещи движутся меньше, чем дальше вы их просматриваете. В этом случае наш фон будет немного прокручиваться по вертикали, чтобы дать тонкий эффект в ответ на движения игрока. Добавьте новую функцию yParallaxи обновите существующую updateпроцедуру следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    // scroll slightly up or down to give more parallax
    public function yParallax(OffsetPercent:Number = 0) : void
    {
      yOffset = (maxY / 2) + (-1 * yParallaxAmount * OffsetPercent);
    }
     
    // called every frame: used to update the scrolling background
    override public function update(currentTime:Number) : void
    {
      var anEntity:Entity;
       
      // handle all other entities
      for(var i:int=0; i<entityPool.length;i++)
      {
        anEntity = entityPool[i];
        if (anEntity.active)
        {
          anEntity.sprite.position.x += anEntity.speedX;
          anEntity.sprite.position.y = yOffset;
 
          if (anEntity.sprite.position.x >= maxX)
          {
            anEntity.sprite.position.x = minX;
          }
          else if (anEntity.sprite.position.x <= minX)
          {
            anEntity.sprite.position.x = maxX;
          }
        }
      }
    }
  } // end class
} // end package

Это все, что нам нужно сделать с фоновым классом. Далее, несколько новых звуковых эффектов и основной класс игры, и мы закончили на этой неделе!


Мы определенно хотим вызвать некоторые звуковые эффекты взрыва. Мы собираемся добавить три звуковых эффекта взрыва в нашу игру и случайным образом переключаться между ними во время игры.

Всякий раз, когда нашему менеджеру сущностей сообщают, что было обнаружено столкновение, он выбирает один из этих трех новых звуков взрыва для воспроизведения. Откройте свой существующий GameSound.asи внесите несколько изменений следующим образом.

01
02
03
04
05
06
07
08
09
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com
 
// GameSound.as
// A simple sound and music system for our game
 
package
{
 
import flash.media.Sound;
import flash.media.SoundChannel;
 
  public class GameSound
  {
     
    // to reduce .swf size these are mono 11khz
    [Embed (source = "../assets/sfxmusic.mp3")]
    private var _musicMp3:Class;
    private var _musicSound:Sound = (new _musicMp3) as Sound;
    private var _musicChannel:SoundChannel;
 
    [Embed (source = "../assets/sfxgun1.mp3")]
    private var _gun1Mp3:Class;
    private var _gun1Sound:Sound = (new _gun1Mp3) as Sound;
    [Embed (source = "../assets/sfxgun2.mp3")]
    private var _gun2Mp3:Class;
    private var _gun2Sound:Sound = (new _gun2Mp3) as Sound;
    [Embed (source = "../assets/sfxgun3.mp3")]
    private var _gun3Mp3:Class;
    private var _gun3Sound:Sound = (new _gun3Mp3) as Sound;
     
    [Embed (source = "../assets/sfxexplosion1.mp3")]
    private var _explode1Mp3:Class;
    private var _explode1Sound:Sound = (new _explode1Mp3) as Sound;
    [Embed (source = "../assets/sfxexplosion2.mp3")]
    private var _explode2Mp3:Class;
    private var _explode2Sound:Sound = (new _explode2Mp3) as Sound;
    [Embed (source = "../assets/sfxexplosion3.mp3")]
    private var _explode3Mp3:Class;
    private var _explode3Sound:Sound = (new _explode3Mp3) as Sound;
 
    // the different phaser shooting sounds
    public function playGun(num:int):void
    {
      switch (num)
      {
        break;
        break;
        break;
      }
    }
     
    // the looping music channel
    public function playMusic():void
    {
      trace("Starting the music...");
      // stop any previously playing music
      stopMusic();
      // start the background music looping
      _musicChannel = _musicSound.play(0,9999);
    }
     
    public function stopMusic():void
    {
      if (_musicChannel) _musicChannel.stop();
    }
 
    public function playExplosion(num:int):void
    {
      switch (num)
      {
        break;
        break;
        break;
      }
    }
     
  } // end class
} // end package

Использование трех разных звуков для одного и того же события поможет сделать вещи менее повторяющимися во время игры. Теперь, когда мы реализовали частицы, новые звуки, обнаружение столкновений, все, что осталось, — это убедиться, что все эти новые функции появятся в нашей игре.


Новые классные обновления, которые мы реализовали в нашей игре выше, также требуют небольших изменений в основном классе игровых документов. Откройте свой существующий Game.asи внесите несколько изменений следующим образом.

(Как и прежде, чтобы избежать путаницы, здесь представлен весь класс. Все начальные значения остаются такими же, как на прошлой неделе, за исключением пары строк кода, где мы добавляем некоторые новые свойства для отслеживания текущей временной метки и количества миллисекунд. истекший с предыдущего кадра. Мы также отправляем ссылку на звуковую систему нашему менеджеру, чтобы он имел доступ к нашим новым звуковым эффектам взрыва.)

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Stage3D Shoot-em-up Tutorial Part 3
// by Christer Kaitila - www.mcfunkypants.com
// Created for active.tutsplus.com
 
package
{
  [SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")]
 
  import flash.display3D.*;
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageQuality;
  import flash.display.StageScaleMode;
  import flash.events.Event;
  import flash.events.ErrorEvent;
  import flash.events.MouseEvent;
  import flash.geom.Rectangle;
  import flash.utils.getTimer;
     
  public class Main extends Sprite
  {
    // the keyboard control system
    private var _controls : GameControls;
    // don't update the menu too fast
    private var nothingPressedLastFrame:Boolean = false;
    // timestamp of the current frame
    public var currentTime:int;
    // for framerate independent speeds
    public var currentFrameMs:int;
    public var previousFrameTime:int;
     
    // player one's entity
    public var thePlayer:Entity;
    // movement speed in pixels per second
    public var playerSpeed:Number = 128;
    // timestamp when next shot can be fired
    private var nextFireTime:uint = 0;
    // how many ms between shots
    private var fireDelay:uint = 200;
     
    // main menu = 0 or current level number
    private var _state : int = 0;
    // the title screen batch
    private var _mainmenu : GameMenu;
    // the sound system
    private var _sfx : GameSound;
    // the background stars
    private var _bg : GameBackground;
     
    private var _entities : EntityManager;
    private var _spriteStage : LiteSpriteStage;
    private var _gui : GameGUI;
    private var _width : Number = 600;
    private var _height : Number = 400;
    public var context3D : Context3D;
     
    // constructor function for our game
    public function Main():void
    {
      if (stage) init();
      else addEventListener(Event.ADDED_TO_STAGE, init);
    }
     
    // called once flash is ready
    private function init(e:Event = null):void
    {
      _controls = new GameControls(stage);
      removeEventListener(Event.ADDED_TO_STAGE, init);
      stage.quality = StageQuality.LOW;
      stage.align = StageAlign.TOP_LEFT;
      stage.scaleMode = StageScaleMode.NO_SCALE;
      stage.addEventListener(Event.RESIZE, onResizeEvent);
      trace("Init Stage3D...");
      _gui = new GameGUI("Stage3D Shoot-em-up Tutorial Part 3");
      addChild(_gui);
      stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
      stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler);
      stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO);
      trace("Stage3D requested...");         
      _sfx = new GameSound();
    }
         
    // this is called when the 3d card has been set up
    // and is ready for rendering using stage3d
    private function onContext3DCreate(e:Event):void
    {
      trace("Stage3D context created! Init sprite engine...");
      context3D = stage.stage3Ds[0].context3D;
      initSpriteEngine();
    }
     
    // this can be called when using an old version of flash
    // or if the html does not include wmode=direct
    private function errorHandler(e:ErrorEvent):void
    {
      trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text);
    }
 
    protected function onResizeEvent(event:Event) : void
    {
      trace("resize event...");
       
      // Set correct dimensions if we resize
      _width = stage.stageWidth;
      _height = stage.stageHeight;
       
      // Resize Stage3D to continue to fit screen
      var view:Rectangle = new Rectangle(0, 0, _width, _height);
      if ( _spriteStage != null ) {
        _spriteStage.position = view;
      }
      if(_entities != null) {
        _entities.setPosition(view);
      }
      if(_mainmenu != null) {
        _mainmenu.setPosition(view);
      }
    }
     
    private function initSpriteEngine():void
    {
      // init a gpu sprite system
      var stageRect:Rectangle = new Rectangle(0, 0, _width, _height);
      _spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect);
      _spriteStage.configureBackBuffer(_width,_height);
       
      // create the background stars
      _bg = new GameBackground(stageRect);
      _bg.createBatch(context3D);
      _spriteStage.addBatch(_bg.batch);
      _bg.initBackground();
       
      // create a single rendering batch
      // which will draw all sprites in one pass
      var view:Rectangle = new Rectangle(0,0,_width,_height)
      _entities = new EntityManager(stageRect);
      _entities.createBatch(context3D);
      _entities.sfx = _sfx;
      _spriteStage.addBatch(_entities.batch);
       
      // create the logo/titlescreen main menu
      _mainmenu = new GameMenu(stageRect);
      _mainmenu.createBatch(context3D);
      _spriteStage.addBatch(_mainmenu.batch);
       
      // tell the gui where to grab statistics from
      _gui.statsTarget = _entities;
       
      // start the render loop
      stage.addEventListener(Event.ENTER_FRAME,onEnterFrame);
 
      // only used for the menu
      stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);  
      stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
    }

Вместо того, чтобы перемещать определенное количество каждого кадра независимо от того, какая частота кадров, мы теперь отслеживаем истекшее время и мультиплеерную скорость игрока, сколько бы времени не прошло с предыдущего кадра.

Мы также собираемся создавать новую частицу в каждом кадре сразу за кораблем игрока, как своего рода эффект «следа пара».

Наконец, в наших подпрограммах ввода мы будем отслеживать последний раз, когда пуля была запущена, и ждать короткое время между выстрелами, вместо того, чтобы новая пуля появлялась в каждом кадре.

Поэтому нам нужно сделать несколько очень незначительных обновлений в существующих логиках игроков. Продолжите редактирование Main.asследующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public function playerLogic(me:Entity):void
{
  me.speedY = me.speedX = 0;
  if (_controls.pressing.up)
    me.speedY = -playerSpeed;
  if (_controls.pressing.down)
    me.speedY = playerSpeed;
  if (_controls.pressing.left)
    me.speedX = -playerSpeed;
  if (_controls.pressing.right)
    me.speedX = playerSpeed;
     
  // keep on screen
  if (me.sprite.position.x < 0)
    me.sprite.position.x = 0;
  if (me.sprite.position.x > _width)
    me.sprite.position.x = _width;
  if (me.sprite.position.y < 0)
    me.sprite.position.y = 0;
  if (me.sprite.position.y > _height)
    me.sprite.position.y = _height;
     
  // leave a trail of particles
  _entities.particles.addParticle(63,
    me.sprite.position.x - 12,
    me.sprite.position.y + 2,
    0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5);
}
 
private function mouseDown(e:MouseEvent):void  
{
  trace('mouseDown at '+e.stageX+','+e.stageY);
  if (_state == 0) // are we at the main menu?
  {
    if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer()))
    { // if the above returns true we should start the game
      startGame();
    }
  }
}
 
private function mouseMove(e:MouseEvent):void  
{
  if (_state == 0) // are we at the main menu?
  {
    // select menu items via mouse
    if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY);
  }
}
 
// handle any player input
private function processInput():void
{
  if (_state == 0) // are we at the main menu?
  {
    // select menu items via keyboard
    if (_controls.pressing.down || _controls.pressing.right)
    {
      if (nothingPressedLastFrame)
      {
        _sfx.playGun(1);
        _mainmenu.nextMenuItem();
        nothingPressedLastFrame = false;
      }
    }
    else if (_controls.pressing.up || _controls.pressing.left)
    {
      if (nothingPressedLastFrame)
      {
        _sfx.playGun(1);
        _mainmenu.prevMenuItem();
        nothingPressedLastFrame = false;
      }
    }
    else if (_controls.pressing.fire)
    {
      if (_mainmenu.activateCurrentMenuItem(getTimer()))
      { // if the above returns true we should start the game
        startGame();
      }
    }
    else
    {
      // this ensures the menu doesn't change too fast
      nothingPressedLastFrame = true;
    }
  }
  else
  {
    // we are NOT at the main menu:
    // we are actually playing the game!
    // if enough time has passed, fire some bullets:
    if (_controls.pressing.fire)
    {
      // is it time to fire again?
      if (currentTime >= nextFireTime)
      {
        //trace("Fire!");
        nextFireTime = currentTime + fireDelay;
        _sfx.playGun(1);
        _entities.shootBullet(3);
      }
    }
  }
}
 
private function startGame():void
{
  trace("Starting game!");
  _state = 1;
  _spriteStage.removeBatch(_mainmenu.batch);
  _sfx.playMusic();
  // add the player entity to the game!
  thePlayer = _entities.addPlayer(playerLogic);    
}

Последний набор обновлений, которые мы должны сделать в нашей игре, — это «цикл рендеринга», который запускается каждый кадр в ответ на ENTER_FRAME событие.

Сначала мы измеряем количество времени, прошедшее с предыдущего кадра, и сохраняем количество миллисекунд, на которое мы будем умножать все виды значений движения и анимации, как здесь, так и в анимации частиц, где мы постепенно уменьшаем или масштабируем различные сущности, которые со временем становятся частью наших взрывов.

Далее мы сообщаем игровому фону, где находится игрок, относительно высоты экрана, чтобы у него был тонкий эффект вертикальной прокрутки параллакса.

Наконец, вместо того, чтобы порождать нового врага в каждом отдельном кадре, мы случайным образом порождаем нового врага в 10% случаев, так что остается немного больше передышки. Затем мы говорим менеджеру сущностей обновить игровую симуляцию и отрендерить все.

01
02
03
04
05
06
07
08
09
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
    // this function draws the scene every frame
    private function onEnterFrame(e:Event):void
    {
      try
      {
        // grab timestamp of current frame
        currentTime = getTimer();
        currentFrameMs = currentTime - previousFrameTime;
        previousFrameTime = currentTime;
         
        // erase the previous frame
        context3D.clear(0, 0, 0, 1);
         
        // for debugging the input manager, update the gui
        _gui.titleText = _controls.textDescription();
         
        // process any player input
        processInput();
 
        // scroll the background
        if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height);
        _bg.update(currentTime);
         
        // update the main menu titlescreen
        if (_state == 0)
          _mainmenu.update(currentTime);
         
        // keep adding more sprites - FOREVER!
        // this is a test of the entity manager's
        // object reuse "pool"
        if (Math.random() > 0.9)
          _entities.addEntity();
         
        // move/animate all entities
        _entities.update(currentFrameMs);
         
        // draw all entities
        _spriteStage.render();
 
        // update the screen
        context3D.present();
      }
      catch (e:Error)
      {
        // this can happen if the computer goes to sleep and
        // then re-awakens, requiring reinitialization of stage3D
        // (the onContext3DCreate will fire again)
      }
    }
  } // end class
} // end package

Были сделаны!Скомпилируйте ваш проект, исправьте все опечатки и запустите игру. Если у вас возникли проблемы с набранным кодом или вы хотите получить мгновенное удовлетворение всего в одном месте, помните, что вы можете скачать полный исходный код здесь . Вы должны увидеть что-то похожее на это:

Screenshot of this week's upgrades in action

Вот и все для учебника номер три в этой серии. Наша супероптимизированная Flash-игра Stage3D Shoot-em-up от Flash 11 наконец-то начинает казаться забавной ! Настройтесь на следующую неделю для первого из трех учебных пособий Премиум, чтобы наблюдать, как игра медленно превращается в невероятно играемую, шелковисто-гладкую стрельбу из кадров в 60 кадров в секунду.

В следующем уроке мы реализуем искусственный интеллект противника (создав простой класс искусственного интеллекта), чтобы плохие парни больше не двигались по прямой линии. Они также будут стрелять в вас и представлять больше проблем. Мы также добавим визуальный интерес к игре, запрограммировав фоновую систему местности. Представьте себе взрывных инопланетян среди астероидных полей, огромных космических станций, планет и галактик. Это поможет разнообразить игру по сравнению с простым, нескончаемым звездным полем, которое мы сейчас используем в качестве фона.

В будущих версиях нашей игры (части пятая и шестая) мы будем программировать здоровье, счет и симпатичный графический интерфейс HUD (head-up-display) для хранения этих счетчиков. Мы реализуем условия прохождения игры и победы, сложность и игровой баланс (чтобы некоторые вражеские корабли уничтожали более одного выстрела). Мы будем добавлять различные улучшения оружия и бонусы, которые меняют типы пуль, которые вы стреляете, и то, что спутник «шар» имеет на вашем корабле, и, наконец, битва с боссом!

К концу этой серии из шести частей у вас будет законченная, играбельная, высокопроизводительная игра-стрелялка, которая выглядит отточенной и законченной и имеет начало, середину и конец. Я надеюсь, что вы присоединитесь ко мне полностью.

Я хотел бы услышать от вас относительно этого урока. Я искренне приветствую всех читателей, которые могут связаться со мной через твиттер: @McFunkypants , мой блог mcfunkypants.com или в Google+ в любое время. В частности, я хотел бы видеть игры, которые вы делаете с использованием этого кода, и я всегда ищу новые темы для написания будущих уроков. Свяжитесь со мной в любое время.

Если вы уже пользовались этими уроками, возможно, вы хотели бы узнать больше о Stage3D? Если так, то почему бы не купить мою книгу Stage3d ! знак равно

Удачи и получайте удовольствие!