Возможно вы искали: 'Capcom Classics Collec...'

May 11 2025 07:13:32
  • Как сделать 8Gamers.Ru домашней страницей?
  • Игры
    • База данных по играх
    • Игровые новости
    • Игровая индустрия
    • Обзоры на игры
    • Прохождения игр
    • Гайды к играм
    • Превью о играх
    • Игровые тизеры
    • Игровые арты
    • Игровые обои
    • Игровые скриншоты
    • Игровые обложки
    • Игровые трейлеры
    • Игровое видео
    • Вышедшие игры
    • Ближайшие релизы игр
  • Кино и ТВ
    • База данных по кино
    • Статьи о кино
    • Постеры
    • Кадры из кино
    • Кино трейлеры
    • Сегодня в кино
    • Скоро в кино
  • Комиксы и манга
    • Манга по алфавиту
    • База данных по комиксах
    • Читать онлайн комиксы
    • Читать онлайн манга
    • База персонажей
  • Читы и коды
    • Чит-коды для PC игр
    • Чит-коды для консольных игр
    • Трейнеры
    • Коды Game Genie
  • Моддинг
    • Модификации
    • Карты к играм
    • Программы для моддинга
    • Статьи о моддинге
  • Геймдев
    • Всё о создании игр
    • Список движков
    • Утилиты в помощь игроделу
    • Конструкторы игр
    • Игровые движки
    • Библиотеки разработки
    • 3D-модели
    • Спрайты и тайлы
    • Музыка и звуки
    • Текстуры и фоны
  • Рецензии
    • Игры
    • Кино
    • Аниме
    • Комиксы
    • Мангу
    • Саундтреки
  • Саундтреки
    • Лирика
  • Файлы
    • Патчи к играм
    • Русификаторы к играм
    • Сохранения к играм
    • Субтитры к кино
  • Медиа
    • Видео
    • Фото
    • Аудио
    • Фан-арты
    • Косплей
    • Фото с виставок
    • Девушки из игр
    • Рисунки
    • Рисуем онлайн
    • Фотохостинг
  • Юмор
    • Анекдоты
    • Афоризмы
    • Истории
    • Стишки и эпиграммы
    • Тосты
    • Цитаты
  • Флеш
    • Азартные
    • Аркады
    • Бродилки
    • Гонки
    • Для девочек
    • Для мальчиков
    • Драки
    • Квесты
    • Леталки
    • Логические
    • Мультфильмы
    • Открытки
    • Приколы
    • Разное
    • Спорт
    • Стратегии
    • Стрелялки
Статистика

Статей: 87772
Просмотров: 96001592
Игры
Injustice:  Gods Among Us
Injustice: Gods Among Us
...
Dark Souls 2
Dark Souls 2
Dark Souls II - вторая часть самой хардкорной ролевой игры 2011-2012 года, с новым героем, сюжето...
Battlefield 4
Battlefield 4
Battlefield 4 - продолжение венценосного мультиплеер-ориентированного шутера от первого ли...
Кино
Steins;Gate
Steins;Gate
Любители японской анимации уже давно поняли ,что аниме сериалы могут дать порой гораздо больше пи...
Ку! Кин-дза-дза
Ку! Кин-дза-дза
Начинающий диджей Толик и всемирно известный виолончелист Владимир Чижов встречают на шумной моск...
Обзоры на игры
• Обзор Ibara [PCB/PS2] 18342
• Обзор The Walking ... 18785
• Обзор DMC: Devil M... 19861
• Обзор на игру Valk... 15866
• Обзор на игру Stars! 17752
• Обзор на Far Cry 3 17933
• Обзор на Resident ... 16011
• Обзор на Chivalry:... 17494
• Обзор на игру Kerb... 17969
• Обзор игры 007: Fr... 16600
Превью о играх
• Превью к игре Comp... 17944
• Превью о игре Mage... 14447
• Превью Incredible ... 14704
• Превью Firefall 13460
• Превью Dead Space 3 16324
• Превью о игре SimC... 14713
• Превью к игре Fuse 15430
• Превью Red Orche... 15531
• Превью Gothic 3 16331
• Превью Black & W... 17342
Главная » Статьи » Всё о XNA » Blender + Xna + Xen = ??? (часть 1)

Blender + Xna + Xen = ??? (часть 1)

Введение

Данная статья является моими собственными исследования в использовании XNA и построении для нее контента за последние 5-6 месяцев.

Что послужило исследованием связки Blender + Xna?  Дело в том, что я и еще несколько человек решили написать небольшую игру (кто бы сомневался :) ). Но мы не смогли нормально работать вместе из-за того, что всем, в том числе и моделерам необходимо было ставить на компьютеры Visual C# и XNA GS для того, чтобы добавлять в игру контент самостоятельно. В итоге мы пришли к выводу, что необходимо написать экспортер моделей из какого-либо пакета моделирования. В итоге наш взгляд упал на Blender. Есть две причины, по которым мы его выбрали: 1) его бесплатность, 2) т.к. в Blender есть собственный игровой движок, то есть некоторые средства, которые могли бы помочь в использовании Blender в качестве редактора уровней (об этих средствах мы tit поговорим в самой статье). Мне понадобился примерно месяц с небольшим на изучение языка Python, т.к. Blender в качестве языка скриптов использует именно его, изучение архитектуры API Blender’а и  создание собственного экспортера сцены.

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

  • Полная замена системы эффектов с интеграцией кода и проверкой.
  • Для вершинных и индексных буферов нет необходимости в создании декларативной части структуры вершины.
  • Быстрое управление состоянием визуализации.
  • Легкие в реализации цели визуализации (прям каламбур какой-то :) )
  • Гибкая игровая логическая система обновления, включая асинхронные обновления (многопоточность).
  • Высокопроизводительный и гибкий формат анимированных моделей.
  • Система материалов, поддерживающая до 10 источников света за проход, нормалмэппинг, инстансинг и др.
  • Мощная 2D/3D система частиц, полностью программируемая используя схему валидации XML. С поддержкой ускорения на GPU.
  • Большие возможности по получении статистики начиная от количества визуализированных треугольников и заканчивая использованием потоков на XBOX
  • Набор 2D элементов, вывод текста, фильтрация изображений и др.

Xen - графическое API для XNA с открытым исходным кодом.

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

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

Материалы и Media, использованные в статье:

В качестве документации по Xen были использованы примеры, входящие в его состав (в состав Xen входит 25 примеров с подробным описанием на английском языке).
Для бонуса к статье (реализация SkyBox на Xen) использовалась медиа из примера реализации воды, также за основу была взята реализация скайбокса
Модель перекрестка была создана моим другом Akima.

Данная статься, можно сказать, является не только описанием загрузки сцены, в статье так же рассматривается создание графа сцены и др. составляющих игрового движка. Альтернативную реализацию графа сцены и др. составляющих движка можно посмотреть в альтернативной реализации Дмитрия Тимофеева (General) – движка gEngine


Итак, экспорт сцены из Blender.


Прежде всего, немного теории.


Blender – бесплатный пакет трехмерного моделирования. Возможностей последней стабильной версии хватает для создания таких мультфильмов как Elephants Dream, Big Buck Bunny и др. Как и все платные пакеты 3D моделирования, такие как 3DS MAX или Maya, Blender имеет возможность расширения своих функций, путем создания скриптов на языке Python.
Чтобы эффективно писать скрипты для Blender, необходимо знать, откуда взять информацию о Python – API функциях , которые можно применять в его скриптах.

Каждый объект сцены в Blender является наследником общего класса Blender.Object и имеет общие свойства и методы, которые характерны для всех трехмерных объектов. Для экспорта интересны такие свойства как имя объекта, тип объекта и его трансформации. После получения общих сведений об объекте необходимо получить данные, специфичные для каждого типа объектов в Blender. Такими объектами могут быть Empty, Mesh, Lamp, Camera, Armature, Curve и другие. Нам пока интересны первые четыре типа объектов, из которых особенно интересны Empty, Mesh.

Краткое описание экспортируемых объектов:

Empty – объект пустышка, имеющий только имя и трансформации и может использоваться для расположения ключевых мест в игре, таких как области входа и выхода из уровня, расположения геометрии, общей для всех уровней, например камни, деревья и другие объекты, которые находятся в общем архиве игры. Как это сделать,  я расскажу чуть позже.
Mesh – объект, специальными данными которого является информация о геометрии. Каждая геометрия имеет набор связанных с ней вершин (Vertex) и граней (Face), причем каждая грянь может быть как треугольником, так и квадом (экспорт таких граней будет не сложнее чем обычных треугольников, об этом Вы узнаете ниже).
Lamp - источник света. В Blender имеются следующие типы источников света: Lamp, Sun, Spot, Hemi, Area, Photon. Lamp – точечный,  Sun – направленный, Spot – конусный и т.д. В данной статье я расскажу как экспортировать первые два типа, источников света. Если вам понадобятся остальные, то их экспорт можно будет выполнить так же просто.
Camera – тут в принципе и рассказывать много не надо. Камера – объект, отвечающий за место и направление взгляда в виртуальном мире. Нам этот объект пригодится для создания анимации полета по уровню или, например, для расположения фиксированных точек взгляда на сцену (например, как в квестах, и некоторых других жанрах игр).

Я сказал анимации полета? Ну конечно! Каждый объект Blender’а можно анимировать, это же мощный пакет 3D анимации! Конечно, я не буду разбирать в этой статье, как экспортировать скелетную анимацию, но простую анимацию объектов, такую как перемещение, масштабирование и вращение мы сможем экспортировать без особых усилий. Экспорт анимации будет выполнен  в исходном виде, т.е. так, как это сохранено в Blender.

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



Теперь перейдем к практике.

Первым делом необходимо скачать Blender, а для работы скриптов Python (какую версию Python необходимо скачать для текущей версии Blender, показано под ссылкой на закачку). На момент написания статьи была возможность закачки Blender v2.49 и Python SDK 2.6.3. Я не буду описывать установку этих продуктов, с этим Вы справитесь сами. Но последовательность работы с Blender я постараюсь описать с применением скриншотов так, что бы Вам не заблудиться в его интерфейсе.

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



В режиме редактирования скриптов Blender изменит расположение своих окон как показано на следующем рисунке:



Режим написания скриптов состоит из трех окон. Левое верхнее – 3D сцена, левое нижнее – привязка скриптов «ScriptLinks» и наконец, правое – текстовый редактор с возможностью подсветки синтаксиса и автозавершения (правда во многих случаях она не работает из-за того, что Python  не строго типизированный язык). Подсветка синтаксиса включается нажатием на специальные кнопки – флажки в панели инструментов данного окна. На следующем рисунке нажаты три из них для обеспечения более комфортного скриптописания.

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



Открытый файл скрипта в редакторе API Blender’а будет выглядеть следующим образом.




Пишем скрипт экспорта


В данной части статьи я хотел бы подробно описать порядок создания скрипта и применяемые при этом API Blender’а. Но сначала несколько слов о языке Python.

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

Так же в языке присутствуют такие элементы как списки, кортежи и словари. Думаю первое и третье понятно, а второе – это аналог списка, только не позволяющий изменять его элементы. Все три объекта создаются с помощью скобок [ ], ( ) и { } соответственно. Для того, чтобы отличить кортеж с одним элементом от математического выражения ставится запятая после первого элемента, а второй не добавляется т.е. например (1,) а не (1).  Каждый из этих трех объектов поддерживает так называемые срезы. Срез – это тоже самое, что и Subctring для строк, т.е. – средство получения подмножеств. Записывается  как [начало : конец]. Ну да ладно о питоне, что-то заговорился я. Если вы захотите подробно изучить питон, то сами найдете достаточно литературы по нему. В справочной системе API Blender’а содержится также достаточное количество примеров. А сейчас давайте перейдем к программированию.

Я надеюсь, что скрипт будет иметь правильные отступы после конвертации статьи в формат для сайта. Если нет, то смотрите лучше код из исходника. Комментарии я в коде писать не буду, т.к. текстовый редактор Blender’а не поддерживает русский шрифт, хотя интерфейс Blender’а можно русифицировать. Комментарии я буду писать здесь в коде примера к статье. 

Первое, что нам встретится в любом скрипте Blender’а, расширяющем его функции – это заголовок. Заголовок читается самим Blender’ом, а не интерпретатором питона и выполнен в виде многострочного теста, перед которым ставится команда #!BPY. В заголовке указывается название скрипта, версия Blender’а, для которой написан скрипт, группа, определяющая размещение скрипта в меню Blender’а (в нашем случае «Export») и подсказка пункта меню.

Заголовок скрипта
1
2
3
4
5
6
7
#!BPY
"""
Name: 'XnaDev.Ru Exporter Example'
Blender: 248
Group: 'Export'
Tooltip: 'Exports data from Blender to XNA'
"""

 

Далее импорт пространств имен и объектов пространств имен в текущее пространство имен (пространство имен скрипта)

Импорт
1
2
3
4
5
import os
import math
import bpy
import Blender
from Blender import *

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# функция записи в файл отступа от края документа
def wrIndent(file, indent): 
  if indent:
    for i in xrange(indent): # функция xrange создает виртуальный массив начиная с единицы
      file.write("  ") # запись в файл отступа
 
# функция записи открывающего xml тэга с атрибутами, передающимися в виде списка кортежей
def xmlWrOp(file, name, attr = None, indent = 0, iscomplete = 0): 
  wrIndent(file, indent)
  file.write("<" + name)
  if attr:
    for key, value in attr:
      # Здесь пример форматирования строки. Символы, начинающиеся с % будут последовательно
      # заменены аргументами, переданными в виде кортежа
      file.write(' %s="%s"' % (key, str(value)))
  if iscomplete:
	  file.write(" /")
  file.write(">\n")
 
# функция записи в файл завершающего тэга
def xmlWrCl(file, name, indent = 0):
  wrIndent(file, indent)
  file.write(" + name + ">\n")
 
# функция записи в файл конструкции в виде двухмерного вектора с заданным именем
def xmlWriteVector2D(file, name, vals, indent = 0):
  xmlWrOp(file, name, [("X", round(vals[0], 5)), ("Y", round(vals[1], 5))], indent, True)
 
# функция записи в файл конструкции в виде трехмерного вектора с заданным именем
# Обратите внимание, что аргументы вектора сохраняются в порядке 1,2,0 а не 0,1,2
# т.к. Blender имеет систему координат, в которой ось Z является верхом
# необходимо преобразовать трансформации Blender’а к системе координат XNA
def xmlWriteVector3D(file, name, vals, indent = 0):
  xmlWrOp(file, name, [("X", round(vals[1], 5)), ("Y", round(vals[2], 5)), ("Z", round(vals[0], 5))], indent, True)
 
# функция записи трансформаций объекта сцены в файл
def xmlWriteTransforms(file, obj, indent = 0):
  xmlWrOp(file, "Transforms", [], indent)
  # получаем локальные трансформации объекта
  # если у объекта нет родителя, то трансформации будут глобальными
  matrix = obj.getMatrix('localspace')
  pos = matrix.translationPart()
  rot = matrix.toEuler()
  scl = matrix.scalePart()
  # сохраняем трансформации в файл
  xmlWriteVector3D(file, "Pos", [pos[0], pos[1], pos[2]], indent + 1)
  xmlWriteVector3D(file, "Rot", [rot[0], rot[1], rot[2]], indent + 1)
  xmlWriteVector3D(file, "Scl", [scl[0], scl[1], scl[2]], indent + 1)
  xmlWrCl(file, "Transforms", indent)
 
# функция записи дополнительных параметров объектов сцены в файл
def xmlWrProperties(file, props, indent = 0):
  xmlWrOp(file, "Properties", [], indent)
  if props:
    for prop in props:
      xmlWrOp(file, "Property", [("Name", prop.getName()),("Value", prop.getData()) ], indent+1, 1)
  xmlWrCl(file, "Properties", indent)
 
# функция переводящая градусы в радианы
def toRadian(value):
  return value * math.pi / 180.0
 
# функция перевода значения lens, используемого в Blender для задания размера линз камеры
# в FOV, для использования при создании камеры в XNA
def convertLens(lens):
  return 2 * math.atan(16 / lens)
 
 
# коллекция типов источников света
lampTypes = ('Lamp', 'Sun', 'Spot', 'Hemi', 'Area', 'Photon' )
 
# коллекция типов продолжения кривых анимации на концах промежутков анимации
ipoExtendTypes = ("CONST", "EXTRAP", "CYCLIC", "CYCLIC_EXTRAP")
 
# коллекция поддерживаемых экспортером объектов 
supportedObjects = ('Mesh', 'Empty', 'Lamp', 'Camera')
 
# переменная списка, в которую будут записаны все материалы экспортируемых объектов
sceneMaterials = []
 
# коллекции необходимее для определения «правильных» имен кривых при экспорте
# основная задача – приведение анимации в координатную систему XNA
ipoFrom = ('LocX', 'LocY', 'LocZ', 'RotX', 'RotY', 'RotZ', 'ScaleX', 'ScaleY', 'ScaleZ')
ipoTo =   ('LocZ', 'LocX', 'LocY', 'RotZ', 'RotX', 'RotY', 'ScaleZ', 'ScaleX', 'ScaleY')
 
# функция, преобразующая имя кривой в соответствии с вышеприведенными коллекциями
def convertIpoName(name):
  for i, n in enumerate(ipoFrom):
    if n == name:
      return ipoTo[i]
  return name # для кривых, не входящих в коллекцию ничего не меняем
 
# функция, записывающая данные кривой в файл
# тут необходимо знать то, что каждая точка кривой состоит из трех 2D векторов h1, p, h2
# первый и третий – это манипуляторы, которыми можно управлять формой кривой в редакторе Blender.
# p – сама точка кривой.
# В координате x вектора хранится значение времени в секундах, в y – значение кривой
def xmlWriteCurve(file, curve, indent = 0):
  curveName = convertIpoName(curve.name)
  xmlWrOp(file, curveName, [( "Extend", ipoExtendTypes[curve.extend] )], indent)
  for point in curve.bezierPoints:
    xmlWrOp(file, "Point", [], indent + 1)
    h1, p, h2 = point.vec
    if (curveName in ["RotX", "RotY", "RotZ"]):
      # вращение переводим в радианы и умножаем на 10, т.к. Blender 
      # хранит градусы в единицах на порядок меньше
      h1 = [h1[0], toRadian(h1[1] * 10.0)]
      p  = [p[0],  toRadian(p[1]  * 10.0)]
      h2 = [h2[0], toRadian(h2[1] * 10.0)]
    elif (curveName == "Lens"):
      # Lens переводим в FOV
      h1 = [h1[0], convertLens(h1[1])]
      p  = [p[0],  convertLens(p[1])]
      h2 = [h2[0], convertLens(h2[1])]
    xmlWriteVector2D(file, "h1", h1, indent + 2)
    xmlWriteVector2D(file, "p",  p,  indent + 2)
    xmlWriteVector2D(file, "h2", h2, indent + 2)
    xmlWrCl(file, "Point", indent + 1)
  xmlWrCl(file, curveName, indent)
 
 
# Функция записывающие все кривые анимации объекта в файл
# здесь отмечу, что есть два места хранения кривых анимации у каждого объекта
# первое место – сам объект, в котором охраняться кривые, общие для всех объектов
# второе место – контейнер данных, который различается для каждого типа объектов сцены
# я привожу код для экспорта кривых из обоих мест, но реализация анимации не трансформаций
# объектов, а других параметров выходит за рамки текущей статьи
def xmlWriteIpo(file, obj, indent = 0):
  xmlWrOp(file, "Ipo", [], indent)
  if obj.ipo: # общие кривые анимации
    for curve in obj.ipo.curves:
      xmlWriteCurve(file, curve, indent + 1)
    try:
      for curve in obj.data.ipo:
        xmlWriteCurve(file, curve, indent + 1)
    except:
      pass
  elif obj.data: # контейнер, специфичный для каждого типа объектов
    try:
      ipo = obj.data.ipo
      if ipo:
        for curve in ipo:
          xmlWriteCurve(file, curve, indent+1)
    except:
      pass
  xmlWrCl(file, "Ipo", indent)
 
 
# функция записи в файл информации о грани геометрии
def xmlWriteFace(file, face, tan, ind, indent = 0):
  xmlWrOp(file, "Face", [], indent)
  for i in ind:
    attribs = [("Index", face.v[i].index)]
    if face.smooth: # Если грань гладкая, то нормаль берется из вершины
      attribs = attribs + [("NX", round(face.v[i].no[1], 5)), \
        ("NY", round(face.v[i].no[2], 5)), ("NZ", round(face.v[i].no[0], 5))]
    else: # иначе нормаль берется из самой грани
      attribs = attribs + [("NX", round(face.no[1], 5)), ("NY", round(face.no[2], 5)), ("NZ", round(face.no[0], 5))]
    attribs = attribs + [("TX", round(tan[i][1], 5)), ("TY", round(tan[i][2], 5)), ("TZ", round(tan[i][0], 5))]
    attribs = attribs + [("UVX", round(face.uv[i][0], 5)), ("UVY", round(face.uv[i][1], 5))]
    xmlWrOp(file, "V", attribs, indent + 1, True)
  xmlWrCl(file, "Face", indent)
 
 
# Функция записи в файл данных о геометрии объекта «Mesh»
# т.к. Blender изначально не проектировался как игровой движок, данные геометрии хранятся
# несколько иначе, чем в игровых движках или той же XNA. Построение нужных буферов 
# мы будем выполнять при загрузке сцены.
# Массив вершин содержит только положение и нормаль, а для каждой грани можно получить текстурные координаты,
# тангенсы и нормаль, если грань не сглаженная. В каждой грани содержатся ссылки на вершины,
# на основе которых она построена. 
# Здесь хочу заметить, что в Blender могут присутствовать как треугольные грани, так и квады,
# поэтому, реализация алгоритма сохранения это учитывает.
def xmlWriteMeshData(file, mesh, indent = 0):
  md = mesh.getData(mesh = 1) # данные о геометрии содержатся в контейнере объекта
 
  # первое что мы должны проверить – это наличие текстурных координат
  if not md.faceUV:
    print "Mesh '" + mesh.name + "' has no UV, skipped."
    return
 
  xmlWrOp(file, "Mesh", None, indent)
 
  # сохраняем массив вершин
  xmlWrOp(file, "Vertices", [("Count", len(md.verts))], indent + 1)
  for v in md.verts:
    xmlWrOp(file, "Vert", [("Index", v.index), \
      ("X", round(v.co[1], 5)), \
      ("Y", round(v.co[2], 5)), \
      ("Z", round(v.co[0], 5))], indent + 2, 1)
  xmlWrCl(file, "Vertices", indent + 1)
 
  # получаем все материалы объекта
  # если массив материалов объекта не пустой, то сохраняем для каждого материала
  # набор граней, которым назначен этот материал
  # если материалов нет, то сохраняем все грани с материалом None
  # Каждый набор граней сохраняется как MeshPart и атрибутом Material
  materials = md.materials
  if materials:
    for matIndex in xrange(len(materials)):
      if not materials[matIndex] in sceneMaterials:
        sceneMaterials.append(materials[matIndex])
      xmlWrOp(file, "MeshPart", [("Material", materials[matIndex].name)], indent + 1)
      tan = md.getTangents()
      for i, face in enumerate(md.faces):
        if face.mat == matIndex:
          xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)
          if len(face.v) == 4: # если грань – квад, то сохраняем второй треугольник
            xmlWriteFace(file, face, tan[i], (2, 0, 3) , indent + 2)
      xmlWrCl(file, "MeshPart", indent + 1)
  else:
    xmlWrOp(file, "MeshPart", [("Material", "None")], indent + 1)
    tan = md.getTangents()
    for i, face in enumerate(md.faces):
      xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)
      if len(face.v) == 4: : # если грань – квад, то сохраняем второй треугольник
        xmlWriteFace(file, face, tan[i], (2, 0, 3) , indent + 2)
    xmlWrCl(file, "MeshPart", indent + 1)
 
  xmlWrCl(file, "Mesh", indent)
 
 
# функция, записывающая в отдельный файл информацию о материалах всей сцены
# это даст возможность после экспорта редактировать материалы объектов
# здесь мы сохраняем цвет материала, ambient составляющую - силу света в тени,
# данные о бликовой составляющей материала (цвет и силу блика), а так же
# имена всех назначенных текстур объекту
def xmlWriteMaterals(filename):
  out = open(filename, "wt")
  xmlWrOp(out, "Materials")
  for mat in sceneMaterials:
    print "Export material '" + mat.name + "'"
    xmlWrOp(out, "Material", [("Name", mat.name)], 1)
    xmlWrOp(out, "Color", [("R", round(mat.R, 3)), ("G", round(mat.G, 3)), \
      ("B", round(mat.B, 3)), ("A", round(mat.alpha, 3)), ("Ambient", round(mat.amb, 3))], 2, 1)
    xmlWrOp(out, "Specular", [("R", round(mat.specR, 3)), ("G", round(mat.specG, 3)), \
      ("B", round(mat.specB, 3)), ("Power", round(mat.spec, 3))], 2, 1)
    xmlWrOp(out, "Textures", [], 1)
    for tex in mat.textures:
      if tex:
        image = tex.tex.image
        if image:
          xmlWrOp(out, "Texture", [("Name", image.name)], 2, 1)
    xmlWrCl(out, "Textures", 1)
    xmlWrCl(out, "Material", 1)
  xmlWrCl(out, "Materials")
  out.close()
 
 
# основная функция, сохраняющая общие данные всех поддерживаемых экспортером объектов,
# так же для каждого типа объектов вызывает функции экспорта специфических данных
def xmlWriteObgectData(file, obj, indent = 0):
  print "Export object '" + obj.name + "'"
  # первым делом создаем список атрибутов, и вносим в него имя и тип объекта 
  objAttribs = [("Name", obj.name), ("Type", obj.type)]
 
  if obj.type == "Lamp": # добавляем в коллекцию атрибутов объекта параметры источника света
    objAttribs.append(("LampType", lampTypes[obj.getData().getType()]))
    if (lampTypes[obj.getData().getType()] == "Lamp"):
      objAttribs.append(("Dist", round(obj.getData().getDist(), 5)))
      objAttribs.append(("Energy", round(obj.getData().getEnergy(), 5)))
 
  elif obj.type == "Camera": # добавляем в коллекцию атрибутов объекта параметры камеры
    objAttribs.append(("Lens", round(convertLens(obj.getData().lens), 5)))
    objAttribs.append(("ClipStart", round(obj.getData().clipStart, 5)))
    objAttribs.append(("ClipEnd", round(obj.getData().clipEnd, 5)))
 
  # создаем запись узла дерева сцены со всеми атрибутами
  xmlWrOp(file, "Node", objAttribs, indent)
  # сохраняем трансформации объекта
  xmlWriteTransforms(file, obj, 2)
 
  if obj.type == "Mesh": # если объект – Mesh, экспортируем данные геометрии
    xmlWriteMeshData(file, obj, 2)
 
  elif obj.type == "Lamp": # для источников света сохраняем цвет
    lamp = obj.getData();
    xmlWriteVector3D(file, "Color", ( lamp.G, lamp.B, lamp.R), 2);
 
  else: # для других типов объектов ничего не выполняем
    pass
 
  # сохраняем дополнительные параметры объекта
  xmlWrProperties(file, obj.getAllProperties(), 2)
  # сохраняем кривые анимации объекта
  xmlWriteIpo(file, obj, 2)
 
  # для всех потомков объекта, поддерживаемых экспортером, вызываем эту же функцию сохранения объекта
  for child in Object.Get():
    if (child.type in supportedObjects) and (child.parent == obj):
      xmlWriteObgectData(file, child, indent + 1)
 
  # закрываем тэг записи узла дерева
  xmlWrCl(file, "Node", indent)
 
# Главная функция экспортера, которая получает имя файла из диалога сохранения
# в который будет выполнен экспорт
def write(filename):
  print "Start export..."
 
  objects = []
  # находим все объекты, у которых нет родителя и поддерживаемые экспортером
  for obj in Object.Get():
    if (obj.type in supportedObjects) and (obj.parent == None):
      objects.append(obj)
 
  # открываем файл на запись в режиме текста
  out = open(filename, "wt")
  # записываем корневой элемент документа XML
  xmlWrOp(out, "Root")
  # записываем в файл все объекты, не имеющие родителей
  for obj in objects:
    xmlWriteObgectData(out, obj, 1)
  # закрываем корневой элемент
  xmlWrCl(out, "Root")
  # закрываем файл
  out.close()
 
  # сохраняем в отдельный файл информацию о материалах
  # обратите внимание, здесь как раз используется срез, о котором я говорил ранее
  xmlWriteMaterals(filename[0:filename.rfind(".")] + ".materials.xml")
 
  print "Finished..."
 
# первое что будет выполнено скриптом, это вызов диалога сохранения файла
# если пользователь нажмет кнопку Export, то диалог сохранения вызовет функцию write
# и будет выполнен экспорт
Window.FileSelector(write, "Export", "level.xml")

 

После того, как скрипт готов, его необходимо сохранить.
Существует несколько способов запустить написанный скрипт. Первый – из меню Text текстового редактора выбрать пункт Run Python Script. Второй – при активном окне текстового редактора (окно считается активным, если над ним расположен указатель мыши) нажать сочетание клавиш Alt+P. Эти два способа будут запускать скрипт, загруженный в текстовый редактор.
Существует третий способ запуска скриптов экспорта – из главного меню File -> Export -> название скрипта, указанного в его заголовке. Но для того, чтобы сделать запуск скрипта возможным с использованием третьего способа, необходимо выполнить еще некоторое количество действий.
Первое что необходимо сделать – создать папку, в которой будут находится Ваши созданные скрипты. Для примера создадим следующую папку на диске C: - «C:\BlenderScripts\». Далее необходимо указать Blender’у, что в этой папке находятся Ваши скрипты (собственно говоря там могут находится и не только Ваши скрипты, а еще и скачанные скрипты, расширяющие возможности Blender). Сделать это можно следующим образом:
  1. Перетащить мышкой главное меню Blender’а вниз. Это откроет настройки программы.
  2. Переключиться в режим редактирования путей.
  3. Указать в поле Python Scripts папку, которую мы создали ранее.
  4. Минимизировать окно настроек, перетащив его заголовок обратно вверх.
  5. Выбрать в меню File пункт Save Default Settings (или нажать сочетание Ctrl+U) для сохранения настроек по умолчанию. Сохранение настроек по умолчанию необходимо для сохранения пути к папке со скриптами.
  6. Результатом выполненных действий в списке подпунктов меню File -> Export появится пункт XnaCev.Ru Exporter Example, выбрав который, будет вызван диалог выбора файла, т.е запущен наш с Вами скрипт экспорта..


На следующем скриншоте отображен результат проделанных действий:



Итак все готово для экспорта, но есть одно но. Наш скрипт не может генерировать текстурные координаты для объектов. Это необходимо сделать до экспорта объектов, иначе при загрузке сцены в приложение XNA вы ничего не увидите т.к. наш скрипт экспортирует узел дерева сцены, но без информации о геометрии. Такое поведение можно увидеть при экспорте сцены по умолчанию в консоли Blender’а «Mesh ‘Cube’ has no UV, skipped.»:



Если добавить кубу текстурные координаты, то мы увидим следующий результат:



Добавить текстурные координаты объекту можно в режиме редактирования сетки (чтобы перейти в режим редактирования сетки необходимо при выбранном объекте нажать клавишу Tab либо выбрать пункт Edit Mode из выпадающего списка, расположенного справа от пункта меню Object окна редактирования 3D модели – 3D View) с помощью горячей клавиши  U.

Результат  экспорта такой сцены будет следующим:

level.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
>
   Name="Camera" Type="Camera" Lens="0.85756" ClipStart="0.1" ClipEnd="100.0">
    >
       X="-6.50764" Y="5.34367" Z="7.48113" />
       X="0.61977" Y="46.69194" Z="63.5593" />
       X="1.0" Y="1.0" Z="1.0" />
    >
    >
    >
    >
    >
  >
   Name="Cube" Type="Mesh">
    >
       X="0.0" Y="0.0" Z="0.0" />
       X="0.0" Y="0.0" Z="0.0" />
       X="1.0" Y="1.0" Z="1.0" />
    >
    >
       Count="8">
         Index="0" X="1.0" Y="-1.0" Z="1.0" />
         Index="1" X="-1.0" Y="-1.0" Z="1.0" />
         Index="2" X="-1.0" Y="-1.0" Z="-1.0" />
         Index="3" X="1.0" Y="-1.0" Z="-1.0" />
         Index="4" X="1.0" Y="1.0" Z="1.0" />
         Index="5" X="-1.0" Y="1.0" Z="1.0" />
         Index="6" X="-1.0" Y="1.0" Z="-1.0" />
         Index="7" X="1.0" Y="1.0" Z="-1.0" />
      >
       Material="Material">
        >
           Index="0" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="2" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="1" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="2" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="0" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="3" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="1.0" />
        >
        >
           Index="4" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
           Index="6" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
           Index="7" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.0" TZ="1.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="6" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
           Index="4" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
           Index="5" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.0" TZ="1.0" UVX="0.0" UVY="1.0" />
        >
        >
           Index="0" NX="-0.0" NY="0.0" NZ="1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="5" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="4" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="-0.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="5" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="0" NX="-0.0" NY="0.0" NZ="1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="1" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="1.0" />
        >
        >
           Index="1" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="6" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
           Index="5" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="6" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
           Index="1" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="2" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="1.0" />
        >
        >
           Index="2" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="7" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="6" NX="0.0" NY="-0.0" NZ="-1.0" TX="-0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="7" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="2" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
           Index="3" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="1.0" />
        >
        >
           Index="4" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
           Index="3" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="0" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        >
        >
           Index="3" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
           Index="4" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
           Index="7" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="-0.0" UVX="0.0" UVY="1.0" />
        >
      >
    >
    >
    >
    >
    >
  >
   Name="Lamp" Type="Lamp" LampType="Lamp" Dist="29.99998" Energy="1.0">
    >
       X="1.00545" Y="5.90386" Z="4.07625" />
       X="3.16371" Y="106.93632" Z="37.26105" />
       X="1.0" Y="1.0" Z="1.0" />
    >
     X="1.0" Y="1.0" Z="1.0" />
    >
    >
    >
    >
  >
>

 

Level.Materials.xml
1
2
3
4
5
6
7
8
>
   Name="Material">
     R="0.8" G="0.8" B="0.8" A="1.0" Ambient="0.5" />
     R="1.0" G="1.0" B="1.0" Power="0.5" />
  >
  >
  >
>

 


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

В данной статье будет взята за основу несколько упрощенная сцена перекрестка, сделанная моим другом Akima (на самом деле этот перекресток он сделал по моей просьбе для данной статьи). Я добавил туда пример простой анимации, чтобы продемонстрировать анимацию на основе кривых Безье. Результат загрузки данной сцены в XNA/XEN проект Вы увидите в следующей части статьи.



Если Вы посмотрите на правый верхний угол Blender, то увидите что в тестовой сцене порядка 16к вершин и полигонов. Это позволит нам выполнить тест формата XML на время загрузки уровня, т.к. экспортированная сцена занимает порядка 13 мегабайт. Для такой маленькой сцены это очень много, но зато позволит нам наглядно изучить состав файла экспортированного уровня изнутри. Кроме того перевод такого количества значений с плавающей точкой из строк значительно увеличит время загрузки сцены, так что не волнуйтесь, если после запуска не увидите окно проекта сразу.

Дополнительные материалы.

Добавление дополнительных свойств объектам сцены.

Как я уже говорил, в Blender’е есть свой игровой движок. В следствие этого, в нем присутствуют средства, которые пригодятся нам для своей игры или другого интерактивного приложения, написанного на XNA. Основная возможность, которая меня заинтересовала в первую очередь – это возможность добавления различных пользовательских параметров объектам сцены.
Чтобы добавить дополнительные параметры объекту необходимо:
  • выбрать этот объект;
  • на кнопочной панели (данная панель по умолчанию находится внизу всех режимов редактирования) переключиться в режим Logic (Логика) [1];
  • нажать на кнопку Add Property (Добавить Свойство) [2].


  • на появившейся строке ввести имя свойства [1] и его значение [2].



Кроме имени и значения необходимо выбрать тип данных свойства. Выпадающий список слева от имени каждого свойства позволит выбрать одно из следующих значений: Timer, String, Float, Int и Bool. Первое нам не нужно, зато остальные вполне могут использоваться в качестве типов параметров объектов.

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


Анимация в Blender.

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



По умолчанию, режим редактирования анимации выглядит, как показано на следующем скриншоте:



Данный режим состоит из пяти окон, которые могут понадобиться аниматору для создания анимации:
  1. Outliner – навигатор по дереву сцены.
  2. 3D View – окно 3D редактора.
  3. IPO Curve Editor – редактор кривых.
  4. Timeline – шкала времени.
  5. Buttons Window – окно с кнопками.

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



Как редактировать кривые анимации будет Вашим домашним заданием. На этом первая часть статьи закончена, впереди вторая не менее увлекательная.
1046 Прочтений •  [Blender + Xna + Xen = ??? (часть 1)] [08.08.2012] [Комментариев: 0]
Добавил: Ukraine Vova
Ссылки
HTML: 
[BB Url]: 
Похожие статьи
Название Добавил Добавлено
• Blender + Xna + Xen = ??? (часть 1) Ukraine Vova 08.08.2012
Ни одного комментария? Будешь первым :).
Пожалуйста, авторизуйтесь для добавления комментария.

Проект входит в сеть сайтов «8Gamers Network»

Все права сохранены. 8Gamers.NET © 2011 - 2025

Статьи
Рецензия на Pressure
Рецензия на Pressure
Чтобы обратить на себя внимание, начинающие маленькие разработчики, как правило, уходят в жанры, ...
Рецензия на Lost Chronicles of Zerzura
Рецензия на Lost Chron...
Игры, сделанные без любви и старания, похожи на воздушный шар – оболочка есть, а внутри пусто. Lo...
Рецензия на The Bridge
Рецензия на The Bridge
«Верх» и «низ» в The Bridge — понятия относительные. Прогуливаясь под аркой, можно запросто перей...
Рецензия на SimCity
Рецензия на SimCity
Когда месяц назад состоялся релиз SimCity, по Сети прокатилось цунами народного гнева – глупые ош...
Рецензия на Strategy & Tactics: World War 2
Рецензия на Strategy &...
Название Strategy & Tactics: World War II вряд ли кому-то знакомо. Зато одного взгляда на ее скри...
Рецензия на игру Scribblenauts Unlimited
Рецензия на игру Scrib...
По сложившейся традиции в информационной карточке игры мы приводим в пример несколько похожих игр...
Рецензия на игру Walking Dead: Survival Instinct, The
Рецензия на игру Walki...
Зомби и продукция-по-лицензии — которые и сами по себе не лучшие представители игровой биосферы —...
Обратная связь | RSS | Донейт | Статистика | Команда | Техническая поддержка