Готовый перевод Adobe Flash 11 Stage3D (Molehill) game programming (Chapter 6 - 10) / Программирование игр в Adobe Flash 11 Stage3D (Molehill) (Глава 6 - 10): Мир, наполненный действием

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

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

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

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

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

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

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

// игровой класс действующего лица версия 1.1
// сущность которая может перемещаться, стрелять, создавать частицы
// запускать звуки и обнаруживать столкновения
//
package
{
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import flash.display3D.Context3D;
import flash.display3D.Program3D;
import flash.display3D.textures.Texture;
import flash.media.Sound;
public class GameActor extends Stage3dEntity
{
// связанные с игрой состояния
public var name:String = ''; // уникальный
public var classname:String = ''; // не уникальный

public var owner:GameActor; // так вы не можете стрелять сами
public var touching:GameActor; // последнее столкновение обнаружено
public var active:Boolean = true; // анимировать?
public var visible:Boolean = true; //рендерить?
public var health:Number = 1; //когда ноль, умер
public var damage:Number = 250; // связана с другими
public var points:Number = 25; // зарабатываем очки, если разрушено
public var collides:Boolean = false; // проверять столкновения?
public var collidemode:uint = 0; // 0=sphere, 1=aabb
public var radius:Number = 1; // используется для столкновения сфер
public var aabbMin:Vector3D = new Vector3D(1, 1, 1, 1);
public var aabbMax:Vector3D = new Vector3D(1, 1, 1, 1);

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

Время для действий - расширяем свойства действующего лица
Давайте добавим ещё несколько свойств, чтобы действительно конкретизировать наш игровой класс, следующим образом:
//функции колбэков
public var runConstantly:Function;
public var runConstantlyDelay:uint = 1000;
public var runWhenNoHealth:Function;
public var runWhenMaxAge:Function;
public var runWhenCreated:Function;
// переменные, связанные со временем
public var age:uint = 0;
public var ageMax:uint = 0;
public var stepCounter:uint = 0;
// переменные анимации в единицах в СЕКУНДУ (1000ms)
public var posVelocity:Vector3D;
public var rotVelocity:Vector3D;
public var scaleVelocity:Vector3D;
public var tintVelocity:Vector3D;
// автоматически стреляет пулями когда враг поблизости
public var bullets:GameActorpool;
public var shootName:String = '';
public var shootDelay:uint = 4000;

public var shootNext:uint = 0;
public var shootRandomDelay:Number = 2000;
public var shootDist:Number = 100;
public var shootAt:GameActor = null;
public var shootVelocity:Number = 50;
public var shootSound:Sound;
// частицы создания
public var particles:GameParticlesystem;
public var spawnConstantly:String = '';
public var spawnConstantlyDelay:uint = 0;
public var spawnConstantlyNext:uint = 0;
public var spawnWhenNoHealth:String = '';
public var spawnWhenMaxAge:String = '';
public var spawnWhenCreated:String = '';
// звуковые эффекты
public var soundConstantlyDelay:uint = 1000;
public var soundConstantlyNext:uint = 0;
public var soundConstantly:Sound;
public var soundWhenNoHealth:Sound;
public var soundWhenMaxAge:Sound;
public var soundWhenCreated:Sound;

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

Время для действий - программируем конструктор класса GameActor
Давайте продолжим и установим новый класс и всю функциональность, обещанную этими новыми свойствами.
public function GameActor(mydata:Class = null,
mycontext:Context3D = null,
myshader:Program3D = null,
mytexture:Texture = null,
modelscale:Number = 1,
flipAxis:Boolean = true,
flipTexture: Boolean = true)
{
super(mydata, mycontext, myshader, mytexture,
modelscale, flipAxis, flipTexture);
}

Что же произошло?
Конструктор класса в предыдущем коде просто запускает оригинальную версию конструктора, который определён в нашем старом классе Stage3dEntity. Это сделано вызовом "super", который ссылается на конструктор класса, который мы расширили (Stage3dEntity). Как всегда обратите внимание, что параметры flipAxis и flipTexture, которые инструктируют OBJfile парсер перевёрнуты ли или нет данные в вашей модели, что обычно случается только в старых экспортёрах, таких как тот, что используется в 3D Studio Max 9. Более новые версии как правило не нуждаются в том, чтобы это было установлено на TRUE, так что если вы имеете проблемы с вашей моделью, которая выглядит "вывернутой наизнанку", попробуйте различные значения для них и ваша проблема исчезнет.

Время для действий - создание функции пошаговой анимации
Теперь мы можем создать пошаговую функцию, которая запускается каждый кадр в нашем цикле рендеринга следующим образом:
public function step(ms:uint):void
{
if (!active) return;
age += ms;
stepCounter++;
if (health <= 0)
{
//trace(name + " out of health.");
if (particles && spawnWhenNoHealth)
{
trace(name + " exploding into " + spawnWhenNoHealth);
var spawnxform:Matrix3D = new Matrix3D();
spawnxform.position = position.clone();
//particles.spawn(spawnWhenNoHealth,transform);
particles.spawn(spawnWhenNoHealth,spawnxform,5555,0,10);
}
if (soundWhenNoHealth)
soundWhenNoHealth.play();
if (runWhenNoHealth != null)
runWhenNoHealth();
die();
return;
}
if ((ageMax != 0) && (age >= ageMax))
{
//trace(name + " old age.");

if (particles && spawnWhenMaxAge)
particles.spawn(spawnWhenMaxAge, transform);
if (soundWhenMaxAge)
soundWhenMaxAge.play();
if (runWhenMaxAge != null)
runWhenMaxAge();
die();
return;
}

Что же произошло?
Предыдущий код просто обновляет возраст действующего лица и выполняет всё, что требуется для этого, если был установлен максимальный возраст. Легко убедиться, что пули, например, не летят вперёд вечно, если они ни с чем ни сталкиваются. Если действующее лицо умирает (кончается здоровье), тогда мы, по желанию, генерируем систему частиц и запускаем функцию обратного вызова, если это было установлено. Эта функция обратного вызова может быть использована, чтобы запустить звук, начислить игроку очки или оповестить об окончании уровня.

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

if (posVelocity)
{
x += posVelocity.x * (ms / 1000);
y += posVelocity.y * (ms / 1000);
z += posVelocity.z * (ms / 1000);
}
if (rotVelocity)
{
rotationDegreesX += rotVelocity.x * (ms / 1000);
rotationDegreesY += rotVelocity.y * (ms / 1000);
rotationDegreesZ += rotVelocity.z * (ms / 1000);
}
if (scaleVelocity)
{
scaleX += scaleVelocity.x * (ms / 1000);
scaleY += scaleVelocity.y * (ms / 1000);
scaleZ += scaleVelocity.z * (ms / 1000);
}

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

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

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

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

// может генерироваться новую частица
if (visible && particles && spawnConstantlyDelay > 0)
{
if (spawnConstantly != '')
{
if (age >= spawnConstantlyNext)
{
//trace("actor spawn " + spawnConstantly);
spawnConstantlyNext =
age + spawnConstantlyDelay;
particles.spawn(spawnConstantly, transform);
}
}
}
// может вызываться звук
if (visible && soundConstantlyDelay > 0)

{
if (soundConstantly)
{
if (age >= soundConstantlyNext)
{
soundConstantlyNext =
age + soundConstantlyDelay;
soundConstantly.play();
}
}
}

Время для действий - стреляем в врагов
"выстрел" ИИ - то же самое, что и выше, но с небольшой дополнительной сложностью: действующие лица будут стрелять только во врагов, которые находятся в пределах определённой ограниченной дистанции, если враг был обнаружен.
Продолжим, добавив в функцию step() следующее:

// может быть "выстрел" (порождается игроком)
if (visible && bullets && (shootName != ''))
{
var shouldShoot:Boolean = false;
if (age >= shootNext)
{
shootNext = age + shootDelay +
(Math.random() * shootRandomDelay);
if (shootDist < 0)
shouldShoot = true;
else if (shootAt &&
(shootDist > 0) &&
(Vector3D.distance(
position, shootAt.position) <= shootDist))
{
shouldShoot = true;
}
if (shouldShoot)
{
var b:GameActor =
bullets.spawn(shootName, transform);
// запоминаем кому эта пуля принадлежит
b.owner = this;

// целимся на врага?
if (shootAt)
{
b.transform.pointAt(
shootAt.transform.position);
b.rotationDegreesY -= 90;
b.posVelocity =
b.transform.position.subtract(
shootAt.transform.position);
b.posVelocity.normalize();
b.posVelocity.negate();
b.posVelocity.scaleBy(shootVelocity);
}
// иначе мы просто стреляем куда угодно
// направление было дано во время генерации
// когда игра установила posVelocity
if (shootSound)
shootSound.play();
}
}
}
} // конец функции step

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

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

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

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

public function cloneactor():GameActor
{
var myclone:GameActor = new GameActor();
updateTransformFromValues();
myclone.transform = this.transform.clone();
myclone.updateValuesFromTransform();
myclone.mesh = this.mesh;
myclone.texture = this.texture;
myclone.shader = this.shader;
myclone.vertexBuffer = this.vertexBuffer;
myclone.indexBuffer = this.indexBuffer;
myclone.context = this.context;
myclone.polycount = this.polycount;
myclone.blendSrc = this.blendSrc;
myclone.blendDst = this.blendDst;
myclone.cullingMode = this.cullingMode;
myclone.depthTestMode = this.depthTestMode;
myclone.depthTest = this.depthTest;
myclone.depthDraw = this.depthDraw;
// связанные с игрой состояния
myclone.name = this.name;
myclone.classname = this.classname;
myclone.owner = this.owner;

myclone.active = this.active;
myclone.visible = this.visible;
myclone.health = this.health;
myclone.damage = this.damage;
myclone.points = this.points;
myclone.collides = this.collides;
myclone.collidemode = this.collidemode;
myclone.radius = this.radius;
myclone.aabbMin = this.aabbMin.clone();
myclone.aabbMax = this.aabbMax.clone();

// функция обратного вызова
myclone.runConstantly = this.runConstantly;
myclone.runConstantlyDelay = this.runConstantlyDelay;
myclone.runWhenNoHealth = this.runWhenNoHealth;
myclone.runWhenMaxAge = this.runWhenMaxAge;
myclone.runWhenCreated = this.runWhenCreated;
// связанные с временем переменные
myclone.age = this.age;
myclone.ageMax = this.ageMax;
myclone.stepCounter = this.stepCounter;
// связанные с анимацией переменные - в мс.
myclone.posVelocity = this.posVelocity;
myclone.rotVelocity = this.rotVelocity;
myclone.scaleVelocity = this.scaleVelocity;
myclone.tintVelocity = this.tintVelocity;
// пули
myclone.bullets = this.bullets;
myclone.shootName = this.shootName;
myclone.shootDelay = this.shootDelay;
myclone.shootNext = this.shootNext;
myclone.shootRandomDelay = this.shootRandomDelay;
myclone.shootDist = this.shootDist;

myclone.shootAt = this.shootAt;
myclone.shootVelocity = this.shootVelocity;
myclone.shootSound = this.shootSound;
// порождаемые частицы
myclone.particles = this.particles;
myclone.spawnConstantly = this.spawnConstantly;
myclone.spawnConstantlyDelay = this.spawnConstantlyDelay;
myclone.spawnConstantlyNext = this.spawnConstantlyNext;
myclone.spawnWhenNoHealth = this.spawnWhenNoHealth;
myclone.spawnWhenMaxAge = this.spawnWhenMaxAge;
myclone.spawnWhenCreated = this.spawnWhenCreated;
// звуковые эффекты
myclone.soundConstantlyDelay = this.soundConstantlyDelay;
myclone.soundConstantlyNext = this.soundConstantlyNext;
myclone.soundConstantly = this.soundConstantly;
myclone.soundWhenNoHealth = this.soundWhenNoHealth;
myclone.soundWhenMaxAge = this.soundWhenMaxAge;
myclone.soundWhenCreated = this.soundWhenCreated;
myclone.active = true;

myclone.visible = true;
return myclone;
}

Время для действий - управляем смертью действующего лица.
Функция смерти чрезвычайно проста и вызывается когда действующее лицо потратило всё здоровье или умирает "от старости". Так как мы будет в конечном счёте создавать "бассейн" действующих лиц, которые могут быть повторно использован во время игры, всё, что нам нужно сделать тут это "выключить" действующее лицо, так что в будущем игра сможет повторно использовать его. Этим способом, так как игрок разрушает вражеские корабли, например, будущие враги могут приходить из повторно используемого бассейна без дальнейшей инициализации или потребления памяти.
{
//trace(name + " dies!");
active = false;
visible = false;
}

Время для действий - пересоздаём действующее лицо
Функция перерождения очень похожа на ту, что ту запрограммировали для нашего класса частиц. Она сбрасывает несколько ключевых свойств и заставляет до этого "мёртвые" или неактивные частицы возвращаться к жизни готовыми к повторному использованию. Как упоминалось ранее, это то, как бассейн действующих лиц сможет повторно использовать "мёртвое" действующее лицо, возвращая его обратно к "жизни":
public function respawn(pos:Matrix3D = null):void

{
age = 0;
stepCounter = 0;
active = true;
visible = true;
// не стреляем сразу
shootNext = Math.random() * shootRandomDelay;
if (pos)
{
transform = pos.clone();
}
if (soundWhenCreated)
soundWhenCreated.play();
if (runWhenCreated != null)
runWhenCreated();
if (particles && spawnWhenCreated)
particles.spawn(spawnWhenCreated, transform);
//trace("Respawned " + name + " at " + posString());

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

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

//использованный для обнаружения столкновений коллбэк выполненный в бассейне GameActor
public function colliding(checkme:GameActor):GameActor
{
if (collidemode == 0)
{
if (isCollidingSphere(checkme))
return checkme;
else
return null;
}
else
{
if (isCollidingAabb(checkme))
return checkme;
else
return null;
}
}

Время для действий - обнаруживаем столкновений двух сфер.
Наша простая игра не должна делать ничего больше, кроме как обнаруживать как близко одно действующее лицо к другому основываясь на позиции трансформации и их радиусе. Это называется обнаружением столкновений двух сфер. Это быстро, просто для понимания и прекрасно подходит для примера shoot-em-up игры, которую мы создаём.

//простое столкновение двух сфер
public function isCollidingSphere(checkme:GameActor):Boolean
{
// никогда не сталкивается с собой
if (this == checkme) return false;
// только проверяет, если эти сферы сталкиваемые
if (!collides || !checkme.collides) return false;
// don't check your own bullets
if (checkme.owner == this) return false;
// не проверять, если нет радиуса
if (radius == 0 || checkme.radius == 0) return false;
var dist:Number =
Vector3D.distance(position, checkme.position);
if (dist <= (radius+checkme.radius))
{
// trace("Collision detected at distance="+dist);
touching = checkme; // remember who hit us
return true;
}
// по умолчанию: слишком далеко
// trace("No collision. Dist = "+dist);
return false;
}

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

//выровненное по осям ограничительного бокса определение столкновений
//не используется в примере игры, но здесь для удобства
private function aabbCollision(
min1:Vector3D, max1:Vector3D,
min2:Vector3D, max2:Vector3D ):Boolean
{
if ( min1.x > max2.x ||
min1.y > max2.y ||
min1.z > max2.z ||
max1.x < min2.x ||
max1.y < min2.y ||
max1.z < min2.z )
{
return false;
}
return true;
}
public function isCollidingAabb(checkme:GameActor):Boolean
{
// никогда не сталкивается с собой
if (this == checkme) return false;
// только проверяет, могут ли эти формы сталкиваться
if (!collides || !checkme.collides) return false;
// не проверяйте ваши собственные пули
if (checkme.owner == this) return false;
// не проверять если нет данных aabb
if (aabbMin == null ||

aabbMax == null ||
checkme.aabbMin == null ||
checkme.aabbMax == null)
return false;
if (aabbCollision(
position + aabbMin,
position + aabbMax,
checkme.position + checkme.aabbMin,
checkme.position + checkme.aabbMax))
{
touching = checkme; // remember who hit us
return true;
}
// trace("No collision.");
return false;
}
} // конец пакета
} // конец класса

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

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

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

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

//Игровой бассейн частиц версия 1.2
//создаёт бассейн действующих лиц врагов по требованию
//и повторно использует неактивные по мере возможности
package
{
import flash.utils.Dictionary;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import GameActor;
public class GameActorpool
{
// список строковых имён каждого известного вида
private var allNames:Vector.<String>;

//содержит одно исходное действующее лицо для каждого вида
private var allKinds:Dictionary;
// содержит много клонированных действующих лиц различных видов
private var allActors:Dictionary;
// временные переменные - используются часто
private var actor:GameActor;
private var actorList:Vector.<GameActor>;
// используется только для состояний
public var actorsCreated:uint = 0;
public var actorsActive:uint = 0;
public var totalpolycount:uint = 0;
public var totalrendered:uint = 0;
// так мы можем поставить на паузу любые работающие действующие лица
//как группу для эффективности
public var active:Boolean = true;
// так мы можем скрыть части карты, если требуется
// прекрасно подходит для комнат/порталов/pvs/lod
public var visible:Boolean = true;
// конструктор класса
public function GameActorpool()
{
trace("Actor pool created.");
allKinds = new Dictionary();

allActors = new Dictionary();
allNames = new Vector.<String>();
}
Предыдущий код определяет такие вещи как список действующих лиц и "типы" действующих лиц, так же как и способы отслеживать статистику или включить и выключить целый бассейн в один вызов тем же способом, которым вы можете сделать отдельное действующее лицо невидимым или неактивным. В функции-конструкторе класса мы просто инициализируем список, который нам понадобится позже.

Время для действий - определяем родителя клона
Первая важная функция, которую мы должны реализовать, это механизм для определения повторно используемых "источников клонов" конкретного типа действующего лица. Например, вы могли бы установить действующее лицо "пуля" или "камень", дайте ему подходящее имя и, затем, используйте его позднее в качестве базового родителя для множественного копирования будущих клонов.
//имя конкретного вида частиц
public function defineActor(
name:String, cloneSource:GameActor):void
{

trace("New actor type defined: " + name);
allKinds[name] = cloneSource;
allNames.push(name);
}
Анимируем весь бассейн действующих лиц
Теперь мы должны реализовать функцию step() бассейна GameActor, которую наш игровой движок будет вызывать каждый кадр. Она будет будет обходить все известные действующие лица и анимировать их один за другим.

public function step(ms:uint,
collisionDetection:Function = null,
collisionReaction:Function = null):void
{
// не делаем ничего если весь бассейн неактивен (на паузе)
if (!active) return;
actorsActive = 0;
for each (actorList in allActors)
{
for each (actor in actorList)
{
if (actor.active)
{
actorsActive++;
actor.step(ms);
if (actor.collides &&
(collisionDetection != null))
{
actor.touching = collisionDetection(actor);
if (actor.touching &&
(collisionReaction != null))
collisionReaction(actor, actor.touching);
}
}
}
}
}

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

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

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

//рендерит все активные действующие лица
public function render(view:Matrix3D,projection:Matrix3D):void
{
// ничего не делать если весь бассейн невидимый
if (!visible) return;
totalpolycount = 0;
totalrendered = 0;
var stateChange:Boolean = true;
for each (actorList in allActors)
{
stateChange = true; // v2
for each (actor in actorList)
{
if (actor.active && actor.visible)
{
totalpolycount += actor.polycount;
totalrendered++;
actor.render(view, projection, stateChange);
}
}
}
}

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

//или повторно используем неактивное действующее лицо или создаём новое
//возвращает действующее лицо, которое было сгенерировано для дальнейшего использования.
public function spawn(
name:String, pos:Matrix3D = null):GameActor
{
var spawned:GameActor = null;
var reused:Boolean = false;
if (allKinds[name])
{
if (allActors[name])
{
for each (actor in allActors[name])
{
if (!actor.active)
{
//trace("A " + name + " was reused.");
actor.respawn(pos);
spawned = actor;
reused = true;
return spawned;
}
}
}
else
{
//trace("This is the first " + name + " actor.");
allActors[name] = new Vector.<GameActor>();
}
if (!reused) // ни одного неактивного не было найдено
{
actorsCreated++;
//trace("Creating a new " + name);
//trace("Total actors: " + actorsCreated);

spawned = allKinds[name].cloneactor();
spawned.classname = name;
spawned.name = name + actorsCreated;
spawned.respawn(pos);
allActors[name].push(spawned);

//trace("Total " + name + "s: "
//+ allActors[name].length);
return spawned;
}
}
else
{
trace("ERROR: unknown actor type: " + name);
}
return spawned;
}

Время для действий - проверяем столкновения между действующими лицами.
Одна из самых важных функций, которая требуется для нашего класса бассейна частиц - та, которая проверяет на столкновения между действующими лицами. Она просто запускает подходящий алгоритм столкновений, когда удобно и вызывается нашим игровым движком, когда необходимо.
public function colliding(checkthis:GameActor):GameActor
{
if (!checkthis.visible) return null;
if (!checkthis.active) return null;
var hit:GameActor;
var str:String;
for each (str in allNames)
for each (hit in allActors[str])
{
if (hit.visible &&
hit.active &&
checkthis.colliding(hit))
{
//trace(checkthis.name +
// " is colliding with " + hit.name);
return hit;
}
else
{
//trace(checkthis.name + " is NOT colliding with " +
hit.name);
}

}
return null;
}

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

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

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

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

//оптимизируем сцену: делаем действующие лица, которые слишком далеко
//или вне ограничивающего бокса невидимыми
public function hideDistant(pos:Vector3D,
// масштабируем по радиусу:
maxdist:Number = 500,
// радиус добавляется сюда:
maxz:Number = 0,
minz:Number = 0,
maxy:Number = 0,
miny:Number = 0,
maxx:Number = 0,
minx:Number = 0
):void
{
for each (actorList in allActors)
{
for each (actor in actorList)

{
if (actor.active)
{
if ((Vector3D.distance(actor.position, pos)
>= (maxdist * actor.radius)) ||
// если установлены не в ноль,
// убеждаемся, что мы внутри бокса в позиции
(maxx != 0 && ((pos.x + maxx) <
(actor.position.x - actor.radius))) ||
(maxy != 0 && ((pos.y + maxy) <
(actor.position.y - actor.radius))) ||
(maxz != 0 && ((pos.z + maxz) <
(actor.position.z - actor.radius))) ||
(minx != 0 && ((pos.x + minx) >
(actor.position.x + actor.radius))) ||
(miny != 0 && ((pos.y + miny) >
(actor.position.y + actor.radius))) ||
(minz != 0 && ((pos.z + minz) >
(actor.position.z + actor.radius)))
)
{
actor.visible = false;
}
else
{
actor.visible = true;
}
}
}
}
}

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

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

//"очистить" сцену, "убить" все известные сущности
//хорошо между новыми уровнями или после проигрыша
public function destroyAll():void
{
for each (actorList in allActors)
{
for each (actor in actorList)
{
// готов к возрождению
actor.active = false;
actor.visible = false;
}
}
}
} // конец класса
} // конец пакета

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

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

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

Однако, возникнет вопрос: как мы спроектируем эти уровни? Есть много способов сделать это. Одним может быть - реализовать алгоритм парсинга файла данных уровня общего вида формата уровней, возможно, используя XML, JSON или специализированный бинарный формат данных. Другим могло бы быть создания внутриигрового редактора уровней, который позволяет создателю использовать клавиши стрелок, чтобы построить уровни. Наконец, вы могли бы сделать уровни "процедурно" генерацией их, используя псевдо-случайное фрактальное число алгоритма генерации. Например, используя функции "шума Перлина", построенные во Flash вы могли бы создать и неопределённое число уровней автоматически.

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

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

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

//Класс парсера игровых уровней - версия 1.1
//порождает меши, основанные на пикселях в изображении
//использует крошечное ключевое изображение, чтобы определить какой RGB = какому мешу
//
package
{
import flash.display.*;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import flash.geom.Point;
import GameActorpool;
public class GameLevelParser
{
// создаём массив уровня для будущего использования
// чтобы избежать повторного просмотра пикселей карты
// в нашем примере игры это не используется, но
// это было бы удобно для поиска пути ИИ, например
public var leveldata:Array = [];
public function GameLevelParser()
{
trace("Created a new level parser.");
}

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

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

public function spawnActors(
keyImage:BitmapData, // крошечное изображение определяющее rgb
mapImage:BitmapData, // план уровня
thecast:Array, // список имён сущностей
pool:GameActorpool, // где порождать их
offsetX:Number = 0, // передвинуть на рассотяниеin X
offsetY:Number = 0, // передвинуть на расстояние X
offsetZ:Number = 0, // передвинуть на расстояние Z
tileW:int = 1, // размер в единцах мира...
tileH:int = 1, // ...для каждого пикселя
trenchlike:Number = 0, // U-форма? #=направление:-1,0,+1
spiral:Number = 0 // спиральная форма? #=число спиралей
):Number // возвращает "длину" карты
{
trace("Spawning level entities...");
trace("Actor List length = " + thecast.length);
trace("keyImage is ",keyImage.width,"x",keyImage.height);
trace("mapImage is ",mapImage.width,"x",mapImage.height);
trace("Tile size is ", tileW, "x", tileH);
trace("Total level size will be ",

mapImage.width * tileW,
"x", mapImage.height * tileH);
var pos:Matrix3D = new Matrix3D();
var mapPixel:uint;
var keyPixel:uint;
var whichtile:int;
var ang:Number;
var degreesToRadians:Number = Math.PI / 180;

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

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

Нам нужно сослаться на бассейн частиц, чтобы порождать частицы, так что мы запрашиваем один из параметров бассейна, который будет приёмником множества запросов нашей функции spawn(). Мы можем захотеть сместить все позиции с (0,0,0), так что мы должны быть способны определить их здесь с параметрами отступа. Параметры tileW и tileH позволяют нам определить отступ каждого элемента. Более высокое значение приведёт к тому, что сущности будут порождаться дальше друг от друга. Эти значения должны сравнивать размер в единицах игры, которому вы хотите, чтобы каждый пиксель на вашем изображении карты соответствовал.

Наконец, мы определяем два необязательных параметра, trenchlike и spiral. Они добавлены просто так. Если trenchlike не ноль, тогда уровень будет "искривляться" в U-образную форму вместо простой плоской поверхности. Если spiralis не ноль, тогда это будет "закручивать" уровень как ленту так много раз как определено. Это используется, чтобы сделать очень круто выглядящие формирования космических кораблей врагов.

Время для действий - парсим пиксели изображения карты.
Следующий шаг - это прочитать каждый пиксель в изображении карты и проверить существует ли значение RGB в ключевом изображении. Если да, сущности генерируются в подходящем положении.
//читаем все пиксели в изображении карты
// и помещаем сущности на уровень
//в соответствующем положении
for (var y:int = 0; y < mapImage.height; y++)
{
leveldata[y] = [];
for (var x:int = 0; x < mapImage.width; x++)
{
mapPixel = mapImage.getPixel(x, y);
for (var keyY:int = 0; keyY < keyImage.height; keyY++)
{
for (var keyX:int = 0; keyX < keyImage.width; keyX++)
{
keyPixel = keyImage.getPixel(keyX, keyY);
if (mapPixel == keyPixel)
{
whichtile = keyY * keyImage.width + keyX;
if (whichtile != 0)
{
pos.identity();
// поворачиваемся лицом "назад"
// лицом к камере

pos.appendRotation(180, Vector3D.Y_AXIS);
pos.appendTranslation(
(x * tileW),
0,
(y * tileH));
В этом цикле мы обходим каждый пиксель и определяем подходящие позиции и повороты для наших сущностей, которые скоро будут сгенерированы.
if (trenchlike != 0)
{
// trenchlike значит U-форму
ang = x / mapImage.width * 360;
pos.appendTranslation(
0,
trenchlike *
Math.cos(ang * degreesToRadians)
/ Math.PI * mapImage.width * tileW,
0);
}
if (spiral != 0)
{
// спиральная формация: как штопор
ang = (((y / mapImage.height * spiral) * 360) -
180);
pos.appendRotation(-ang, Vector3D.Z_AXIS);
}

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

В завершении функции spawnActors( а также целого класса, так как он содержит только эту функцию) нам просто нужно добавить смешение положений, которые могут передаваться в параметры функции, сказать бассейну действующих лиц породить сущность при запросе и, наконец, сохранить некотрую полезную информацию для дальнейшего использования в игре.
pos.appendTranslation(
offsetX,
offsetY,
offsetZ);

//существует имя для этого индекса?
if (thecast[whichtile-1])
pool.spawn(thecast[whichtile-1], pos);
// сохраняем местоположение для дальнейшего использования
// хорошо для поиска пути ИИ и т.д.
leveldata[y][x] = whichtile;
}
break;
}
}
}
}
}
// говорим игре координаты "конечной линии"
// самое дальнее положение на карте по оси z
// это используется в примере игры, чтобы определить когда
// вы достигли конца уровня
return mapImage.height * tileH;
}
} // конец класса
} // конец пакета

Что же произошло?
Это всё, что нужно для нашего фантастического нового класса GameLevelParser! Теперь этот класс может обрабатывать различные битмапы, которые вы можете нарисовать в любой программе редактирования изображений. Это могло бы считаться простейшим возможным приложением редактирования уровней. Просто нарисуйте вашу карту (будь то лабиринт или лес) и позвольте классу GameLevelParser породить сотни или тысячи объектов GameActor в подходящих местах. Это позволит вам лепить гигантские, сложные и интересные уровни просто рисуя их как план.

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

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

Некоторые международные пользователи имеют AZERTY клавиатуры (вместо стандартной QWERTY схемы). Так что, чтобы угодить им, мы можем также позволить дополнительные клавиши движения так чтобы они не пересекались с общепринятой W, A, S, D конфигурацией.

Клавиши стрелок будут использоваться и дальше, но для удобства многие геймеры любят использовать левую сторону клавиатуры для движения, особенно если игра требует, чтобы мышь также использовалась. Пользователи с AZERTY клавиатурами (найдены в нескольких европейских странах, таких как Франция) часто жалуются, что игры заставляют их использовать W, A, S, D для движения. Хорошие новости то, что эти клавиши могут остаться активными и одновременно наша игра может поддерживать международные клавиатуры, позволяя движение следующим образом: Z, Q, S, D. Они обе работают одновременно без проблем!

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

Время для действий - добавляем больше свойств в класс ввода.
Отредактируйте GameInput.as и сделайте несколько минимальных изменений следующим образом:
//алгоритмы ввожа игры Stage3d версия 1.2
//
package
{
import flash.display.Stage;
import flash.ui.Keyboard;
import flash.events.*;

public class GameInput
{
// текущее состояние мыши
public var mouseIsDown:Boolean = false;
public var mouseClickX:int = 0;
public var mouseClickY:int = 0;
public var mouseX:int = 0;
public var mouseY:int = 0;
// текущее состояние контроля клавиатуры
public var pressing:Object =
{ up:0, down:0, left:0, right:0, fire:0,
strafeLeft:0, strafeRight:0,
key0:0, key1:0, key2:0, key3:0, key4:0,
key5:0, key6:0, key7:0, key8:0, key9:0 };
// если обзор мышью включён, это добавляется к преследующей камере
public var cameraAngleX:Number = 0;
public var cameraAngleY:Number = 0;
public var cameraAngleZ:Number = 0;
//если это true, перетаскивание мыши меняет угол камеры
public var mouseLookMode:Boolean = true;
// главная сцена игры
public var stage:Stage;
// для событий клика и отправления их в игру

private var _clickfunc:Function = null;
Единственные отличия от нашей предыдущей версии это добавление нескольких дополнительныхз свойств класса, чтобы поддержать дополнительные клавиши движения, и функция обратного вызова, которая будет вызываться когда-бы мышь ни была кликнута.

Время для действий - обработка событий клика.
_clickfuncfunctionpointer определённый выше должен быть помешён в конструктор, так что отредактируйте его следующим образом:
//конструктор класса
public function GameInput(
theStage:Stage,
clickfunc:Function = null)
{

stage = theStage;
// получаем keypresses и обнаруживаем потерю игрой фокуса
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased);
stage.addEventListener(Event.DEACTIVATE, lostFocus);
stage.addEventListener(Event.ACTIVATE, gainFocus);
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUp);
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);
// обрабатываем клики в игре
_clickfunc = clickfunc;
}
Функции mouseMove и mouseUp остаются неизменными, так что мы не быдем включать их здесь.
private function mouseDown(e:MouseEvent):void
{
trace('mouseDown at '+e.stageX+','+e.stageY);
mouseClickX = e.stageX;
mouseClickY = e.stageY;
mouseIsDown = true;
if (_clickfunc != null) _clickfunc();
}

Единственные изменения в коде mouseDown выше в отличие от того, что мы уже имели, это одна строка, где мы в необязательном порядке запускаем функцию обратного вызова для клика, если это было установлено.
Время для действий - обновляем события клавиш.
Наконец посчитаем дополнительные клавиши движения, которые мы обсуждали выше в событиях клавиш.
private function keyPressed(event:KeyboardEvent):void
{
// qwer 81 87 69 82
// asdf 65 83 68 70
// left right 37 39
// up down 38 40
// 0123456789 = 48 to 57
// zxcv = 90 88 67 86
// trace("keyPressed " + event.keyCode);
if (event.ctrlKey ||
event.altKey ||

event.shiftKey)
pressing.fire = true;
switch(event.keyCode)
{
/*
case 81: // Q
pressing.strafeLeft = true;
break;
case 69: // E
pressing.strafeRight = true;
break;
*/
case Keyboard.UP:
case 87: // W
case 90: // Z
pressing.up = true;
break;
case Keyboard.DOWN:
case 83: // S
pressing.down = true;
break;
case Keyboard.LEFT:
case 65: // A
case 81: // Q
pressing.left = true;
break;
case Keyboard.RIGHT:
case 68: // D
pressing.right = true;
break;
case Keyboard.SPACE:
case Keyboard.SHIFT:
case Keyboard.CONTROL:
case Keyboard.ENTER:
// case 90: // z
case 88: // x
case 67: // c
pressing.fire = true;

break;
case 48: pressing.key0 = true; break;
case 49: pressing.key1 = true; break;
case 50: pressing.key2 = true; break;
case 51: pressing.key3 = true; break;
case 52: pressing.key4 = true; break;
case 53: pressing.key5 = true; break;
case 54: pressing.key6 = true; break;
case 55: pressing.key7 = true; break;
case 56: pressing.key8 = true; break;
case 57: pressing.key9 = true; break;
}
}

События фокуса практически идентичны предыдущим версиям, за исключением того, что мы сбрасываем ещё больше флагов в lostFocusevent.
private function gainFocus(event:Event):void
{
trace("Game received keyboard focus.");
}
// если игра теряет фокус, не оставляем клавиши зажатыми
private function lostFocus(event:Event):void
{
trace("Game lost keyboard focus.");
pressing.up = false;
pressing.down = false;
pressing.left = false;
pressing.right = false;
pressing.strafeLeft = false;
pressing.strafeRight = false;
pressing.fire = false;
pressing.key0 = false;
pressing.key1 = false;
pressing.key2 = false;
pressing.key3 = false;
pressing.key4 = false;
pressing.key5 = false;
pressing.key6 = false;
pressing.key7 = false;
pressing.key8 = false;
pressing.key9 = false;
}

Наконец, точно также как мы сделали для события keyPressed, мы должны добавить дополнительные клавиши в нашу функцию keyReleased.
private function keyReleased(event:KeyboardEvent):void
{
switch(event.keyCode)
{
/*
case 81: // Q
pressing.strafeLeft = false;
break;
case 69: // E
pressing.strafeRight = false;
break;
*/
case Keyboard.UP:
case 87: // W
case 90: // Z
pressing.up = false;
break;
case Keyboard.DOWN:
case 83: // S
pressing.down = false;
break;
case Keyboard.LEFT:
case 65: // A
case 81: // Q
pressing.left = false;
break;
case Keyboard.RIGHT:
case 68: // D
pressing.right = false;
break;
case Keyboard.SPACE:
case Keyboard.SHIFT:
case Keyboard.CONTROL:
case Keyboard.ENTER:
// case 90: // z
case 88: // x

case 67: // c
pressing.fire = false;
break;
case 48: pressing.key0 = false; break;
case 49: pressing.key1 = false; break;
case 50: pressing.key2 = false; break;
case 51: pressing.key3 = false; break;
case 52: pressing.key4 = false; break;
case 53: pressing.key5 = false; break;
case 54: pressing.key6 = false; break;
case 55: pressing.key7 = false; break;
case 56: pressing.key8 = false; break;
case 57: pressing.key9 = false; break;
}
}
} // конец класса
} // конец пакета

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

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

Почему мы создали класс GameActor, который расширяет базовый класс Stage3dEntity?
а) Для того, чтобы отделить специфичную для игры функциональность от низкоуровневого кода рендеринга.
б) Потому что объекты Stage3dEntity не способны содержать код движения.
в) Чтобы определение столкновений работало.
г) Чтобы уступить требованиям союза актёров.

Вперёд, герой - весёлый сторонний квест: арт-атака!
Ваш сторонний квест в этот раз - это приготовить привлекательные игровые арты для использования в вашей финальной игре. Готовясь к главе 10, возьмите перерыв от кодинга и создания новых мешей. Нарисуйте несколько новых текстур. Напишите какую-нибудь вводную музыку. Запишите высокопарные звуковые эффекты для таких вещей как взрывы и лазерные ружья.

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

Пример видов простых вводных изображений вы можете добавить в вашу игру не прибегая к полностью анимированным 3D последовательностям, это простое прозрачное PNG изображение, готовое быть наложенным на вашу игру:

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

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

Переведено на Нотабеноиде
http://notabenoid.org/book/40281/216598

Переводчики: Orion45

Настройки

Готово:

100.00% КП = 1.0