Пишем Shoot 'em up на Alternativa3D - Часть 3: Прототип.
Часть 3: Прототип.
CHAPTER 1 :: Chapter 2 :: Chapter 3
Устали от теории? Не волнуйтесь, больше ее не будет :). То, что так жаждали активные читатели свершилось - мы пишем полноценный игровой прототип. Здесь не будет динамического освещения, blooma'а и навороченных эффектов при взрыве. Зато можно будет летать, стрелять и убивать - джентельменский набор счастья.
Ранее я не объяснял принципов самого языка ActionScript, для этого есть целый учебник (ищите в начале первой части). Во второй мы рассмотрели всю технологию вывода трехмерного изображения на экран. На этот раз будет проектирование и программирование, к языку и технологиям мы возвращаться не будем.
Перед началом разбора кода лучше скачать готовый проект здесь.
Поиграть в прототип здесь.
Итак, наш трехмерный трубный шмап будет выглядеть в пространстве примерно так:
То есть у нас есть воображаемый цилиндр, обращенный к нам одним основанием и уходящий вдаль от нас. По окружности ближнего к нам основания движется наш главный герой. На окружности дальнего основания появляются противники и двигаются прямолинейно равномерно по стенкам этого цилиндра к ближнему основанию.
Запускаем Flash Develop.
Прототипирование.
Для написания прототипа (около четырех часов на все про все) мне хватило трех классов: Main, GlobVars и Player. В главном классе кода очень мало, всего 56 строк. Там происходит настройка рабочей области, инициализация движка, обработчик кадров и вызов функций других классов.
В спойлеры будут убраны коды и скриншоты (потому что форматирование кода не сохраняется и читать их не очень удобно). При копировании кода обращайте внимание на переносы строк. Когда исправите все появившиеся переносы строк, нажмите Ctrl+Shift+2 и FD самостоятельно отформатирует весь класс.
package
{
//импорт классов
import flash.display.Sprite;
import flash.display.Stage3D;
import flash.display.StageAlign;
import flash.display.StageQuality;
import flash.display.StageScaleMode;
import flash.events.Event;
import alternativa.engine3d.core.Camera3D;
import alternativa.engine3d.core.View;
//метаданные - настройка приложения
[SWF(width="600",height="600",backgroundColor="#000000",frameRate="60")]
/**
* Главный класс
* @author Verdana_hd
*/
public class Main extends Sprite
{
public var stage3d:Stage3D; //переменная для ссылки на основной stage3d
private var camera:Camera3D; //камера
public function Main()
{
if (stage)
init();
else
addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(event:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
//настройки рабочей области
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.quality = StageQuality.HIGH;
stage.stageWidth = 600;
stage.stageHeight = 600;
camera = new Camera3D(0.1, 5000); //создаем камеру
camera.x = 0;
camera.y = 0;
camera.z = -800; //сдвигаем ее назад
Game.rootContainer.addChild(camera); //добавляем в главный контейнер
camera.view = new View(600, 600, false, 0x0, 1, 4); //создаем вьюпорт
addChild(camera.view); //добавляем вьюпорт к оглавному объекту
addChild(camera.diagram); //добавляем к главному объекту диаграмму для вывода отладочной информации
stage3d = stage.stage3Ds[0]; //запись ссылки в переменную
stage3d.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate); //слушание события создания контекста
stage3d.requestContext3D(); //запуск создания контекста
}
private function onContextCreate(e:Event):void
{
stage3d.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreate);
Game.init(this, stage); //вызываем инициализацию в классе с глобальными переменными, передаем объект главного класса для доступа в нему
addEventListener(Event.ENTER_FRAME, enterFrameHandler); //слушаем событие входа в кадр
}
private function enterFrameHandler(event:Event):void
{
camera.render(stage3d); //рендеринг
Game.playerObject.update(); //каждый кадр выполняем функцию обновления в объекте игрока
}
}
}
Все что тут написано просто и понятно, разве что настройка среды может вызвать у вас вопросы. По ним легко найти ответ в онлайн-справочнике, достаточно выбрать нужный пакет и класс в меню слева (пути к класса смотри сверху в импорте). В функции onContextCreate есть обращение к статическим свойствам класса Game: атрибуту playerObject и методу init.
Итак, класс Game хранит в виде атрибутов ссылки на все основные игровые объекты, а также главные игровые функции, связанные с созданием и обработкой трехмерного пространства (за исключением рендеринга).
Разберем его подробно:
package
{
import alternativa.engine3d.core.Object3D;
import alternativa.engine3d.core.Resource;
import alternativa.engine3d.materials.FillMaterial;
import alternativa.engine3d.primitives.GeoSphere;
import flash.display.InteractiveObject;
import flash.events.TimerEvent;
import flash.geom.Vector3D;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import flash.utils.Timer;
/**
* Глобальные переменные
* @author Verdana_hd
*/
public final class Game
{
public static var mainObject:Main; //ссылка на объект главного класса
public static var playerObject:Player; //объект игрока
public static var tf:TextField; //текстовой поле
public static var rootContainer:Object3D = new Object3D(); //главный контейнер
public static var enemyFlow:Vector.<Object3D> = new Vector.<Object3D>(); //вектор с врагами
public static var playerBulletFlow:Vector.<GeoSphere> = new Vector.<GeoSphere>(); //вектор с пулями игрока
public static var enemyBulletFlow:Vector.<Object3D> = new Vector.<Object3D>(); //вектор с пулями врагов
public static var enemySpawnTimer:Timer = new Timer(1000); //таймер для создания (спауна) врагов, с задержкой в 1с=1000мс
public static function init(main:Main, stage:InteractiveObject):void //эта функция в классе выполняется первой (именно ее мы вызывали из главного класса)
{ //сдесь происходит предварительная инициализация всех объекто и загрузка всех необходимых ресурсов
mainObject = main; //записываем ссылку на главный объект, переданную параметром, в отдельный атрибут
playerObject = new Player(main, stage);
/* загружаем ресурсы шаблонов в контекст
нам не нужно видеть сами шаблоны, поэтому мы не добавляем их в rootContainer и грузим отдельно */
Lib3D.enemyTemplate.geometry.upload(mainObject.stage3d.context3D); //загружаем шаблон врага
Lib3D.playerBulletTemplate.geometry.upload(mainObject.stage3d.context3D); //загружаем шаблон пули игрока
Lib3D.enemyBulletTemplate.geometry.upload(mainObject.stage3d.context3D); //загружаем шаблон пули врагов
Lib3D.playerTemplate.geometry.upload(mainObject.stage3d.context3D); //загружаем шаблон модели игрока
Lib3D.load();
var myFormat:TextFormat = new TextFormat(); //создаем формат текста
myFormat.color = 0xFFFFFF;
myFormat.size = 20;
tf = new TextField(); //создаем текстовое поле
tf.x = 0; //задаем двумерный координаты на рабочей области
tf.y = 0;
tf.selectable = false; //делаем текст невыделяемым курсором
tf.autoSize = TextFieldAutoSize.LEFT; //авторазмерность по левому краю
tf.defaultTextFormat = myFormat; //устанавливаем формат
tf.text = "Игра."; //записываем базовый текст
mainObject.addChild(tf); //добавляем к главному объекту (он у нас класса Sprite) наше текстовое поле
//собираем и грузим все ресурсы главного контейнера
for each (var resource:Resource in rootContainer.getResources(true))
{
resource.upload(mainObject.stage3d.context3D);
}
enemySpawnTimer.start(); //запускаем таймер спауна врагов
enemySpawnTimer.addEventListener(TimerEvent.TIMER, spawn); //слушаем событие таймера, наступающее раз в 1000мс=1с, и спауним врагов
}
private static function spawn(e:TimerEvent = null):void
{
var enemy:Object3D = Lib3D.enemyTemplate.clone(); //создаем временную переменную и клонируем в нее шаблон врага (экономим ресурсы)
var n:Number = Math.random(); //создаем две случайных переменных
var i:Number = Math.random();
//систему распределения врагов при спауне смотрите в статье
if (i <= 1)
{
enemy.x = Math.sin(n) * 500;
enemy.y = Math.cos(n) * 500;
}
if (i <= 0.75)
{
enemy.x = Math.sin(n) * 500;
enemy.y = -Math.cos(n) * 500;
}
if (i <= 0.5)
{
enemy.x = -Math.sin(n) * 500;
enemy.y = Math.cos(n) * 500;
}
if (i <= 0.25)
{
enemy.x = -Math.sin(n) * 500;
enemy.y = -Math.cos(n) * 500;
}
enemy.z = 4000; //враги должны спауниться далеко
rootContainer.addChild(enemy); //добавляем врага в конейнер, чтобы он отображался
enemyFlow.push(enemy); //добавляем противника в поток врагов
}
public static function enemyActions():void //эта функция выполняется в каждом кадре - обновление противников
{
for (var i:uint = 0; i <= enemyFlow.length - 1; i++) //перебираем все объекты в потоке врагов
{
if (enemyFlow[i].z >= 0) //если противник не достиг конца цилиндра
{
enemyFlow[i].z -= 10; //сдвигаем противника на десять единиц ближе
var position:Vector3D = new Vector3D(enemyFlow[i].x, enemyFlow[i].y, enemyFlow[i].z); //записываем координаты врага в переменную
for (var j:uint = 0; j <= playerBulletFlow.length - 1; j++) //перебираем все пули в потоке пуль игрока
{
var bulPos:Vector3D = new Vector3D(playerBulletFlow[j].x, playerBulletFlow[j].y, playerBulletFlow[j].z); //записываем поток пуль в переменную
bulPos = bulPos.subtract(position); //отнимаем от вектора с позицией пули вектор с позицией врага
var deltaB:Number = Math.abs(bulPos.x) + Math.abs(bulPos.y) + Math.abs(bulPos.z); //складываем разницу координам по модулю
if (deltaB <= 50) //если разница координат меньше 40 (то есть если пуля находится внутри сферы радиуса 40 с центром в координатах врага)
{
rootContainer.removeChild(enemyFlow[i]); //удаляем врага из контейнера
enemyFlow.splice(i, 1); //удаляем врага из потока
rootContainer.removeChild(playerBulletFlow[j]); //удаляем пулю игрока из контейнера
playerBulletFlow.splice(j, 1); //удаляем пулю игрока из потока
}
}
var n:Number = Math.random(); //создаем рандомную переменную (диапазон у нее [0;1]) - обеспечиваем случайность выстрелов
if (n <= 0.01) //проверяем попадание числа в диапазон, обеспечиваем редкость выстрелом
{
var enemyBullet:Object3D = Lib3D.enemyBulletTemplate.clone(); //создаем пулю и клонируем в нее шаблон
enemyBullet.x = enemyFlow[i].x; //присваиваем пуле координаты стрелка
enemyBullet.y = enemyFlow[i].y;
enemyBullet.z = enemyFlow[i].z;
rootContainer.addChild(enemyBullet); //добавляем пулю в контейнер
enemyBulletFlow.push(enemyBullet); //добавляем пулю в потом вражеских пуль
}
}
else
{ //если враг достиг конца пути
rootContainer.removeChild(enemyFlow[i]); //удаляем его из контейнера
enemyFlow.splice(i, 1); //и из потока врагов
}
}
for (var h:uint = 0; h <= enemyBulletFlow.length - 1; h++) //перебираем все пули в потоке
{
if (enemyBulletFlow[h].z >= -800)
{
enemyBulletFlow[h].z -= 20; //приближаем к нам
}
else
{
rootContainer.removeChild(enemyBulletFlow[h]);
enemyBulletFlow.splice(h, 1);
}
}
}
public static function restart():void //функция перезапуска, которая выполняется по нажатию Enter после проигрыша
{
for each (var enemy:Object3D in enemyFlow) //перебираем всех врагов
{
rootContainer.removeChild(enemy); //удаляем из контейнера
}
for each (var enemyBul:Object3D in enemyBulletFlow) //перебираем все вражеские пули
{
rootContainer.removeChild(enemyBul); //удаляем из контейнера
}
for each (var bul:Object3D in playerBulletFlow) //перебираем все пули игрока
{
rootContainer.removeChild(bul); //удаляем из контейнера
}
enemyFlow = new Vector.<Object3D>(); //очищаем вектор фрагов
enemyBulletFlow = new Vector.<Object3D>(); //вражеских пуль
playerBulletFlow = new Vector.<GeoSphere>(); //и пуль игрока
playerObject.crack = false; //говорим, что игрок не убит
playerObject.stoped = false; //и снимаем с паузы
playerObject.enable(); //"включаем" игрока
tf.text = "Игра."; //заменяем текст
}
}
}
Некоторые вещи кажутся очевидными, некоторые непонятны. Например система спауна врагов. Посмотрите на схему с цилиндром сверху. Знаете как я создал игрока? Просто добавил его в дочерний (по отношению к rootContainer) контейнер и сдвинул в локальных координатах на 500 единиц. В самой игре мы вращаем контейнер (координаты у него 0,0,0) с игроком вокруг своей оси, благодаря чему игрок движется по кругу с радиусом в 500 и постоянно повернут "верхней" стороной к центру. Удобно, не правда ли :) ? С врагами дело совсем другое. Вариант с контейнерами сразу отметаем, он тут не сработает. Как же тогда?..
Вспомните тригонометрический круг. Если вам нужно каким-то образом оперировать координатами на окружности и углом между отрезком из этих координат в центр, используйте тригонометрический круг. Как это реализовано в системе спауна врагов.
Уловили зависимость абсциссы и ординаты от угла? Радиус равен единице.
- Создаем две случайные дробные переменные (i,n в диапазоне [0;1]).
- Смотрим, в каком диапазоне находится i (вложенные условия тут не нужны, достаточно воспользоваться последовательностью условий с сужающимся интервалом) - это выбор четверти на координатной плоскости (так как у нас изменяется только абсцисса и ордината, аппликатой можно пренебречь).
- Определив в какой четверти будет находиться враг, устанавливаем ему координаты на основе второго случайного числа (используем синус и косинус, в зависимости от четверти ставим знак) и умножаем его на 500 - радиус нашего воображаемого цилиндра.
Вектора с врагами и пулями нам нужны для хранения последних в первых. Так как враги и пули создаются уже во время выполнения программы и нам нужно постоянно обновлять их положение и проверять на "столкновения", нужны хранилища. Потому и созданы вектора.
Слово "столкновения" не зря взято в кавычки. В Alternativa3D 8 есть класс Ellipsoid Collider специально для просчета столкновений, но при включенном antiAliasing у камеры многие вещи начинают глючить и он в том числе. Метод с расчетом разницы координаты в нашем случае (с небольшими объектами простой формы) удобнее и менее затратен в плане ресурсов.
Теперь класс Player:
package
{
import alternativa.engine3d.core.Camera3D;
import alternativa.engine3d.core.Object3D;
import alternativa.engine3d.core.View;
import alternativa.engine3d.materials.FillMaterial;
import alternativa.engine3d.primitives.Box;
import flash.display.InteractiveObject;
import flash.display.Sprite;
import flash.display.Stage3D;
import flash.events.KeyboardEvent;
import flash.geom.Vector3D;
import flash.ui.Keyboard;
/**
* Игрок
* @author Verdana_hd
*/
public final class Player
{
public var container:Object3D = new Object3D(); //конейнер с моделью игрока
private var stage:InteractiveObject; //ссылка на stage
private var model:Object3D;
private var isLEFT:Boolean; //нажато ли "влево"
private var isRIGHT:Boolean; //нажато ли "вправо"
private var speed:uint = 2; //скорость вращения в градусах
private var shoot:Boolean = false; //выстрел
private var spacePressed:Boolean = false; //нажат ли пробел
private var bulSpeed:uint = 15; //скорость пули
private var hitballSize:uint = 40; //размер сферы попадания в игрока
public var crack:Boolean = false; //попали в игрока или нет
public var stoped:Boolean = false; //остановка игрока
public function Player(main:Sprite, eventSource:InteractiveObject) //конструктор, принимаем ссылки на главный класс и stage
{
model = Lib3D.playerTemplate.clone();
model.x = 0; //позиционируем модель
model.y = 500; //сдвигаем в локальных координатах
model.z = 0;
container.addChild(model); //добавляем в контейнер игрока
container.x = 0; //позиционируем контейнер
container.y = 0;
container.z = 0;
Game.rootContainer.addChild(container); //добавляем контейнер игрока к главному контейнеру
stage = eventSource; //записываем ссылку в атрибут
enable(); //включаем управление
}
public function enable():void //включение управления
{
if (stage.hasEventListener(KeyboardEvent.KEY_DOWN)) //проверяем если ли уже подписка на событие
{
stage.removeEventListener(KeyboardEvent.KEY_DOWN, stopedKeyDOWN); //удаляем подписку на нажатие клавиши в стоп-режиме
}
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDOWN); //подписываем на событие нажатия клавиши
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUP); //подписываем на событие отпуска клавиши
}
public function disable():void //отключение управления
{
isLEFT = false; //отключаем
isRIGHT = false;
shoot = false;
stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDOWN); //удаляем подписки
stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUP);
}
public function update():void //функция обновления, выполняется каждый кадр в главном объекте
{
action(); //обновления действий игрока
Game.enemyActions(); //обновление действий врагов
}
private function action():void
{
checkCollisions(); //проверяем "стокновения"
if (!crack) //если игрок не подбит
{
if (isLEFT) //если нажато влево
{
container.rotationZ += speed * Math.PI / 180; //поворачиваем контейнер (угол указывается в радианах, поэтому переводим из градусов)
}
else if (isRIGHT) //если нажато в право
{
container.rotationZ -= speed * Math.PI / 180; //аналогично
}
if (shoot) //если выстрел
{
shoot = false; //сообщаем что уже выстрелено
var bullet:Object3D = Lib3D.playerBulletTemplate.clone(); //создаем пулю, клонируем шаблон
var position:Vector3D = container.localToGlobal(new Vector3D(model.x, model.y, model.z)); //записываем координаты игрока, переводим их из локальных в глобальные
bullet.x = position.x; //позиционируем пулю
bullet.y = position.y;
bullet.z = position.z;
Game.rootContainer.addChild(bullet); //добавляем ее в главный контейнер
Game.playerBulletFlow.push(bullet); //добавляем ее в поток
}
}
else if (!stoped) //если игрок подбит и игра еще не остановлена
{
stoped = true; //говорим, что игра остановлена
disable(); //отключаем управление
Game.tf.text = "Убит. Нажмите Enter, чтобы начать заново"; //пишем
stage.addEventListener(KeyboardEvent.KEY_DOWN, stopedKeyDOWN); //подписываем на событие нажатия клавиши
}
for each (var bul:Object3D in Game.playerBulletFlow) //перебираем пули в потоке
{
if (bul.z <= 5000) //если пуля еще не улетела слишком далеко
{
bul.z += bulSpeed; //сдвигаем ее по направлению от игрока
}
}
}
private function checkCollisions():void //проверка "столкновений"
{
var position:Vector3D = container.localToGlobal(new Vector3D(model.x, model.y, model.z)); //берем глобальные координаты игрока
for each (var enemyBullet:Object3D in Game.enemyBulletFlow) //перебираем все пули врагов в потоке
{
var bulPos:Vector3D = new Vector3D(enemyBullet.x, enemyBullet.y, enemyBullet.z); //берем координаты пули
bulPos = bulPos.subtract(position); //отнимаем вектор с координатами пули от вектора с координатами игрока
var deltaB:Number = Math.abs(bulPos.x) + Math.abs(bulPos.y) + Math.abs(bulPos.z); //суммируем разность координат по модулю
if (deltaB <= hitballSize) //если разность меньше радиуса сферы "столкновения"
{
crack = true; //сообщаем, что игрок подбит
}
}
for each (var enemy:Object3D in Game.enemyFlow) //перебираем всех врагов в потоке
{//аналогично проверяем столкновение с врагами
var enemyPos:Vector3D = new Vector3D(enemy.x, enemy.y, enemy.z);
enemyPos = enemyPos.subtract(position);
var deltaE:Number = Math.abs(enemyPos.x) + Math.abs(enemyPos.y) + Math.abs(enemyPos.z);
if (deltaE <= hitballSize)
{
crack = true;
}
}
}
private function onKeyDOWN(e:KeyboardEvent):void //событие нажатия клавиши при включенном управлении
{
switch (e.keyCode) //смотрим какая клавиша нажата
{
case Keyboard.A:
isLEFT = true; //говорим, что влево
break;
case Keyboard.D:
isRIGHT = true; //говорим, что вправо
break;
case Keyboard.SPACE:
if (!spacePressed) //если пробел не зажат
{
spacePressed = true; //говорим, что пробел нажат
shoot = true; //говорим стрелять
}
break;
}
}
private function onKeyUP(e:KeyboardEvent):void //события отпуска клавиши, все наоборот
{
switch (e.keyCode)
{
case Keyboard.A:
isLEFT = false;
break;
case Keyboard.D:
isRIGHT = false;
break;
case Keyboard.SPACE:
spacePressed = false;
break;
}
}
private function stopedKeyDOWN(e:KeyboardEvent):void //событие нажатия, если игра остановлена (игрок побит)
{
if (e.keyCode == Keyboard.ENTER) //если нажат Enter
{
Game.restart(); //перезапускаем игру
}
}
}
}
На мой взгляд комментариев в классе достаточно, но все же я хотел бы объяснить вам некоторые свои решения:
Как видите, камера и вьюпорт создаются именно здесь. На самом деле я сделал это по инерции своего последнего проекта. Камеру стоит создавать в классе с игроком, только если вам нужно как-то управлять ею вместе с действиями игрока, ну а вьюпорт само собой создается там же где камера, т.к. является ее атрибутом. Нам же управление камерой вообще не нужно. Значит заносим в список как будущее структурное изменение.
Про вращение игрока по окружности уже писалось выше как предисловие к системе спауна врагов. Удобно, не правда ли?)
С функциями выключения и выключения управления все просто. Запомните лишь, что подписывать на клавиатурные события нужно stage нашего главного объекта. В данном случае стэйдж мы передаем через параметр в конструкторе и записываем в приватный атрибут.
Система поворота крайне проста, просто ин- или декрементируем величину угла поворота контейнера по оси аппликат и не забываем перевести градусы в радианы ("2" в радианах слишком много, поэтому удобнее работать с градусами).
По выстрелам: нам не нужно, чтобы при зажатом пробеле каждый кадр выпускалась пуля, поэтому проверяем нажат ли уже пробел и произведен ли уже выстрел, предельно логично. Система создания пули аналогична таковой у врагов, не забываем лишь переводит координаты врага из локальных в глобальные.
Стоп-режим, для него понадобилось две булевых переменных: подбит ли игрок, остановлена ли игра. Одной не обойтись, а отправка собственных событий тут не оправдана. Потому две переменные.
Движение вектора с пулями такое же как в предыдущем классе. Это движение оптимизировано, в отличие от вражеских пуль. Потому что вражеские пули удаляются их потока только при перезапуске игры. Также в список на оптимизацию.
Проверка столкновений такая же как с врагами, разве что здесь мы ввели переменную размера хитбола (сферы столкновения), что не принципиально.
Функции событий нажатия и отпускания клавиши простые, инструкция switch case проще некуда.
Скачать проект со всеми файлами можно здесь.
Поиграть в прототип здесь.
ВЫводы
Как видите, игровой прототип он действительно игровой прототип. Потому и отражает сугубо игровую механику с нулевой схематической графикой. В следующей (последней) части мы завершим разработку, заменив примитивы на полноценные трехмерные модели со сложными материалами, пули заменим на анимированные спрайты, добавим эффекты взрыва, создадим звездное небо и динамическое осещение. А пока можете разобрать код.
- 22 марта 2012, 08:49
- 011
извиняюсь за немногословность, но мне кажется, что самое главное я ранее уже объяснил. по языку все должно быть понятно, сложны конструкций не использовал, а понятность программной логики зависит уже от вашей личной математической логики)
кстати заметил для себя, что игра в прототип затягивает
надеюсь с прототипом легче?
сегодня добавлю много нового в этот пост, например разберу подробнее класс Player, разобью пару классов и слегка изменю структуру приложения (не меняя игровой логики), добавлю диаграммы классов и т.п.
Вчера слишком устал и пост казался цельным, но сейчас вижу что нет.
А зачем нужны исходники в виде картинок? Текстом никак не получается?
Текстом копипастить будут, а так набирать придется, может чему научатся :)) А если серьезно, то скорее всего из-за отсуствия подсветки синтаксиса и форматирования, я думаю.
Поддерживаю, код лучше в текст перевести, как опцию оставить картинку тоже.
синтаксис-то ладно, а вот форматирование важно. хотя бы сразу же отобразящиеся разрывы строк (учитывая обилие комментариев, разрывов будет по три на строку).
при копировании табуляция не сохраняется :( и места займет оооооочень много. был бы хороший визуализатор кода (как здесь например) конечно сделал бы тектом.
а так есть цельный исходник проекта со всеми файлами.
и код кстати в спойлеры не прячется :(
а тот кусочек что в самом конце, я ему вручную пробелы наставил. были бы строки подлиннее, было бы плохо
зима не будетПоследняя часть будет только после Гаминатора. Потому что всю неделю я писал эти три части, а нужно было диплом и доклады к трем (!!! ад) конференциям :D сейчас до гаминатора разгребаю дела, участвую в конкурсе, а потом завершаю.Если сойду с конкурсной дорожки раньше срока, то и за четвертую часть возьмусь раньше срока. жаль, что не успел показать главных плюсов движка (иморта моделей, карт материалов, освещения), но что поделать, если наведение красоты займет слишком много сил.
Сейчас напишу небольшой обзорчик пары игр))