В этой серии учебных пособий (частично бесплатно, частично Premium) мы создаем высокопроизводительную 2D-съемку с использованием нового аппаратно-ускоренного Stage3D
рендеринга Stage3D
. В этой части мы добавим приятную глазу систему частиц, эффект параллакса, независимые от частоты кадров таймеры игрового цикла и обнаружение столкновений.
Окончательный результат предварительного просмотра
Давайте посмотрим на конечный результат, к которому мы будем стремиться: демо-ролик с аппаратным ускорением, который включает в себя все, начиная с первой и второй частей этой серии, а также эффективную систему частиц для множества приятных глаз, частоту кадров. независимые таймеры для последовательного движения, тонкий фоновый эффект параллакса, способность объектов вращаться вокруг друг друга и система обнаружения столкновений, способная обрабатывать тонны объектов.
Проверьте это: каждый взрыв немного отличается!
Введение: Добро пожаловать на третий уровень!
Давайте продолжим делать шутер с боковой прокруткой, вдохновленный ретро аркадными играми, такими как R-Type или Gradius в ActionScript.
В первой части этой серии мы реализовали базовый движок 2D-спрайтов, который обеспечивает отличную производительность за счет использования аппаратного рендеринга Stage3D, а также нескольких оптимизаций.
Во второй части мы реализовали титульный экран, главное меню, звуковые эффекты и музыку, а также систему ввода, чтобы игрок мог управлять своим космическим кораблем с помощью клавиатуры.
В этой части мы собираемся добавить все приятное на глаз: систему частиц, полную искр, летящих обломков, ударных волн, следов пожара двигателя и множества взрывов.
В предыдущих версиях наша игра имела блокировку частоты кадров и работала медленнее на старых компьютерах. Чтобы обеспечить одинаковую синхронизацию для всего, независимо от частоты кадров, мы собираемся изменить все единицы моделирования движения и анимации для учета точного количества миллисекунд, прошедших с предыдущего кадра. Таким образом, независимо от того, работаете ли вы со скоростью 60 кадров в секунду на современном игровом оборудовании или на старом нетбуке вашей бабушки, сам игровой опыт будет идентичным.
Наконец, мы собираемся запрограммировать обнаружение столкновений, которое требуется практически во всех играх, которые вы можете себе представить. Для того, чтобы вызвать взрывы, мы должны быть в состоянии определить, когда пуля попала в противника. Пока мы на этом, мы собираемся добавить немного дополнительного пизацза, просто для удовольствия, в том числе эффект вертикального параллакса на фоне звездного поля и спутник «power orb», вдохновленный R-Type, который окружает корабль игрока ,
Шаг 1: Откройте ваш существующий проект
Если у вас его еще нет, обязательно загрузите исходный код из второй части . Откройте файл проекта в FlashDevelop и будьте готовы обновить игру!
Этот исходный код будет работать в любом другом компиляторе AS3, от CS6 до Flash Builder. Если вы используете FB, обязательно -default-frame-rate 60
« -default-frame-rate 60
» в параметры компилятора, чтобы обеспечить максимальную производительность.
Шаг 2: Начните вечеринку!
Мы собираемся воспользоваться хорошо оптимизированными внутренними компонентами вашего класса менеджера сущностей с прошлого раза, добавив в него простую систему частиц, которая все еще использует все те же базовые функции сущностей и таблиц спрайтов.
Таким образом, мы по-прежнему визуализируем спрайты всей игры (корабли, пули и все) в одном пакете геометрии, используя одну текстуру. Поэтому большая часть симуляции частиц будет обрабатываться так же, как мы в настоящее время управляем движением врагов. Самое главное, мы собираемся свести к минимуму количество вызовов отрисовки, вставляя частицы в нашу существующую партию спрайтов.
Первое, что нам нужно сделать, это определить несколько интересных эффектов. Мы собираемся немного повеселиться и создать несколько круто выглядящих эффектов, таких как расширяющееся кольцо голубоватой энергии («ударная волна»), куча разных огненных шаров, которые вращаются и исчезают, некоторые быстро движущиеся искры, которые остаются красивыми и яркий и какой-то металлический обломки корпуса космического корабля.
Создайте новый файл в вашем проекте под названием 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()
будет вызываться в ответ на обнаружение столкновения между пулей и противником во время игры. Он породит ударную волну, немного мусора, огромный огненный шар, несколько маленьких вращающихся огненных шаров и кучу летящих искр. Давайте определим эти различные эффекты дальше — но прежде чем мы сможем, нам нужна общая функция создания частиц.
Шаг 3: Определите основную частицу
Продолжайте добавлять в 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;
}
|
Шаг 4: Eye Candy!
Последний шаг в создании нашего удивительного нового класса систем частиц — создание функций для различных специальных эффектов, используемых нашими взрывами в игре, как указано выше.
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
|
Вот и все для нашей упрощенной системы частиц. Как вы можете видеть, мы определяем только поведение каждого типа визуального эффекта в этом классе: работа по анимации каждой частицы выполняется одновременно с анимацией всех других сущностей с помощью нашего класса менеджера сущностей, который мы обновим следующим. Во-первых, однако, нам нужно добавить несколько новых свойств в наш базовый класс сущностей для поддержки этого нового поведения.
Шаг 5: Обновите класс сущности
Нам нужно добавить несколько новых свойств в наш базовый класс сущностей. Так как мы собираемся сделать так, чтобы корабль игрока выводил непрерывный поток огня от двигателей, например, мы хотим иметь возможность хранить эту новую информацию для каждой сущности. Кроме того, некоторые новые свойства, которые относятся к моделированию частиц и обнаружению столкновений, должны быть определены здесь. Откройте существующий 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;
}
|
Как вы можете видеть, большинство из них такие же, как в предыдущем уроке. Когда сущность «умирает» (то есть становится невидимой и доступной для повторного использования нашим оптимизированным пулом повторного использования сущностей), мы отключаем некоторые из этих новых значений, чтобы следующая сущность, повторно использующая этот спрайт, не принимала нежелательных поведения.
Шаг 6: внедрить обнаружение столкновений
Это самая важная часть этого урока. Мы собираемся превратить нашу техническую демонстрацию из простой графической демонстрации в нечто, похожее на настоящую игру, путем реализации нашей процедуры обнаружения столкновений.
Для простоты (и быстрой) пока мы не собираемся реализовывать столкновение ограничивающего прямоугольника, столкновение прямоугольника с сферой или луч, которое часто используется в сложных физических движках. Мы сконцентрируемся на том, что нужно нашей игре, и это простой способ определить, находится ли что-то «достаточно близко» к чему-то другому, чтобы вызвать столкновение (и последующий взрыв).
Обнаружение столкновения сфер просто проверяет, находится ли один круг в радиусе другого. Таким образом, мы можем дать разным объектам размер «радиуса» и проверить, как далеко находятся их центральные точки, чтобы определить, перекрываются ли эти два круга. Чтобы заставить нашу игру работать еще быстрее, мы будем выполнять эту математику только в том случае, если обе сущности настроены на столкновение.
В качестве дальнейшей оптимизации вместо использования встроенной функции 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
. Теперь в нашей игре есть сущности, которые хранят статистику, необходимую для этого урока, и могут рассчитывать столкновения.
Шаг 7: Обновите Entity Manager
В диспетчере сущностей появилось много новых дополнений, которые добавляют возможность запрашивать обнаружение столкновений, запускать звуковые эффекты, добавлять частицы и многое другое. Откройте существующий 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 колеблется.
Шаг 8: Обновите Spritesheet
В приведенном выше коде мы сохраняем постоянные значения для позиций различных спрайтов в нашем изображении таблицы спрайтов. Мы добавили несколько новых видов спрайтов, которые относятся к частицам, и реорганизовали таблицу спрайтов, чтобы наши незваные враги появлялись только из первых нескольких рядов. Эти изменения привели к небольшим изменениям в нашей таблице спрайтов:
Щелкните правой кнопкой мыши, чтобы загрузить.
Как видите, теперь у нас есть несколько дополнительных спрайтов для взрывов, ударных волн, искр и осколков.
Шаг 9: Обновите Inits
Продолжая 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
который сообщает менеджеру сущности, является ли возвращаемый им спрайт совершенно новым. или не. Таким образом, в наших списках хранится только одна ссылка на каждый спрайт.
Шаг 10: Быстрый Случайный
В качестве еще одной небольшой оптимизации вместо того, чтобы использовать 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);
}
|
Шаг 11: Обнови плеер
Продолжая 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;
}
|
Шаг 12: Пули и враги
Функции 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);
}
|
Шаг 13: Удобные математические утилиты
Далее нам нужно реализовать некоторые вспомогательные математические функции, используемые в подпрограммах порождения выше. Они очень удобны и могут быть использованы во многих отношениях в будущем. Поскольку 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);
}
|
Шаг 14: Обнаружение столкновения
Каждый кадр, когда каждый объект перемещается в новое местоположение, те, у которых свойство 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;
}
|
Шаг 15: Обновление симуляции
Последний шаг в обновлении нашего скоростного 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% невидимости или уменьшаются в масштабе до небытия.
Вот и все для обновления менеджера сущностей. Все, что остается в третьей версии нашей игры — это добавить несколько маленьких дополнений и обновить класс основной игры.
Шаг 16: Обнови фон
Просто для удовольствия, давайте реализуем простой и тонкий эффект параллакса с вертикальной прокруткой на нашем фоне. Мы изменим наш класс фона, чтобы отслеживать текущую вертикальную позицию игрока в процентах от высоты экрана.
Когда корабль движется, мы немного прокрутим фоновые спрайты в противоположном направлении, что придаст игре немного больше трехмерного ощущения. Откройте свой существующий 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; }
|
Шаг 17: Фоновый параллакс
Параллакс — это слово, которое используется для описания того, как вещи движутся меньше, чем дальше вы их просматриваете. В этом случае наш фон будет немного прокручиваться по вертикали, чтобы дать тонкий эффект в ответ на движения игрока. Добавьте новую функцию 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 |
Это все, что нам нужно сделать с фоновым классом. Далее, несколько новых звуковых эффектов и основной класс игры, и мы закончили на этой неделе!
Шаг 18: Время бума!
Мы определенно хотим вызвать некоторые звуковые эффекты взрыва. Мы собираемся добавить три звуковых эффекта взрыва в нашу игру и случайным образом переключаться между ними во время игры.
Всякий раз, когда нашему менеджеру сущностей сообщают, что было обнаружено столкновение, он выбирает один из этих трех новых звуков взрыва для воспроизведения. Откройте свой существующий 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 |
Использование трех разных звуков для одного и того же события поможет сделать вещи менее повторяющимися во время игры. Теперь, когда мы реализовали частицы, новые звуки, обнаружение столкновений, все, что осталось, — это убедиться, что все эти новые функции появятся в нашей игре.
Шаг 19: Обновите основной игровой класс
Новые классные обновления, которые мы реализовали в нашей игре выше, также требуют небольших изменений в основном классе игровых документов. Откройте свой существующий 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); }
|
Шаг 20: Независимость FPS
Вместо того, чтобы перемещать определенное количество каждого кадра независимо от того, какая частота кадров, мы теперь отслеживаем истекшее время и мультиплеерную скорость игрока, сколько бы времени не прошло с предыдущего кадра.
Мы также собираемся создавать новую частицу в каждом кадре сразу за кораблем игрока, как своего рода эффект «следа пара».
Наконец, в наших подпрограммах ввода мы будем отслеживать последний раз, когда пуля была запущена, и ждать короткое время между выстрелами, вместо того, чтобы новая пуля появлялась в каждом кадре.
Поэтому нам нужно сделать несколько очень незначительных обновлений в существующих логиках игроков. Продолжите редактирование 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); }
|
Шаг 21: обновить цикл рендеринга
Последний набор обновлений, которые мы должны сделать в нашей игре, — это «цикл рендеринга», который запускается каждый кадр в ответ на 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 |
Были сделаны!Скомпилируйте ваш проект, исправьте все опечатки и запустите игру. Если у вас возникли проблемы с набранным кодом или вы хотите получить мгновенное удовлетворение всего в одном месте, помните, что вы можете скачать полный исходный код здесь . Вы должны увидеть что-то похожее на это:
Часть третья завершена: готовьтесь к четвертому уровню!
Вот и все для учебника номер три в этой серии. Наша супероптимизированная Flash-игра Stage3D Shoot-em-up от Flash 11 наконец-то начинает казаться забавной ! Настройтесь на следующую неделю для первого из трех учебных пособий Премиум, чтобы наблюдать, как игра медленно превращается в невероятно играемую, шелковисто-гладкую стрельбу из кадров в 60 кадров в секунду.
В следующем уроке мы реализуем искусственный интеллект противника (создав простой класс искусственного интеллекта), чтобы плохие парни больше не двигались по прямой линии. Они также будут стрелять в вас и представлять больше проблем. Мы также добавим визуальный интерес к игре, запрограммировав фоновую систему местности. Представьте себе взрывных инопланетян среди астероидных полей, огромных космических станций, планет и галактик. Это поможет разнообразить игру по сравнению с простым, нескончаемым звездным полем, которое мы сейчас используем в качестве фона.
В будущих версиях нашей игры (части пятая и шестая) мы будем программировать здоровье, счет и симпатичный графический интерфейс HUD (head-up-display) для хранения этих счетчиков. Мы реализуем условия прохождения игры и победы, сложность и игровой баланс (чтобы некоторые вражеские корабли уничтожали более одного выстрела). Мы будем добавлять различные улучшения оружия и бонусы, которые меняют типы пуль, которые вы стреляете, и то, что спутник «шар» имеет на вашем корабле, и, наконец, битва с боссом!
К концу этой серии из шести частей у вас будет законченная, играбельная, высокопроизводительная игра-стрелялка, которая выглядит отточенной и законченной и имеет начало, середину и конец. Я надеюсь, что вы присоединитесь ко мне полностью.
Я хотел бы услышать от вас относительно этого урока. Я искренне приветствую всех читателей, которые могут связаться со мной через твиттер: @McFunkypants , мой блог mcfunkypants.com или в Google+ в любое время. В частности, я хотел бы видеть игры, которые вы делаете с использованием этого кода, и я всегда ищу новые темы для написания будущих уроков. Свяжитесь со мной в любое время.
Если вы уже пользовались этими уроками, возможно, вы хотели бы узнать больше о Stage3D? Если так, то почему бы не купить мою книгу Stage3d ! знак равно
Удачи и получайте удовольствие!