Итак, я подумал и решил, что стоит мне завести здесь тему для моего небольшого пока проекта. Вернее, проектом назвать это сложно, т.к. из всего имеется только небольшая идея в каком направлении двигаться и небольшие наработки. Геймдизайнер из меня хреновый ввиду недостатка фантазии и творческого начала, посему, буду делать то, что лучше всего у меня получается, а именно код. Также, буду выкладывать сюда прогресс, возможно, даже больше одного раза :D А может и заброшу) Ну а направление движения кодописания буду корректировать находу с учетом или неучетом желающих.
Что могу сказать о проекте сейчас.... хочу сделать это тактическим шутером, есть карта с непроходимыми блоками и кустами, есть какие то юниты на этой карте. Графика продолжительное время будет условной ввиду отсутствия карманного раба-дизайнера.
To be continued... maybe...
Я ведь не инвесторов ищу, а рекламодателей :D
Вообще, планирую в виде статей расписывать чего я делаю, кто то может полезное для себя чего нить почерпнул) было бы хорошо как то возможностей оформления добавить, ну или хотя бы тег [/code] сделать нормально, чтоб читаемый код получался с отступами.
Не так давно мне в голову пришла идея, а что если сделать World of Tanks только про людей? Мне нравилось в танках играть за светляка - стоять в кустах и без палева передавать координаты врагов союзникам, либо делать это у всех на виду маневрируя и скрываясь в укрытиях. Отсюда пошли первые пожелания к игре:
1) Это должен быть кооперативный тактический шутер на один раунд как контра либо танки.
2) Должны быть элементы стелса в виде обзора, маскировки, кустов, etc
3) это должна быть 2d игра(физика и логика 2д, графика вполне может быть трехмерной), т.к. на освоение 3d физики и геометрии тратить время и силы не шибко хочется.
4) вместо танков - люди, соответственно, свои особенности в виде ходьбы, бега, позы и прочее. гранаты, да.
5) ну и всякая прокачиваемость навыков, покупка оружия и прочие плюшки между боями
Немного про класс юнитов.
Я решил применить компонентный подход для юнитов. Если кто не знает, это когда логика класса разделена по своим ролям на другие классы. Т.е. вместо того чтобы хранить координаты, имя, счет в банке и прочие данные и поля юнита скопом в одном классе в виде полей мы распределяем их по составляющим подчиненным классам. Один подчиненный класс, допустим, будет отвечать за физику, другой за звук и так далее. Одним из плюсов такого подхода является его гибкость и возможность комбинировать, буквально конструировать юнита из разных частей. Теоретически, для того чтобы сделать летающего орка с голосом котенка, в последующем можно будет написать всего одну строчку типа
Orc := TUnit.Create(TPhysicFly, TGraphicOrc, TSoundCat);
круто, да? вот и я так подумал.... Чтобы было удобно обращаться этим компонентам друг к другу внутри юнита(например, графическая компонента отрисовывает модель в координатах указанных в физической компоненте), я добавил ссылки на каждую компоненту в класс базовой компоненты. Также чтоб не парится закинул туда ссылки на карту и таймер. Все эти ссылки заполняются динамически от родителя к чайлду. Это все для того, чтобы не было каких либо глобальных переменных, а была замкнутая внутри себя система. У меня объявление класса юнита выглядит сейчас так:
T_Unit = class
Timer : T_Timer; // ссылка на игровой таймер, один на всех
Map : T_Map; // ссылка на карту
Bases : TObjectList; // список базовых частей
Logic : T_LogicBase; // логическая часть, заполняется динамически из списка Bases
Physic : T_PhysicBase; // физическая часть, заполняется динамически
Graph : T_GraphBase; // графическая часть, заполняется динамически
constructor Create(ABases : array of T_Base); virtual; overload; // в конструктор передаем любое количество частей составляющих юнита
constructor Create(ABases : array of T_BaseClass); virtual; overload; // либо передаем не экземпляры, а сами классы
procedure Init;
procedure Update; virtual; // в Update выполняется Update для всех частей
procedure Draw; virtual; // в Draw выполняется Draw для всех частей
end;
Сейчас у меня выстрел формируется вызовом Shoot := T_Unit.Create([T_PhysicShoot, T_LogicBase, T_GraphBase]);
а юнит вызовом xUnit := T_Unit.Create([T_PhysicUnit, T_LogicUnit, T_GraphUnit]);
Насколько это удобно? - Покажет время, а сейчас у меня пока не так много разных игровых классов, чтобы оценить.
Идея не нова. И лучше применяй полностью компонентный подход или декораторы. Сильно задумаешься над архитектурой и красивым кодом и забудешь об игре. Старайся сначала сделать прототип а потом делать игру.
Идею почерпнул в статье на геймдев ру. Про юнити знаю только то, что она существует, но если идея кажется с3.14зженной из юнити, то можете считать, что так и есть.
Чуть позже напишу немного про поле зрения и кусты, запишу немного видео.
Компонентный подход штука хорошая. Но реализация (так как она описана) мне кажется странной. Зачем TSoundCat наследовать от TSoundBase? Тут же просто набор звуков должен быть:
Если пойти дальше: FlyingCatOrc.Init("fco.xml");
Ну и если пойти совсем дальше, то Map.Init(level.ToString()+".xml"); и внутри уже у вас летающие мяукающие орки и и прочий трэш :)
В GameObject не нужны Draw, Update. Просто где-то в GameLevel должны быть ArrayLogics, ArrayPhisics, ArrayGraphics. И при GameLevel.Update делается update для всей логики и физики. А в GameLevel.Render делается draw для всех графики. Добавлять виртуальную функцию draw для логики - бессмысленно же.
Реализовывал небольшую игру через компоненты и пришел к выводу что полиморфизм и наследование идут лесом при использовании компонент. Возможно, такое впечатление из-за маленького размера проекта. Там где наследование не убиралось компонентным подходом, применил шаблоны.
Насчет TSoundBase... Просто набор звуков может быть и прокатит в простой игрушке, в более менее сложных проектах уже добавляются всякие условия для воспроизведения звука, типа если долго ничего не говорили, то произнести какую нибудь шутку. И логично эту логику для звука вынести в отдельный класс. В прошлом году я делал BattleChat для одной космической стратегии. В игре в боевке летали корабли, атаковали противника, лазеры, ракеты, кооперация между собой и прочее. Ну а BattleChat должен был создавать реалистичное звуковое окружение радиопереговоров между кораблями.
Типа:
"-Атакую!"
"-Понял, прикрываю"
"-Не пробил, нужна пушка побольше!"
В общем, если бы там был просто набор звуков, то в активном бое получилась бы полная неразбериха, каждый корабль кричал бы что то свое в радиоэфир и разобрать в этом хоть что то было бы весьма проблематично. Так что, какая то логика для управления звуком должна быть.
Насчет массивов физики, графики и логики - мысль понятна. Это легко можно добавить, но сейчас не будем усложнять код)
Ну а касательно того, что у Logic есть метод Draw - так это сделано из соображений, что разделение модулей на логику, графику, звук, физику - весьма условно и нужно только для программиста, на общем уровне есть только набор модулей, а что у них внутри - это уже второстепенный вопрос. Только некоторые модули должны обновляться при игровом тике, а другие при отрисовке кадра. Поэтому и сделал и Update и Draw для T_Base. К тому же таким образом можно объединить функции двух модулей в одном если очень захочется, например не заводить отдельный модуль для TSoundCat для добавления единственного звука "Meow", а встроить в TLogicCat.
Зрение.
В тактическом шутере да и в шутере вообще очень важным является обзор, и если в трехмерном FPS игрок видит то, что видно на экране, то в двухмерном шутере с видом сверху надо искусственно ограничить обзор игрока. Обычно это выглядит как фонарик в руках героя в темном месте.
Для своего случая я ввел следующие характеристики: Дальность обзора, Направление взгляда, Угол обзора, Маскировка. Первые три вместе дают нам некоторый сектор видимости. Маскировка - действует на дальность обзора врага, например, юнит с маскировкой 0.5 будет виден для врага с дальностью обзора 50м подойдя на расстояние 25м.
Не будем забывать, что игра происходит не в открытом поле, а на карте, которая будет влиять на наш обзор. Копировать систему зрения как в танках будет не шибко правильно, думаю. Поэтому сделал по своему. Для каждой ячейки карты я добавил параметр LightPassPower, характеризует сколько силы света надо потратить, чтобы преодолеть эту ячейку. Так у стенки LightPassPower = 1.0, значит при столкновении со стенкой луч света дальше не летит, расходуя всю оставшуюся энергию, у кустов - 0.6, так если у света оставался запас хода в 30 метров, то после куста запас хода останется 30*(1-0.6) = 12м.
В общем, мне нравится такая система обзора, остаются только сомнения насчет маскировки, стоит ли оставить её влияние таким как сейчас, либо сделать её влияние не в процентах от обзора а в фиксированной величине, типа маск-халат дает -20 метров обзору врага. Либо сделать и то и то.
Посмотреть получившееся, наглядно можно в этом видео:
Сегодня весь день занимался сетью, а именно, передачей карты. Долго думал делать ли солянку из UDP + TCP или оставить только UDP. Решил остановиться на UDP ввиду того, что связь нынче почти везде стабильная, потерь пакетов и искажений обычно нет. Передачу карты сделал при первом пакете от клиента. Сделал поток для сокета, в потоке чтение всех поступивших пакетов - вызов каллбека на каждый пакет, далее посылка всех исходящих пакетов.
При размере карты 100х100 появились первые глюки, а именно карта загружалась неполностью, пакеты явно не доходили. Поломав немного голову, понял, что записываю данные карты в потоке сокета в каллбэке, т.е. паралелльно никто эти данные не отправляет, они отправляются потом всем скопом в том же потоке. Клиент их не успевает все забрать и они теряются.
Ввиду этого решил отказаться от каллбэков, выделил кучу буферов, в которые сокет будет писать пакеты, приложение в любой момент может забрать очередной пакет вызовом типа GetNextPacket и обработать его. Карта стала передаваться клиенту нормально, на этом хотел было остановиться, но решил потестить на размере 1000х1000. Как и ожидалось, полезли глюки - карта стала загружаться процентов на 5 от силы. Посчитав размер данных, а их оказалось по 27 байт на ячейку карты, итого около 27 мегабайт, пришел к выводу, что не резон их все сразу пихать в сокет, т.к. 27 мбайт отправляться то отправляются за небольшое время, но мой adsl их за такое короткое время принять ну никак не сможет. Пока поставил задержку небольшую, но потом надо как то внедрить в обертку сокета шейпер трафика. С задержкой карта принялась нормально, потестил на vps со связью через adsl.
В худшем случае клиент вылетит с ошибкой, в лучшем ничего плохого не произойдет. Серваку должно быть по барабану, т.к. на серваке исповедую принцип не доверяй клиенту ни в чем и все данные должны там проверяться. Да и большая часть данных будет идти от серва к клиенту.