Вам бывает скучно на работе? Часто сидите два-три дня без единого намёка на трудовую деятельность и пытаетесь ее имитировать, а руководство так и ждет ошибки с вашей стороны?
Теперь этого можно будет избежать, ведь в разработке находится пошаговая RPG в стиле Dragon Quest на NES для офисного приложения Excel.
Конечно, идея не нова. В Excel и раньше делали разнообразные игры (даже полноценный шутер). Но если вы читали мои предыдущие статьи, то поймёте, что создавать троллейбусы из батонов белого (или чёрного) хлеба — мое небольшое хобби.
Перейдём же к делу
На данный момент готова альфа-версия графического движка и редактор карт. С вероятностью в 99% они будут дорабатываться в процессе разработки.
Сперва немного расскажу о некоторых технических характеристиках.
1. Скорее всего в игре будет использоваться событийная модель, а не циклическая, ведь для пошаговой стратегии цикл не так уж и важен. Это будет зависеть от того, как быстро Excel справится с рендером одного кадра. Минус такого подхода заключается в том, что написать какой-нибудь платформер на этой основе не получится. В любом случае я попробую оба варианта.
2. Все текстуры имеют размер 16*16 пикселей. Палитра состоит всего из 56 цветов (стандартный размер палитры Excel). В качестве основы я взял палитру NES.
3. Текстуры я рисую в программе Aseprite, а в Excel из. bmp перевожу с помощью небольшого, написанного на VBA софта, который нашел в интернете.
4. Бэкграунд состоит из тайлов, спрайты же привязаны к системе координат.
Графический движок
Карта уровня представляет собой двумерный массив с кодовым обозначением тайла в формате «XXXY», где XXX — номер текстуры по порядку, Y — значение, указывающее на то, можно ли пройти сквозь тайл. Вторая функция пока что не реализована.
Спрайты хранятся в отдельном массиве в формате: координата X, координата Y, порядковый номер спрайта, тип спрайта и тэг. Два последних значения пока не используются.
Для создания графического движка сперва необходимо инициализировать различные переменные. Часть из них хранится на отдельном листе, а в коде я сделал их публичными (знаю, что так нельзя):
— высота и ширина игрового экрана;
— диапазон игрового экрана;
— массив игрового экрана для рендеринга;
— размер одной текстуры;
— координаты камеры;
— координаты игрока;
— номер уровня;
— карта тайлов и координаты спрайтов;
— массив спрайтов, тайлов и спрайтов игрока.
Все текстуры хранятся на отдельном листе с отображением индексов цветов палитры.
Так как это только первая версия движка, нажатие на «Новую игру» тут же запускает метод fillWithTextures (процесс создания массива цифровых значений цвета).
‘##########ЯДРО ГРАФИЧЕСКОГО ДВИЖКА##########
Sub fillWithTextures()
Dim cntRow As Integer, cntCol As Integer, pixOffsetX As Double, pixOffsetY As Double, _
mapBlockX As Integer, mapBlockY As Integer, texXOffset As Integer, texYOffset As Integer, _
texNumber As Integer, arrBlockTex() As Variant
Dim cntSprites As Integer, spriteOffsetX As Integer, spriteOffsetY As Integer, _
spriteNumber As Integer
ReDim arrRender(main.screenH, main.screenW)
For cntRow = 1 To main.screenH
For cntCol = 1 To main.screenW
‘Считаем смещение пикселя экрана относительно координат камеры
pixOffsetX = main.cameraX + cntCol
pixOffsetY = main.cameraY + cntRow
‘считаем спрайты
For cntSprites = 1 To UBound(arrMapSprites(), 1)
‘считаем смещение спрайта относительно пикселя
spriteOffsetX = pixOffsetX — arrMapSprites(cntSprites, 1)
spriteOffsetY = pixOffsetY — arrMapSprites(cntSprites, 2)
‘если пиксель содержит спрайт
If spriteOffsetX >= 0 And spriteOffsetY >= 0 And spriteOffsetX + pixOffsetX < pixOffsetX + main.blockSize _
And spriteOffsetY + pixOffsetY < pixOffsetY + main.blockSize Then
‘если элемент спрайта не пуст
If arrSprites((spriteOffsetY + blockSize * arrMapSprites(cntSprites, 3)) + 1, spriteOffsetX + 1) <> 0 Then
arrRender(cntRow, cntCol) = arrSprites((spriteOffsetY + blockSize * arrMapSprites(cntSprites, 3)) + 1, spriteOffsetX + 1)
End If
End If
Next
‘рисуем тайлы
If (pixOffsetX > 0 And pixOffsetX < main.mapWidth) And _
(pixOffsetY > 0 And pixOffsetY < main.mapHeight) Then
‘расчет тайла, в который входит пиксель
mapBlockX = WorksheetFunction.RoundUp(pixOffsetX / main.blockSize, 0)
mapBlockY = WorksheetFunction.RoundUp(pixOffsetY / main.blockSize, 0)
‘определяем номер текстуры
texNumber = arrMapTiles(mapBlockY, mapBlockX)
‘Определение цвета текстуры для пикселя
texXOffset = getTexOffset(pixOffsetX) + 1
texYOffset = getTexOffset(pixOffsetY) + 1
If arrMapTiles(mapBlockY, mapBlockX) <> «» And arrTiles(texNumber * _
main.blockSize + texYOffset, texXOffset) <> «» And arrRender(cntRow, cntCol) = «» Then
arrRender(cntRow, cntCol) = arrTiles(texNumber * main.blockSize + texYOffset, texXOffset)
End If
End If
Next
Next
End Sub
Первым делом происходит поиск спрайтов, которые нужно отрисовать. Для этого программа проверяет каждый «пиксель» игрового экрана, считает смещение этого пикселя относительно стартовых координат камеры, а также смещение относительно всех спрайтов на уровне. Если смещение «пикселя» относительно спрайта по каждой оси равно от 0 до 15 (так как размер текстуры 16*16), берётся индекс нужного цвета из массива спрайтов.
Вторым пунктом программа на основе смещения относительно координат камеры высчитывает позицию «пикселей» на карте тайлов. Когда нужный тайл найден, программа с помощью функции getTextOffset возвращает индекс цвета пикселя из массива тайлов.
‘Возвращает координату текстуры
Function getTexOffset(dCoordinate As Double) As Integer
getTexOffset = (dCoordinate — 1) Mod main.blockSize
End Function
Почему сперва происходит проверка спрайтов, а затем тайлов?
Все просто. Программа просто не трогает те «пиксели», которые уже закрашены спрайтами, что влияет на производительность в лучшую строну.
P.S. На следующий день я понял, что проверка условия «закрашенности» спрайтами должна производиться в начале. Тогда это повлияет на производительность. Привет, оптимизация.
Вторая проблема — это проверка спрайтов для каждого пикселя экрана. Предположим, что на уровне находится 40 спрайтов. При размере экрана 96*64 = 6144 пикселей количество итераций цикла достигает 6144 * 40 = 245760. Если пойти другим путём и проверять спрайты не для каждого пикселя, а по условию нахождения в поле зрения камеры, то количество итераций не превысит 40*16*16 = 10240. Эта проблема решается быстро.
Создание игрока и рендеринг изображения
Для того, чтобы поместить спрайт игрока в рендер-массив нужно посчитать смещение спрайта относительно координат камеры.
Sub renderPlayer(imagePose As Integer)
Dim offsetX As Double, offsetY As Double, cntRow As Integer, _
cntCol As Integer
‘Считаем смещение относительно координат камеры
offsetX = main.playerX — main.cameraX
offsetY = main.playerY — main.cameraY
‘Если игрок не находится за пределами камеры
If offsetX >= 0 And offsetY >= 0 And _
offsetX < main.cameraX + main.screenW And _
offsetY < main.cameraY + main.screenH Then
For cntRow = 0 To main.blockSize — 1
For cntCol = 0 To main.blockSize — 1
‘Если пиксель текстуры заполнен
If main.arrPlayerSpr((cntRow + 1) + (imagePose * main.blockSize), cntCol + 1) <> 0 Then
‘Если пиксель не заходит за пределы камеры
If cntRow + main.playerY <= main.screenH + main.cameraY And cntCol + main.playerX <= main.screenW + main.cameraX Then
arrRender(offsetY + cntRow, offsetX + cntCol) = main.arrPlayerSpr((cntRow + 1) + (imagePose * main.blockSize), cntCol + 1)
End If
End If
Next
Next
End If
End Sub
Аргумент imagePose будет использоваться для имитации поворота игрока при движении.
В конце-концов рендер-массив готов, поэтому вызываем последнюю процедуру, которая рисует сформированный кадр на игровом поле. Ее я показывать не буду, потому что она очень сырая и будет дорабатываться.
Будут дорабатываться и текущие процедуры, потому что на данный момент быстродействие немного хромает и реализовать упомянутую циклическую модель в данный момент проблематично.
Вот, что получается в итоге:
На этом можно пока закончить. Надеюсь, что из ваших глаз не пошла кровь от «лучшего в мире» языка программирования и попыток написать что-то осмысленное.
В любом случае жду критику и советы. Всегда интересно послушать, что скажут люди.
Немного саморекламы
Я создал паблик ВКонтакте, куда буду выкладывать свои мысли, алгоритмы, код, ссылки на эти статьи и конечно мемасики:) Если вам интересно наблюдать за разработкой игр и разных странных вещей, добро пожаловать.
Развлекая — поучай.
(Гораций)
Если вы уже имели какой-то опыт программирования в прошлой жизни (привет, Basic, Pascal и т.д.), то, скорее всего, уже прошли этап «игрописательства». Однако, тряхнуть стариной и размять мозги вполне можно. Если же вы никогда не программировали игр, то никогда не поздно начать этот весьма увлекательный процесс. Всё, что нам потребуется — это Excel (любой версии) и 15-20 минут времени для начала.
Тренироваться будем на известной в узких кругах программистов игре «Жизнь» (Life). Её придумал британский математик Джон Конвей еще в 1970 году на основе работ легендарного Джона фон Неймана — прадедушки всех современных компьютеров. Если вы не сталкивались с ней раньше — не проблема, правила можно объяснить за полминуты:
-
Игра идет на большом (иногда даже бесконечном) поле в клеточку («вселенной»). Как вы понимаете, Excel для такого подходит идеально
-
В один момент времени каждая клетка может быть в двух состояниях — живой (обозначим её каким-нибудь значком или просто единичкой) или же мертвой (пустой). Начальное состояние всех клеток в игре называют первым поколением.
-
Если брать блок клеток 3х3 с текущей клеткой в середине, то вокруг неё оказывается 8 клеток-соседей. Дальнейшая судьба клетки зависит от того, сколько именно живых клеток (N) окажется в этой окружающей области. Вариантов несколько:
- Если клетка была пустая (мертвая), но у нее есть ровно 3 живых соседа, то в ней зарождается жизнь.
- Если клетка живая, но у неё меньше 2 соседей, то она умирает от одиночества.
- Если клетка живая, но у неё больше 3 соседей, то она умирает от перенаселения.
- Если клетка живая и у нее 2-3 соседа, то клетка продолжает жить.
Вот, собственно, и все правила. Можно даже сказать, что всё это неправильно называть игрой, т.к. здесь нет соперников в привычном понимании. Вы расставляете первое поколение, запускаете процесс и затем просто наблюдаете за развитием вашей «колонии» на протяжении нескольких поколений.
Однако, не стоит недооценивать обманчивую простоту этой логики — количество комбинаций, сценариев игры и многообразие фигур в такой игровой вселенной поражает своим разнообразием и поистине бесконечно. В математике подобные модели называют клеточными автоматами. А самое интересное, что реализовать подобную модель можно в любой версии Excel буквально на 20 строчках кода.
Поехали.
Шаг 1. Готовим игровое пространство
Создадим в новой книге три листа:
- game — это будет основной листы игры, где мы будем наблюдать за развитием нашей «колонии»
- next — этот лист будет формировать следующее поколение, которое затем придет на смену текущему
- start — на этом листе мы будем задавать начальную конфигурацию, т.е. первое поколение в нашей игре
На каждом листе (можно выделить их заранее, удерживая клавишу Shift или Ctrl, чтобы не повторять трижды одни и те же действия), разметим игровое поле размером, допустим, 30 на 30 ячеек. Впоследствии размер поля можно будет подправить в соответствии с вашими аппетитами и мощью вашего ПК:
На листе start разметим с помощью единичек первое поколение любым желаемым образом:
Шаг 2. Пишем макрос
Теперь пришла пора расчехлить наш VBA и написать макрос, который и будет делать всю работу, а именно:
- Копировать первое поколение с листа start на лист game.
- Проходить по ячейкам игрового поля на листе game и проверять окружающих соседей (блок 3х3) для каждой из них.
- В зависимости от результатов проверки помечать на листе следующего поколения next ту же ячейку как живую (1) или мертвую (пусто).
- Копировать получившееся новое поколение с листа next вместо текущего на листы игры game.
- Повторять пункты 2-4 несколько раз, сменяя одно поколение другим и отображая на экране изменения в нашей «колонии».
Для начала откроем редактор Visual Basic на вкладке Разработчик (Developer). Если такой вкладки не видно, то её нужно будет сначала отобразить через Файл — Параметры — Настройка ленты (File — Options — Customize Ribbon), включив соответствующий флажок.
В открывшемся окне редактора создадим новый модуль с помощью команды меню Insert — Module, а затем скопируем и вставим туда код нашего макроса:
Sub Life() Dim cell As Range, n As Integer, i As Integer Set rGame = Worksheets("Game").Range("B2:AE31") Set rStart = Worksheets("Start").Range("B2:AE31") Set rNext = Worksheets("Next").Range("B2:AE31") Set wNext = Worksheets("Next") rStart.Copy Destination:=rGame For i = 1 To 50 rNext.ClearContents For Each cell In rGame.Cells n = WorksheetFunction.CountA(cell.Offset(-1, -1).Resize(3, 3)) - cell.value If cell = "" And n = 3 Then wNext.Cells(cell.Row, cell.Column) = 1 If cell = 1 And (n = 2 Or n = 3) Then wNext.Cells(cell.Row, cell.Column) = 1 If cell = 1 And (n < 2 Or n > 3) Then wNext.Cells(cell.Row, cell.Column) = "" Next cell rNext.Copy Destination:=rGame Next i End Sub
Теперь давайте разберем его построчно для понятности:
Поскольку в коде нам придется несколько раз ссылаться и много раз работать с диапазонами игрового пространства (B2:AE31) на каждом из трёх листов книги, то имеет смысл сразу оформить их как переменные. Это делается в блоке:
Set rGame = Worksheets("Game").Range("B2:AE31") Set rStart = Worksheets("Start").Range("B2:AE31") Set rNext = Worksheets("Next").Range("B2:AE31")
Заодно мы создаем ещё и переменную wNext, которая ссылается на весь лист next целиком — это нам тоже пригодится в будущем:
Set wNext = Worksheets("Next")
Затем, перед началом игры, мы должны перенести первое поколение с листа start на лист game. Это выполяется командой прямого копирования с использованием уже созданных переменных:
rStart.Copy Destination:=rGame
Поскольку мы хотим прокрутить в нашей игре не одно, а несколько (например, 50 для начала) поколений, то дальнейшие действия заключены в цикл:
For i = 1 to 50 ... Next i
А внутри этого цикла мы, во-первых, сначала очищаем рабочее пространство на листе next для формирования следующего поколения:
rNext.ClearContents
А, во-вторых, запускаем вложенный цикл прохода по всем ячейкам игровой вселенной на листе game, чтобы проверить каждую из них — это реализовано циклом прохода по коллекции:
For Each cell in rGame.Cells ... Next cell
Ссылка на очередную проверяемую ячейку будет храниться в переменной cell. Для этой ячейки нам нужно сначала построить окрестность 3х3 с ней в середине. Это выполняется с помощью конструкции:
cell.Offset(-1, -1).Resize(3, 3)
Здесь метод Offset(-1,-1) виртуально сдвигает текущую проверяемую ячейку на одну строку вверх и на один столбец влево, а потом метод Resize(3,3) опять же виртуально растягивает эту одну ячейку до новых размеров 3 на 3:
Чтобы посчитать количество заполненных ячеек в полученной окрестности применяется функция рабочего листа СЧЁТЗ (COUNTA), которую в VBA можно вызвать с помощью объекта WorksheetFunction. Таким образом количество живых соседей в окружающей текущую ячейку области 3 на 3 мы получаем выражением (не забыв вычесть из полученного количества текущую ячейку):
n = WorksheetFunction.CountA(cell.Offset(-1, -1).Resize(3, 3)) - WorksheetFunction.CountA(cell)
Дальше нам нужно проверить полученное количество соседей и пометить на листе следующего поколения текущую ячейку как живую или мертвую, согласно правилам игры. Это выполняет блок из трёх проверок:
If cell = "" And n = 3 Then wNext.Cells(cell.Row, cell.Column) = 1 If cell = 1 And (n = 2 Or n = 3) Then wNext.Cells(cell.Row, cell.Column) = 1 If cell = 1 And (n < 2 Or n > 3) Then wNext.Cells(cell.Row, cell.Column) = ""
Когда цикл прохода по ячейкам будет завершен, то сформированное следующее поколение с листа next нужно скопировать на место текущего на листе game — делаем это уже знакомой конструкцией:
rNext.Copy Destination:=rGame
Вот, собственно, и вся логика.
Осталось вернуться в Excel на лист game, запустить нашу игру через вкладку Разработчик — Макросы (Developer — Macro) и насладиться процессом развития нашей колонии:
Ссылки по теме
- Что такое макросы и как их программировать в Microsoft Excel
- Справочник по игре «Жизнь» — сайт LifeWiki
В мир информатики # 91 (1-15 мая).
Microsoft Excel углубленно
Кто сам пилит свои дрова,
тот согревается дважды1.
Кто сам программирует свои
компьютерные игры, тот наслаждается дважды2.
Мы продолжаем (см. [1]) публикацию
материалов, посвященных моделированию
простейших игр средствами программы Microsoft Excel. В
этой статье мы рассмотрим известную игру
“Крестики-нолики”. Правила этой игры вы,
конечно, знаете.
На рис. 1 представлен вид рабочего
листа в начале новой игры (после щелчка на кнопке
с надписью “Новая игра”).
Рис. 1
Прежде всего надо оформить рабочее
поле игры — диапазон ячеек А6:С8:
— размеры строк и столбцов сделать
такими, чтобы каждая клетка поля имела форму
квадрата;
— установить соответствующие размеры
символов, которые будут записываться в каждую
ячейку диапазона (это будут буквы Х и О);
начертание символов предусмотреть полужирное;
— выравнивание текста в ячейках
сделать по центру.
Разместите на листе три кнопки. О том,
как это сделать и как разместить на кнопке
соответствующую надпись, рассказано в статье [1].
Там же описано, как создаются и “привязываются”
к кнопке макросы — программы, написанные на
языке программирования VBA, которые выполняются
по щелчку на той или иной кнопке.
Текст в ячейках А3 и А4 выводится по
щелчку на кнопке с надписью “Новая игра”, а
название игры в ячейку А1 следует ввести
“вручную”. По щелчку на этой кнопке очищаются
также 9 ячеек рабочего поля игры и ячейка E3.
Макрос, “привязанный” к кнопке с
надписью “Новая игра”, можно назвать
аналогичным именем:
Sub НоваяИгра()
Range(«A3»).Value = «Имя первого
игрока:»
Range(«A4»).Value = «Имя второго
игрока:»
‘Очищаем 9 ячеек рабочего поля
Range(«A6»).Value = «»
Range(«A7»).Value = «»
…
Range(«C7»).Value = «»
Range(«C8»).Value = «»
‘Готовимся принять имена играющих
‘Устанавливаем указатель
‘активной ячейки на ячейку E3
Range(«E3»).Activate
‘и очищаем ее
Range(«E3»).Value = «»
End Sub
Напомним, что:
1) запись Range(«E3»).Value (и подобные)
означает значение, записанное в ячейке с адресом
Е3. Это значение может использоваться в любом
месте программы — в операторе присваивания (в
его левой или правой части), в условном операторе
и т.д.;
2) запись Range(«E3»).Activate
соответствует установке указателя активной
ячейки (его часто называют “курсором”) на ячейку
Е3.
Вернемся к игре. В ходе нее после ввода
имен играющих в ячейки E3 и E4 нужно щелкнуть на
кнопке с надписью “Начало” — картина изменится
на приведенную на рис. 2. В ней в ячейке будет
отражаться имя игрока, делающего очередной ход.
Рис. 2
Макрос, связанный с кнопкой с надписью
“Начало”, имеет вид:
Sub Начало()
‘Очищаем ячейки А4 и Е4
Range(«A4»).Value = «»
Range(«E4»).Value = «»
‘Запоминаем имена участников игры
Имя1игрока = Range(«E3»).Value
Имя2игрока = Range(«E4»).Value
‘Выводим текст в ячейку А3
Range(«A3»).Value = «Очередной ход
делает»
‘Определяем начинающего игру
‘и выводим его имя в ячейке Е3
If Int(Rnd * 2) + 1 = 1 Then
‘Начинает 1-й участник
Range(«E3»).Value = Имя1игрока
НомерИгрокаДелающегоХод = 1
Else
‘Начинает 2-й участник
Range(«E3»).Value = Имя2игрока
НомерИгрокаДелающегоХод = 2
End If
‘Устанавливаем указатель
‘активной ячейки на ячейку D7
Range(«D7»).Activate
End Sub
— где Имя1игрока и Имя2игрока —
переменные величины, означающие имена
участников (эти имена в ходе игры будут
поочередно отражаться в ячейке Е3);
НомерИгрокаДелающегоХод — переменная
величина — номер участника игры, делающего
очередной ход.
Так как значения этих трех величин
будут использоваться в макросе, связанном с
кнопкой с надписью “Х/О”3, то они должны быть
описаны не в процедуре Начало, а как глобальные —
перед всеми макросами.
Option Explicit4
DIM Имя1игрока, Имя2игрока As String,
НомерИгрокаДелающегоХод As Integer
Теперь настало время рассказать, что
для простановки того или иного знака следует
установить (мышью или используя клавиатуру)
указатель активной ячейки на нужную клетку
рабочего поля, после чего щелкнуть на кнопке с
надписью “Х/О”. Поскольку знаки должны
чередоваться, макрос, связанный с этой кнопкой,
нужно оформить следующим образом:
Sub СтавимЗнак()
If НомерИгрокаДелающегоХод = 1 Then
‘Ставим крестик
ActiveCell.Value = «X»
‘Следующий ход будет делать другой
игрок
НомерИгрокаДелающегоХод = 2
‘Выводим его имя в ячейке Е3
Range(«E3»).Value = Имя2игрока
Else
‘Ставим нолик
ActiveCell.Value = «O»
‘Следующий ход будет делать другой
игрок
НомерИгрокаДелающегоХод = 1
‘Выводим его имя в ячейке Е3
Range(«E3»).Value = Имя1игрока
End If
End Sub
Но всегда ли можно ставить крестик или
нолик? Нет, конечно. Во-первых, знак нельзя
ставить в клетки (ячейки), не принадлежащие
рабочему полю игры. Это условие записывается в
виде:
ActiveCell.Address <> “$A$6” And ActiveCell.Address <>
“$A$7” And ActiveCell.Address <> “$A$8” And ActiveCell.Address <>
“$B$6” And ActiveCell.Address <> “$B$7” And ActiveCell.Address <>
“$B$8” And ActiveCell.Address <> “$C$6” And ActiveCell.Address <>
“$C$7” And ActiveCell.Address <> “$C$8”
Кроме того, нельзя ставить знак в
клетку, в которой уже что-то записано.
С учетом сказанного процедура
СтавимЗнак примет вид:
Sub СтавимЗнак()
If ActiveCell.Address<>»$A$6″ And
ActiveCell.Address<>»$A$7″ And … Then
MsgBox «Клетка вне рабочего поля!»
Else
If ActiveCell.Value <> «» Then
MsgBox «В этой клетке уже стоит знак!»
Else ‘Можно ставить знак
If НомерИгрокаДелающегоХод = 1 Then
ActiveCell.Value = «X»
НомерИгрокаДелающегоХод = 2
Range(«E3»).Value = Имя2игрока
Else
ActiveCell.Value = «O»
НомерИгрокаДелающегоХод = 1
Range(«E3»).Value = Имя1игрока
End If
End If ‘Можно ставить знак
End If
End Sub
Видно, что в приведенной процедуре нет
проверки на факт победы того или иного игрока
(такая проверка должна проводиться после каждой
простановки крестика или нолика). Связанные с
такой проверкой изменения процедуры СтавимЗнак
должны быть следующими:
…
Else ‘Можно ставить знак
If НомерИгрокаДелающегоХод = 1 Then
ActiveCell.Value = «X»
‘Проверяем, не выиграл ли 1-й игрок
If Range(«A6″).Value=»X» And
Range(«A7″).Value=»X» And
Range(«A8″).Value=»X» Or
Range(«B6″).Value=»X» And
Range(«B7″).Value=»X» And
Range(«B8″).Value=»X» Or … Then
MsgBox («Выиграл» + Имя1игрока)
Else ‘игра продолжается
НомерИгрокаДелающегоХод = 2
Range(«E3»).Value = Имя2игрока
End If ‘Конец проверки
Else
ActiveCell.Value = «O»
‘Проверяем, не выиграл ли 2-й игрок
If Range(«A6″).Value=»О» And
Range(«A7″).Value=»О» And
Range(«A8″).Value=»О» Or
Range(«B6″).Value=»О» And
Range(«B7″).Value=»О» And
Range(«B8″).Value=»О» Or … Then
MsgBox («Выиграл» + Имя2игрока)
Else ‘игра продолжается
НомерИгрокаДелающегоХод = 1
Range(«E3»).Value = Имя1игрока
End If ‘Конец проверки
End If
End If ‘Можно ставить знак
End If
End Sub
Внимание! При оформлении условия
победы того или иного игрока следует учитывать
одинаковое написание русских и латинских
символов “Х” и “О”.
И, наконец, следует проверить, не
окончилась ли игра вничью:
…
End If ‘Можно ставить знак
End If
‘Проверка на ничью
If … Then
MsgBox «Ничья!»
End If
End Sub
Условие, при котором игра закончилась
вничью, запишите самостоятельно. Обращаем
внимание на то, что если все 9 клеток рабочего
поля игры заполнены, то это не означает, что игра
закончилась вничью — один из играющих мог
выиграть в результате последнего, девятого, хода.
Литература
1. Игра “Быки и коровы” в среде Microsoft Excel. / “В
мир информатики” № 78 (“Информатика”
№ 19/2006).
1 Французская поговорка
2 Из книги: Арсак Ж. Программирование игр и
головоломок. М.: Наука, 1985
А ведь с помощью Excel и VBA можно не только решать сложные задачи. Можно и развлечься в пятницу вечером, сделав простую игру. Позабавить себя и коллег, прослыв душой компании:)
Итак приступим. Что же нам необходимо? Какие знания и умения? Сколько времени?
- MS Excel
- VBA
- 30 минут свободного времени
- Фантазия на все это
Итак, что же мы понапридумывали?
. Общая задумка — есть поле 20 на 10. В котором в случайном порядке появляется надпись Click ME!
. Мы должны на нее нажать в течении установленных в настройках секунд. Попали? Продолжаем дальше, набивая рекорды! Не попали? Что ж, GAME OVER
Более детально по пунктам:
- Максимальное количество секунд между кликами. Ставим меньше, чтобы более поразить своих друзей!
- Количество кликов в текущей игре. Чем больше — тем круче!
- Рекорды. Что тут сказать — Вы должны быть на 1 месте и точка.
- Само поле, где будут появляться те самые надписи
Click ME!
Итак, нажимаем на PLAY
и в поле появляется первый кандидат на клик мышкой:
Кликаем, пока хватит сил — занимаем первое место в рекордах. Если же не успели вовремя кликнуть, либо дрогнула рука — встречайте GAME OVER
В конце концов, потратив немного времени, мы создали простую и забавную игру в Excel!
Файл как обычно в конце блога! Создавайте свои мини-шедевры и будьте душой коллектива;)
Ссылка на файл
У нас появилось свободное время.. значит можно поиграть! В этой рубрике мы будем выкладывать простенькие игры в Excel. Да, в Excel тоже можно писать игры, ведь в нем встроен язык программирования VBA. После того как вы скачаете игру (по ссылкам ниже), необходимо будет только включить макросы (как это сделать описано тут). Во многих Ирах в Excel (в макросах) есть много интересных находок. Кто знает, может игра в Excel поможет вам не только скоротать время, но и научиться чему-то новому. Играйте в Excel и пишите игры сами!
Игра в Excel «Морской бой»:
Battleships
Игра в Excel «Змейка»:
snaked
Игра в Excel «Вертолет»:
gmexcopter
Игра в Excel «Квадрат»
Squares
Игра в Excel «Лягушка»
frog_leap
Игра в Excel «Темница»
dungeon
Если у вас есть чем поделиться — присылайте!
Спасибо за внимание.
Время на прочтение
1 мин
Количество просмотров 190K
Бухгалтер из Торонто по имени Кэри Уолкин (Cary Walkin) никогда не занимался разработкой компьютерных игр, но в совершенстве освоил Excel. Этого оказалось достаточно для создания RPG-игры, которая работает на макросах VBA, встроенного языка программирования Excel.
Игру Arena.Xlsm версии 1.0 можно скачать здесь:
v1-0-arena.xlsm (работает в Excel 2007 и Excel 2010 под Windows)
Ключевые особенности:
- Увлекательный сюжет с четырьмя различными концовками, в зависимости от прохождения игры
- Более 2000 врагов с различным уровнем ИИ
- 39 модификаторов вещей означает более 1000 возможных комбинаций и свойств предметов
- 8 схваток с боссами, для победы над каждым из них нужна особая тактика
- 4 предварительно подготовленных игровых пространства
- 31 заклинание, есть много разных стратегий для успеха
- 15 уникальных артефактов
- 36 наград (достижений)
- Вся игра от начала до конца проходит внутри рабочей книги Excel
Официальная wiki-страница для помощи по игре, там же информация о базовых стратегиях.
Так что если увидите бухгалтера в офисе, который целыми сутками сидит в программе Excel, присмотритесь к нему повнимательнее. Кто знает, какие заклинания он сейчас подбирает для босса на шестом уровне.
Теги:
- Arena.Xlsm
- Excel
- RPG
- рабочая книга Excel
- макрос
- VBA
Хабы:
- Ненормальное программирование
- Разработка игр
- Visual Basic for Applications
Перейдем к следующему примеру — необходимо разработать несложную игру. На рабочем листе (рис. 1.42) следует разместить пять окон для изображений (элементы управления типа «Изображение»). При этом два окна будут основными — расположены слева и большие по размеру. Другие три окна имеют меньшие размеры и расположены в правой части рабочего листа (они будут содержать эталонные изображения). Также на рис. 1.42 расположены две кнопки и две надписи, при этом надпись Результат является просто поясняющей, и далее мы ее упоминать не будем.
Теперь смысл игры. Предварительно в три маленьких окна в правой части листа выводятся три различные картинки (условно их можно называть эталонными). При щелчке на кнопке Бросок в каждое из двух основных окон для изображений должна попадать (случайным образом) одна из трех возможных картинок (из набора эталонных, которые видны на экране). Если в результате такого случайного выбора картинки в обоих основных окнах совпадают, то в элемент управления «Надпись» для результата добавляется 3 балла, а если нет, то вычитается 1 балл.
Рис. 1.42. Интерфейс разрабатываемой игры
Нам также потребуется датчик случайных чисел — здесь мы воспользуемся уже знакомой функцией Rnd. На первом этапе необходимо дать имена объектам в соответствии с табл. 1.2. Назначение элементов достаточно очевидно. Нескольких слов заслуживает элемент «Надпись» Res для отображения результата. В ней вычисляется суммарный результат по итогам серии произведенных бросков. А по щелчку на кнопке Начать игру снова значение результата обнуляется. Таким образом, вступительная часть перед программированием завершена, и поэтому перейдем к описанию программного кода.
Для того чтобы между сеансами работы на компьютере текущий результат сохранялся, отведем для него ячейку в первой строке на листе — М1. В начале в нее необходимо записать ноль. После каждого броска в эту ячейку программно будет записываться результат. В этом случае при сохранении книги предыдущий результат сохраняется в ячейке М1. Процедура, выполняемая при открытии книги, должна извлечь из ячейки М1 предыдущий результат, и тогда пользователь будет иметь возможность продолжить игру.
В качестве первого программного фрагмента оформим процедуру, выполняемую при открытии книги, так, как представлено в листинге 1.26. Здесь в надпись для результата подставляется содержимое ячейки М1 с первого листа книги.
1 2 3 4 |
' Листинг 1.26. Процедура, выполняемая при открытии книги Private Sub Workbook_Open() Worksheets(1).Res.Caption = Worksheets(1).Range("M1").Value End Sub |
Таблица 1.2. Имена объектов на рис. 1.42
Свойство Name | Тип объекта | Комментарий |
---|---|---|
Image1 | Изображение | Левое основное окно для изображения |
Image2 | Изображение | Правое основное окно для изображения |
ImageEtalon1 | Изображение | Левое окно для образца |
ImageEtalon2 | Изображение | Среднее окно для образца |
ImageEtalon3 | Изображение | Правое окно для образца |
Label1 | Надпись | Подпись «Результат» |
Res | Надпись | Для отображения результата |
Brosok | Надпись | Для выполнения броска |
NewGame | Надпись | Начать игру снова |
В листинге 1.27 приведена основная процедура, выполняемая по щелчку на кнопке с надписью Бросок.
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 |
' Листинг 1.27. Обработка щелчка на кнопке Бросок Private Sub Brosok_Click() Symma = Range("M1") Randomize ' Формирование случайных чисел a = CInt(Rnd * 3 + 0.5) b = CInt(Rnd * 3 + 0.5) ' Отображение картинки в первом окне If a = 1 Then Image1.Picture = ImageEtalon1.Picture ElseIf a = 2 Then Image1.Picture = ImageEtalon2.Picture Else Image1.Picture = ImageEtalon3.Picture End If ' Отображение картинки во втором окне If b = 1 Then Image2.Picture = ImageEtalon1.Picture ElseIf b = 2 Then Image2.Picture = ImageEtalon2.Picture Else Image2.Picture = ImageEtalon3.Picture End If ' Формирование результата If a = b Then Symma = Symma + 3 Else Symma = Symma - 1 End If ' Фиксирование результата в надписи и в ячейке на листе Res.Caption = Symma Range("M1").Value = Symma End Sub |
В тексте процедуры используется функция Rnd, которая нам уже встречалась. Преобразование CInt (Rnd * 3 + 0.5)
позволяет сформировать случайное целое число, которое может принимать одно из трех возможных значений: 1, 2 или 3. Таким образом, можно значения переменных а и b сопоставить с номерами картинок. В результате при совпадении картинок мы увеличиваем сумму выигрыша на 3 балла, а при несовпадении, соответственно, уменьшаем на один.
Если не предпринимать каких-либо действий, то функция Rnd при каждом повторном открытии книги выдает одну и ту же последовательность случайных чисел. Чтобы устранить данный недостаток, мы воспользовались функцией Randomize, которая выполняет перемешивание случайных чисел. В результате при различных сеансах работы последовательность, выдаваемая по нажатию кнопки Бросок, каждый раз будет разная.
Другая кнопка на листе (NewGame) позволяет сбросить содержимое ячейки М1 и значение свойства Caption элемента Res в ноль для начала новой игры (листинг 1.28).
1 2 3 4 5 6 |
' Листинг 1.28. Процедура сброса суммы баллов в ноль Private Sub NewGame_Click() Symma = О Res.Caption = Symma Range("M1") = Symma End Sub |
Один из вариантов развития игры показан на рис. 1.43.
Рис. 1.43. Один из сценариев развития игры
Математика, Разработка игр, Дизайн игр
Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе — https://katalog-kursov.ru/
В этой части мы решим задачи об оптимальном размещении оружия на танке, пространственном расположении телепортов в MMORPG и сбалансируем бои четырёх классов персонажей RPG.
Задачи о размещении объектов
Электронные таблицы для этой части можно скачать здесь: (SuperTank) (телепорты, часть 1) (телепорты, часть 2)
SuperTank: задача решена!
В первой статье серии мы рассказали о примере задачи для игры под названием SuperTank. Во второй её части, мы познакомились с основными концепциями моделирования решений и я рассказал о решении простого примера с помощью инструмента «Поиск решений» в Excel.
Теперь мы можем применить полученные во второй части знания к задаче SuperTank, и доказать, что с их помощью можно решить эту задачу легко и быстро. Освежу вашу память: SuperTank — это игра, в которой вы можете сражаться на настраиваемом танке. Супертанк выглядит примерно так:
У каждого супертанка может быть любое количество орудий пяти различных типов:
На супертанк может поместиться 50 тонн оружия, а игрок может потратить 100 кредитов. Также у супертанка есть 3 «критических слота», в которые помещаются такие специальные пушки, как MegaRocket и UltraLaser.
Электронную таблицу для этого примера можно скачать здесь.
Цель заключается в том, чтобы подобрать вооружение, максимизирующее наносимый супертанком урон, не выходя при этом за ограничения в 50 тонн, 100 кредитов и 3 критических слота. Также мы предполагаем, что в этой таблице находится вся необходимая информация, и что такие факторы, как дальность, частота и точность стрельбы к делу не относятся или уже учтены в параметре Damage соответствующего оружия.
Чтобы оптимизировать эту схему, мы сначала внесём эти данные в электронную таблицу. Сразу под ней мы добавим ещё одну таблицу, в которых будет набор из 5 «количественных» ячеек для указания количества каждого из 5 типов оружия.
Пока мы введём в эти ячейки значение 1, просто чтобы протестировать их работу, но это будут наши ячейки решений — мы попросим инструмент «Поиск решений» (Solver) найти правильные значения этих ячеек. Понять, что это ячейки решений, можно по жёлтой окраске, потому что мы продолжаем следовать правилам форматирования, изложенным во второй части. Справа от «количественных» ячеек мы добавим ячейки вычислений, которые будут умножать значения количества в ячейках решений на значения Damage, Weight, Cost и Critical Slots из таблицы выше. Таким образом, каждая строка этой таблицы будет верно отображать урон, вес, цену и критические слоты, требующиеся для всех использованных пушек во всех категориях вооружений.
Также мы создадим ниже раздел, в котором будут суммироваться все значения количества, веса, стоимости и критических слотов из таблицы выше, и сравниваться с максимальными значениями веса, стоимости и критических слотов, указанных в условиях задачи (соответственно 50, 100 и 3).
В соответствии с правилами форматирования из второй части статьи, синие ячейки наверху являются критериями из условий задачи. Серые ячейки — это ячейки вычислений, представляющие общие значения веса, стоимости и критических слотов на основании суммирования из таблицы количества (т.е. общих значений столбцов Weight x Quantity, Cost x Quantity, и Critical Slots x Quantity). Наконец, оранжевая ячейка представляет общий урон нашего супертанка, полученный на основании общего урона столбца Damage x Quantity из таблицы выше.
Прежде чем мы приступим к решению, давайте сделаем нашу электронную таблицу более дружелюбной к пользователю. Мы воспользуемся возможностью Excel присваивать каждой ячейке название и дадим понятные названия семи ячейкам в последней таблице вычислений. Это необязательно, но в дальней перспективе это позволит электронной таблице выглядеть гораздо понятнее (например, если вместо $F$21 ячейка будет называться MaxCriticalSlots). Для этого мы просто выбираем ячейку и переходим в поле ввода названия слева от поля формул, и вводим новое название.
Теперь наконец-то давайте перейдём в Excel Solver и найдём решение (перейдите в правую часть вкладки Data («Данные») и выберите Solver («Поиск решений»). Если вы её не видите, то зайдите в Options («Параметры») Excel, выберите категорию Add-Ins («Надстройки»), убедитесь, что в раскрывающемся списке Manage («Управление») выбрано Excel Add-Ins («Надстройки Excel»), нажмите Go («Перейти…») и убедитесь, что поставлен флажок Solver Add-in («Поиск решения»).
В поле Set Objective («Оптимизировать целевую функцию») мы выберем оранжевую ячейку цели, а ниже нажмём на радиокнопку Max («Максимум»). В поле By Changing Variable Cells («Изменяя ячейки переменных») выберем ячейки решений (жёлтые ячейки в столбце Quantity второй таблицы). Ниже нажмём на кнопку Add («Добавить»), чтобы добавить следующие ограничения:
- Значения ячеек решений должны находиться в интервале от 0 до какого-то разумного максимума (мы выбрали 50, даже несмотря на то, что это вероятно намного большее предельное значение, чем нужно). Также необходимо задать каждой ячейке решения ограничение «= integer» («цел»), потому что у нас не может быть дробной части вооружения, а Excel Solver считает по умолчанию каждую переменную вещественным числом, если не указать обратное.
- Также нам нужно ограничить значения общей стоимости, общего веса и общего количества критических слотов значениями из условий задачи. На изображении диалогового окна видно, что теперь у них есть удобные названия, которые мы добавили в нижнюю таблицу, благодаря чему диалоговое окно читается проще.
Теперь мы нажмём на кнопку Solve («Найти решение») и после краткого ожидания Solver заполнит значения Quantity, что даст нам следующее:
- 1 Machine Gun
- 3 Rockets
- 2 MegaRockets
- 1 Laser
- 1 UltraLaser
Всё это даёт нам общий урон в 83 единиц и занимает ровно 50 тонн, 100 кредитов и 3 критических слота. Можно увидеть, что наилучшее решение не меняется от времени выполнения Solver. Если сбросить эти значения и выполнить повторную оптимизацию, или перейти в Options и изменить seed, то мы всё равно получим те же значения. Мы не можем быть уверенными на 100%, что это решение оптимально, но с учётом того, что у Solver не получилось усовершенствовать его после нескольких проходов оптимизации, то с большой вероятностью он и является реальным оптимумом, а не просто локальным максимумом.
Задача решена!
Дополнительные способы использования
Здорово здесь то, что мы не только решили задачу гораздо быстрее, чем справились бы вручную, но и настроили её таким образом, что она позволит протестировать, какое оружие в игре SuperTank будет наиболее полезно с разными параметрами (вес, стоимость, критические слоты). Это значит, что мы сможем относительно просто изменить влияние различных изменений этих параметров на игру SuperTank, и если мы захотим добавить новую альтернативную модель супертанка, который будет легче, тяжелее или иметь другое количество критических слотов, то это можно будет сделать очень просто.
Изменяя все эти параметры, мы сможем также получить понимание относительной полезности каждого из этих вооружений, и быстро определять, какое из них слишком полезно, недостаточно полезно, имеет цену, неподходящую к её весу и урону, и так далее.
Повторюсь, смысл в том, что подобный инструмент позволяет нам выполнять поиск по пространству дизайна гораздо быстрее, чем мы бы смогли вручную. Он обеспечивает нам удобную возможность оценить эффект подобных изменений при любом инкрементном дизайнерском решении, которое мы можем придумать, будь то изменение параметров оружия или самого супертанка, добавление нового вооружения или моделей супертанков, а также добавление новых параметров (допустим ограничение размера в кубических метрах).
Чтобы понять, что я имею в виду, перейдите к синей ячейке «Max Cost» и измените её значение с 100 на 99. Теперь запустите Solver заново, и вы получите совершенно другую схему размещения оружия:
- 0 Machine Guns
- 2 Rockets
- 3 MegaRockets
- 3 Lasers
- 0 UltraLasers
Такая схема даёт чуть меньший показатель урона (82 вместо 83), но она радикально отличается от предыдущей.
Если присвоить Max Cost значение 101 или 102, и выполнить расчёт заново, то есть вероятность, что мы получим конфигурацию, похожую первой или совпадающую с ней; как бы то ни было, урон останется равным 83 (схемы могут меняться, потому что в таких случаях есть несколько оптимальных схем). Однако если присвоить Max Cost значение 103, то вы должны получить следующее:
- 1 Machine Gun
- 4 Rockets
- 2 MegaRockets
- 0 Lasers
- 1 UltraLaser
Что увеличивает общий урон до 84.
Это интересно: такая схема размещения оружия очень отличается от первых двух.
Как вы видите, мы получаем неожиданный результат: оптимальный выбор оружия в нашей схеме сильно зависит от параметров супертанка и может значительно меняться даже при небольших изменениях в этих параметрах. Кроме того, это даёт нам всевозможную полезную информацию: все пять типов оружия полезны по крайней мере в двух из трёх настройках супертанка, а Rockets и MegaRockets очевидно полезны во всех трёх. Похоже, это говорит нам, что все пять видов оружия хорошо сбалансированы, то есть полезны относительно друг друга, и в то же время они остаются уникальными.
И как можно также заметить, подобные моделирование и оптимизация решений предоставляют нам отличную возможность быстро выполнять поиск в локальной окрестности и повторную оптимизацию. При некоторых типах задач оно позволит нам обнаружить доминирующие стратегии и эксплойты игроков, которые трудно или невозможно найти любым другим способом.
Телепорты-«червоточины»
Посмотрев на последние два примера (пример с налоговыми ставками в стратегической игре и SuperTank), вы можете подумать, что такие техники применимы только в случаях, когда пользователи имеют дело с числами. Но вы будете абсолютно не правы! Как мы увидим, существует множество примеров того, что можно получить преимущества от оптимизации элементов дизайна, которые не только не выглядят для пользователей числами, но и вообще не похожи на них!
Также вы можете думать, что моделирование решений применимо только к решениям, которые могут принимать в играх игроки. Это тоже неверно: в некоторых случаях их можно использовать для моделирования, чтобы оптимизировать собственные решения как дизайнера.
Допустим, вы работаете над космической MMORPG. Однажды ваш ведущий дизайнер подходит к вам с видимой тревогой на лице. «Мы завершаем редизайн сектора Омега», — говорит он. «И у нас возникла проблема. Мы планируем добавить несколько телепортов-»червоточин» в этом сегменте мира, но не можем договориться, где их размещать».
«Сколько телепортов?», — спрашиваете вы.
«Мы пока не знаем. Вероятно, три, но их может быть от двух до четырёх. Мы ещё не уверены». Потом он показывает вам карту, которая выглядит вот так:
«Что это?», — спрашиваете вы.
«Это карта сектора Омега. Или, по крайней мере, звёздные системы, которые игрок может посетить в этом квадранте. Нам нужно определить, в каких клетках должны быть „червоточины“».
«Ну ладно, а по каким правилам они размещаются? Можно ли размещать „червоточину“ в одном квадранте со звёздной системой?»
«Мы хотим, чтобы ты разместил „червоточины“ таким образом, чтобы минимизировать расстояние от любой звёздной системы до ближайшей „червоточины“. И да, можно помещать их в тот же квадрант, что и звёздная система; это просто небольшие телепорты, висящие в космосе, поэтому их можно помещать где угодно. И помни, что мы ещё не решили, сколько их должно быть, так что дай мне решения для 2, 3 и 4 „червоточин“».
Как сформулировать эту задачу, и как её решить?
Оптимизируем телепорты!
Давайте начнём с подготовки ячеек решений. Обозначим четыре телепорта как A, B, C и D. Мы знаем, что каждый телепорт по сути является не чем иным, как координатами (x,y) на звёздной карте сектора Омега. Также мы знаем, что нам понадобится какой-то способ указания количества активных телепортов, поэтому мы добавим ячейку, позволяющую задать количество телепортов. Телепорт D мы используем только в случае, когда используются 4 «червоточины», а C — только когда у нас есть 3 или больше.
Ниже мы подготовим таблицу для вычисления расстояния от каждой звёздной системы до ближайшего телепорта. Эта таблица выглядит так:
Слева синим показаны координаты каждой звёздной системы на карте. Каждая строка — это одна звёздная система. Мы просто перенесли их из карты сектора Омега, которую нам дал ведущий дизайнер.
Справа мы вычисляем расстояние до каждого из четырёх телепортов. Это просто теорема Пифагора. Расстояние вычисляется как квадратный корень из горизонтального и вертикального расстояния между звёздной системой и телепортом:
=SQRT(($B14-Ax)^2+($C14-Ay)^2)
(Не волнуйтесь — я обещаю, что это самая сложная математика, которая нам встретится в серии!)
Мы берём координаты X и Y каждой звёздной системы из синих ячеек таблицы выше, а координаты X и Y каждого телепорта (ячейки с названиями Ax и Ay для телепорта A в показанной выше функции SQRT()) — из жёлтых ячеек решений сверху.
Наконец, мы берём минимум из этих четырёх значений в столбце Dist to Closest, то есть просто используем функцию MIN() для определения минимума четырёх значений слева. Затем мы внизу суммируем весь столбец; сумма и является ячейкой цели.
Вы могли заметить, что на скриншоте выше все ячейки имеют значение Dist to D. Причина в том, что мы используем ячейку «Number of Teleporters?» в верхнем разделе модели решений, позволяющую настроить количество учитываемых телепортов. Если количество телепортов равно 2, то мы используем значение 99 и в Dist to C, и в Dist to D, а если оно равно 3, то значение 99 используется только в столбце Dist to D. Благодаря этому каждая звёздная система будет игнорировать все лишние телепорты при вычислении расстояния до ближайшего телепорта в случае 2 или 3 телепортов.
Теперь мы запустим Solver:
Ячейка цели — это сумма внизу столбца Dist to Closest. Заметьте, что в отличие от других примеров, здесь мы хотим использовать радиокнопку «To: Min» («До: Минимум»), потому что нам нужно минимальное расстояние между всеми звёздными системами и телепортами, а не максимум.
Ниже мы укажем в качестве ячеек решений («By Changing Variable Cells») восемь жёлтых ячеек решений координат X и Y «червоточин» A, B, C и D. В разделе ограничений мы ограничим каждую из координат как целочисленное значение в интервале от 0 до 12. Заметьте, что мы используем для этих ячеек решений целочисленное ограничение, потому что подразумеваем, что ведущий дизайнер просто хочет знать, в какой ячейке будет каждый телепорт, но мы можем запросто пропустить это ограничение, если бы дизайнеру понадобились вещественные координаты.
Если мы зададим для «Number of Teleporters?» значения 2, 3 и 4, и последовательно будем запускать Solver при каждом значении, то получим следующие конфигурации:
Имея эту информацию, мы можем подойти к ведущему дизайнеру и показать ему оптимальные места для расположения любого количества телепортов в интервале от 2 до 4. Вот как оптимальные расположения «червоточин» для 2, 3 и 4 телепортов выглядят на карте (показаны зелёным).
Электронную таблицу для этого примера можно скачать отсюда.
Я говорил о ниндзя?
«Потрясающе», — говорит ведущий дизайнер, но на лице его вы видите страдание. «Эээ, но я забыл сказать тебе, что некоторые из этих систем населены космическими ниндзя. И мы хотим, чтобы системы с ниндзя были дальше от „червоточин“, чтобы игроки не ощущали чрезмерной угрозы».
«Ого. Это полностью меняет дело».
«Точно. Кроме того, в некоторых звёздных системах есть не одна, а две колонии, то есть им вдвое важнее находиться близко к телепортам. Или вдвое важнее находиться дальше, если это система с двумя колониями космических ниндзя. Вот, как выглядит карта теперь:»
Он продолжает: «Каждое отрицательное число — это колония космических ниндзя. Система с числом 2 содержит две человеческие колонии, а с числом -2 — две колонии ниндзя. Можешь сказать, где разместить телепорты в этом случае?»
«Скажи, ну вы хотя бы решили уже, сколько будет телепортов: 2, 3 или 4?», — спрашиваете вы язвительно.
«Боюсь, что пока нет».
Решаем с учётом ниндзя
Чтобы решить эту задачу, нам нужно добавить в таблицу новый столбец, обозначающий веса таблицы. Мы назовём его «множителем» (multiplier). Мы просто будем умножать это значение на значение в столбце «Dist to Closest».
Когда мы это делаем, Dist to Closest слегка изменяет свой смысл. Теперь это не расстояние до ближайшей звёздной системы, потому что для звёздных систем ниндзя значение меняется в -1 раз. Оно больше напоминает обобщённые «очки» (score), поэтому давайте так их и назовём.
Таким образом, очки теперь обозначают совокупное значение. Минимизируя его, мы делаем так, чтобы Solver стремился быть как можно ближе к системам с человеческими колониями и одновременно как можно дальше от населённых ниндзя систем.
Теперь мы получаем следующие результаты:
Как видите, это даёт нам конфигурацию телепортов, в каждом случае сильно отличающуюся от более простых версий без ниндзя.
Электронную таблицу для этой расширенной версии примера с телепортами можно скачать отсюда.
Как видите, наша модель решений смогла очень быстро решить эту нетривиальную задачу, и мы можем адаптировать её к меняющимся требованиям.
Эта задача относится к классу задач, называемых «задачами о размещении объектов», которые очень хорошо изучены в области оперативного управления. Но как видите, потенциально их можно применять и в гейм-дизайне, а также в дизайне уровней, а решение просто (если не тривиально) находится в Excel.
Балансировка классов для боёв Player-vs-Player
Электронную таблицу для этой части можно скачать отсюда: ссылка
Электронные таблицы и симуляции
В предыдущих трёх частях этой серии статей мы познакомились с концепцией моделирования и оптимизации решений, а также с инструментом «Поиск решений» (Solver) пакета Excel. Мы показали, как их можно использовать для вычисления оптимальных налоговых ставок города в 4X-стратегии, для определения оптимального размещения телепортов в космической игре и для выбора оптимальной схемы расположения оружия для задачи с супертанком, описанной в первой части.
Возникает естественный вопрос: а как насчёт балансировки игры? Можно ли применять подобные техники к всевозможным видам задач сложной балансировки, которые встречаются во множестве разных типов игр, в частности, в стратегиях, RPG и MMORPG?
Ответ на этот вопрос: да, разумеется, но со множеством оговорок. Электронные таблицы в особенности имеют множество ограничений, потому что в большинстве нетривиальных случаев они не точно описывают игру. Поэтому нам сложно будет выполнять надёжную балансировку с помощью техник оптимизации; реальные задачи балансировки подавляющего большинства игр будут далеко за пределами того, что мы можем смоделировать в электронной таблице. Сама по себе симуляция игры обычно бывает слишком сложной, имеет очень много «подвижных частей» и часто выполняется в реальном времени, при попытке дискретной симуляции мы можем столкнуться со всевозможными проблемами.
Поэтому если бы мы хотели использовать подобные техники для балансировки классов в таких MMORPG, как WildStar или в стратегических играх наподобие Planetary Annihilation, то для обеспечения хотя бы какой-то точности и полезности нам бы пришлось интегрировать их в саму симуляцию игры.
Кроме того, истина заключается в том, что некоторые аспекты балансировки невозможно автоматизировать; как мы объяснили в первой части статьи, ощущения от игры автоматически настраивать невозможно.
Поэтому лучшее, на что нам стоит надеяться — это демонстрация простого примера, иллюстрирующего общий подход к задачам такого типа: на простом примере в Excel мы узнаем, как подходить к формулировке такого типа задач балансировки и оптимизировать их. Мы покажем, что по крайней мере для примера простого боя Solver может хорошо выполнять балансировку нескольких RPG-классов относительно друг друга. Потом вы сможете использовать эту базовую структуру как основу для решения подобных задач оптимизации с более сложной схемой и глубже интегрированных в симуляцию игры.
Мы надеемся, что вместе с нами вы изучите все хитрости и увидите, что нам может дать этот простой пример.
Балансировка не определена
Не существует единого, общепринятого определения слова «балансировка». Оно имеет множество значений, и истинное обычно зависит от контекста рассматриваемой игры. В разных условиях балансировка может быть связана с настройкой нескольких классов персонажей с целью равенства их возможностей в ролевой игре, с количеством сил противников, сражающихся друг против друга в стратегической игре или с подгонкой стоимости различных юнитов или ресурсов в соответствии с их полезностью.
Наилучшее определение «балансировки» обычно зависит от целей дизайна рассматриваемой игры, но так как эти цели могут быть любыми, то невозможно априори определить, что балансировка на самом деле означает для игр в целом.
Некоторые игроки склонны считать, что балансировка в бою означает равный урон. Это особенно относится к MMORPG, в которых игроки часто жалуются, что величина урона в секунду (damage per second, DPS) одного класса слишком мала или слишком велика относительно других.
Разумеется, классы невозможно балансировать только по DPS; вполне допустимо, чтобы один класс имел больший DPS, чем другой, но это должно компенсироваться другими факторами, ограничивающими общую полезность класса, например, пониженная выживаемость или меньший долговременный DPS по сравнению с кратковременным DPS.
Крошечная MMO
Представьте, что мы создаём новый проект, очень упрощённую многопользовательскую массовую онлайновую ролевую игру под названием «Tiny MMO». В рамках разработки дизайна мы стремимся сбалансировать четыре класса для боёв «игрок против игрока» (PVP) таким образом, чтобы все четыре класса были относительно равными в бою друг против друга, и чтобы не было явного «лучшего» или «худшего» класса, которым можно сражаться против других классов.
Хоть «Tiny MMO» и является игрой реального времени, действие каждого игрока длится ровно 3 секунды, поэтому мы можем дискретизировать её, представив в виде пошаговой игры, в которой каждый ход является трёхсекундной долей геймплея.
Игроки в этой игре могут выбрать один из четырёх классов персонажей:
- Warrior (Воин) наносит наибольший урон
- Mage (Маг) кастует заклинания на расстоянии и имеет набольшую дальность атаки из всех четырёх классов
- Healer (Хилер) автоматически лечится, восстанавливая за каждый ход определённую часть своего здоровья
- Barbarian (Варвар) имеет больше всего здоровья
Это всё, что мы знаем об этих четырёх классах, и нам нужно задать изначальные параметры здоровья (HP), урона, лечения и дальности атак для всех четырёх классов. Нам нужно сбалансировать их таким образом, чтобы каждый класс был уникальным и его характеристики значительно отличались от всех других классов, но чтобы в результате каждый класс оказался как можно более «сбалансированным» относительно трёх остальных.
Другими словами, мы стремимся оптимизировать следующую таблицу:
Пока мы используем временные значения и предполагаем, что каждый класс начинает с 50 HP, наносит при атаке 10 единиц урона за ход, излечивает 0 HP за ход и имеет дальность атаки 40 метров. Каждый персонаж движется со скоростью 10 метров за ход. Так как в дизайне указано, что все четыре класса персонажей могут двигаться с одной скоростью, то мы будем считать это значение постоянным, и не станем вносить скорость движения в таблицу переменных решений.
Очевидно, что это учебный пример с очень упрощённой моделью урона. Это непрерывное усреднённое значение урона в секунду, которое игнорирует отличия импульсного урона от длительного урона, а также ману и другие механики, модифицирующие атакующие способности классов. У нас будет только один тип урона, что довольно нереалистично, потому что у большинства классов есть десятки типов урона, и нам нужно будет реализовывать систему ИИ, выбирающую атаку в каждом ходу. Кроме того, в большинстве игр урон имеет элемент случайности, но мы пока это опустим и предположим, что вариативность урона не так велика, чтобы значительно влиять на результат боя между двумя классами.
Разумеется, любая балансировка, выполняемая в Excel, вряд ли будет идеальной или соответствующей окончательному балансу игры; она должна будет пройти множество итераций плейтестинга. Но если мы уделим один-два часа на получение хорошего первого варианта для нашей игры в Excel, то по крайней мере мы с гораздо большей вероятностью приблизимся к качественным параметрам первоначального баланса, что сильнее приблизит нас к тому конечному балансу, который мы хотим получить.
Таблица побед
Нам нужно сбалансировать четыре класса с каждым другим в бою один на один. Так как у нас только 4 класса (Warrior, Mage, Healer и Barbarian), то всего есть 6 возможных комбинаций разных классов:
- Warrior — Mage
- Warrior — Healer
- Warrior — Barbarian
- Mage — Healer
- Mage — Barbarian
- Healer — Barbarian
Подобная балансировка может быть достаточно сложной. Даже в нашем довольно простом случае с четырьмя классами у нас получились шесть междуклассовых соотношений, так же, как мы можем провести шесть прямых между четырьмя точками квадрата.
Каждый раз, когда мы захотим внести даже небольшое изменение в один из параметров любого из классов, то это изменение также повлияет на балансировку PvP между этой парой классов и другими двумя классами. Эта степенная взаимосвязанность при увеличении количества классов будет только расти, и решения о балансировке PvP между любой парой классов, принимаемые «в вакууме», без учёта всех других взаимодействий, могут стать очень опасными.
В идеале нам хотелось бы создать некую таблицу побед наподобие показанной ниже. Если мы сможем смоделировать в электронной таблице бой между каждой из этих 6 пар, то у нас получится сгенерировать некую переменную «очков» для каждой из 6 пар. Чем больше очков, тем лучше, поэтому мы сможем скомбинировать все эти шесть значений очков, чтобы сгенерировать функцию цели.
Заметьте, что в показанной выше таблице ячейки вдоль диагоналей равны нулю, потому что они обозначают пары одного и того же класса, которые будут сбалансированными по определению. Кроме того, ячейки в правом верхнем углу тоже равны нулю, потому что они обозначают точно такие же пары, что и в ячейках внизу слева.
Теперь давайте подготовим модель для боя между двумя разными классами.
«Симулятор боя»
Расположим каждую пару классов на расстоянии 100 метров друг от друга. Каждый персонаж имеет 3 секунды на атаку, поэтому мы можем представить это как пошаговую симуляцию, в которой каждый «ход» обозначает 3 секунды. На каждом «ходу» каждый персонаж или атакует другого, если он находится в пределах дальности атаки, или продолжает двигаться, чтобы сократить расстояние.
Симуляция выглядит так:
Наверху показана пара персонажей, вступивших в бой: в данном случае это Mage (класс 1) и Healer (класс 2). В левом столбце показано текущее расстояние между двумя симулируемыми персонажами.
Для каждого персонажа столбцы будут такими:
- Max Range: это максимальное расстояние, на котором персонаж может атаковать. Оно берётся непосредственно из жёлтых переменных решений в таблице переменных решений.
- Healing: это величина лечения персонажа за ход, получаемая непосредственно из таблицы переменных решений.
- HP: это здоровье персонажа в каждом ходе. Изначально оно равно соответствующему значению HP из таблицы переменных решений, но со временем при атаках другого персонажа уменьшается. Также оно увеличивается в каждом ходе на величину лечения, которую персонаж может применить к себе в каждом ходе.
- Damage: величина урона, наносимого персонажем врагу, когда тот находится в пределах дальности атаки. Когда персонаж умирает, это значение снижается до 0.
- Attacks?: этот столбец проверяет, находится ли персонаж в пределах дальности атаки. Если да, то это будет означать, что в текущем ходе персонаж атакует; если нет, то персонаж перемещается ближе, чтобы дойти до другого персонажа.
Таким образом, оба персонажа начинают двигаться друг к другу, а затем атакуют, пока один из них или оба не умрут. Каждый персонаж перемещается за каждые 3 секунды на 5 метров (5 метров за «ход»). Когда оба персонажа движутся друг к другу, то Range будет изменяться в каждом ходу на 10 единиц, и на 5 единиц, если движется только один из них. Сама игра структурирована так, что оба персонажа могут начать двигаться одновременно, после чего ход разрешается одновременно, поэтому вполне возможно, что оба персонажа могут умереть одновременно.
Далее нам нужно настроить подсчёт очков для этой таблицы и сгенерировать численное значение, обозначающее, насколько «хорошим» был бой; другими словами, насколько близко мы оказались к достижению наших дизайнерских целей.
Очевидно, что мы хотим, чтобы к концу боя оба персонажа были мертвы, или, по крайней мере, были как можно ближе к смерти. Если бой сбалансирован, то оба из сражающихся классов должны максимально снизить здоровье противника в конце боя.
Однако самого по себе этого недостаточно. Если мы организуем подсчёт очков таким образом, то оптимизатор просто максимально увеличит значения урона, чтобы оба персонажа мгновенно убили друг друга! (Если вам любопытно, то попробуйте изменить приложенную к статье электронную таблицу, чтобы убедиться в этом самостоятельно). Очевидно, что мы стремимся не к мгновенной смерти: нам нужно, чтобы к концу боя оба персонажа были мертвы или почти мертвы, но в то же время мы хотим, чтобы бой длился разумное количество времени.
Другими словами, мы не только стремимся обеспечить относительно равную балансировку всех классов против друг друга; мы ещё и хотим сделать так, чтобы баланс был интересным, в том числе и чтобы бои длились подходящее количество времени.
Чтобы сгенерировать такую оценку баланса, нам нужно создать несколько ячеек справа от каждой таблицы. Duration обозначает длительность боя; она подсчитывает количество строк таблицы, в которых оба персонажа пока живы. Total HP подсчитывает общую сумму хитпоинтов двух выживших персонажей. В идеале она должна быть равна 0, то есть к моменту завершения боя оба персонажа умирают.
И. наконец, Score комбинирует длительность и общую сумму хитпоинтов в виде ( Duration / ( 1 + Total HP ) ). Заметьте, что мы прибавили к делителю 1, потому что Total HP может быть равно 0, и в таком случае мы бы получили ошибку деления на ноль. Таким образом мы можем гарантировать, что вознаграждаем оптимизатор за нахождение максимальной длительности боя и минимального значения суммы хитпоинтов.
(Заметьте, что поскольку в каждой «симуляции» боя класса против класса у нас есть 17 строк. Это значит, что мы по сути приняли дизайнерское решение о том, что бой должен длиться примерно 17 раундов. Если мы хотим, чтобы бой был короче или дольше, то можно изменить количество строк, соответствующим образом отредактировать формулы подсчёта оценки и выполнить повторную оптимизацию.)
Наконец, мы берём эти шесть значений Score (по одному для каждой таблицы) и используем их в представленной выше «Таблице побед», чтобы показать результаты боя между каждой из пар классов.
Можно просто суммировать эти шесть значений оценок и использовать результат как финальное значение Score. Однако, если мы это сделаем, то Solver с большой вероятностью не сможет найти хорошего баланса между наибольшей и наименьшей оценкой для отдельных боёв, а также получит очень высокие оценки для некоторых пар классов и низкие оценки для других. Мы хотим не этого: нам нужно чтобы все оценки были высокими и мы стремимся повышать их все. Чтобы исправить это, мы умножим сумму оценок на наименьшую оценку в группе (с помощью функции Excel MIN()), чтобы заставить Solver сосредоточиться на оценках с наименьшим значением.
Добавляем ограничения
Мы пока не закончили. Если оптимизировать модель решений с текущими параметрами, то, скорее всего, классы будут настроены неправильно — на самом деле, высока вероятность, что модель запишет в таблицу переменных решений одинаковые значения HP, Damage, Healing и Range.
А мы, разумеется, хотим, чтобы у каждого класса была собственная индивидуальность. Нам нужно, чтобы Warrior наносил наибольший урон, Mage имел самый большой Range, Healer имел максимальное значение Healing, а Barbarian обладал наибольшим HP. Также мы хотим, чтобы эти различия не были слишком маленькими — нам нужно чтобы, эти классы сильно отличались друг от друга.
Для этого мы создадим небольшую таблицу ограничений. Эта таблица гарантирует, что каждый из четырёх классов будет иметь соответствующий атрибут, после чего давать оценку 0 или 1, в зависимости от того, удовлетворено ли условие ограничения.
В таблице Min difference справа указана минимальная разность каждого атрибута класса относительно всех других классов. Другими словами, Warrior должен иметь по крайней мере на 4 HP больше урона, чем все другие классы, Mage должен иметь дальность атаки по крайней мере на 10 больше, и так далее.
Теперь, когда мы добавили эти особые ограничения, настало время оптимизировать!
Поиск решений
Теперь мы можем запустить встроенный в Excel инструмент Solver («Поиск решений»), чтобы попытаться оптимизировать исходные параметры. В качестве ячейки цели мы выберем ячейку Score, которая комбинирует результаты всех шести турниров. Мы задаём переменные решений так, чтобы включить в них все 16 ячеек в жёлтой таблице Decision variables, которую мы создали в начале.
Также мы задаём ограничения (в поле Subject to the Constraints) следующим образом:
- Все ячейки решений должны быть целочисленными с минимальным значением 0.
- Все ячейки в столбце HP должны иметь максимальное значение 200 и минимальное 30.
- Все ячейки в столбце Damage имеют максимальное значение 20.
- Все ячейки в столбце Healing имеют максимальное значение 15.
- Все ячейки в столбце Range имеют максимальное значение 100.
- Кроме того, все четыре ячейки в особом разделе Constraints должны иметь значение 1, чтобы удовлетворялись их особые условия.
Наконец, выберем в качестве Solving Method значение Evolutionary и запустим Solver. Учтите, так как это эволюционный алгоритм, существует вероятность улучшения найденного решения при втором или третьем прогоне Solver, или после настройки параметров (кнопка Options) для эволюционной оптимизации.
В результате у нас должно получиться нечто подобное:
… и как по волшебству Solver дал нам хорошую исходную конфигурацию баланса.
Как видите, Warrior теперь наносит наибольший урон, Mage имеет наибольшую дальность, Healer лучше всех лечит, а у Barbarian больше всех HP. Кроме того, можно опуститься к результатам отдельных турниров «класс против класса» и увидеть, как классы проявили себя в бою друг с другом; как видно, большинство из них сбалансировано очень равномерно — к концу боя оба класса умирают, или один из них едва выживает. К тому же все турниры длятся достаточно долго, ни один из классов не может «ваншотнуть» другой.
Неплохо для нескольких часов работы, правда?
Заключение
В этом примере мы создали простую задачу балансировки и продемонстрировали, что на самом деле мы можем решить её с помощью симуляции и оптимизации. Хоть и очевидно, что это простой пример, он показывает нам мощь техник моделирования и оптимизации решений. К тому же он может стать источником вдохновения, который можно использовать в более сложных инструментах балансировки, тесно интегрированных в симуляцию игры. Надеемся, что вы сможете использовать этот пример как руководство к формулировке подобных задач на практике.
В следующих двух частях серии мы погрузимся в область задач о назначениях, которая связана с выбором оптимальных назначений из двух и более множеств сущностей. Мы покажем, как решать такие типы задач и продемонстрируем, как использовали этот подход для создания дизайна башен в нашей стратегической игре для iOS/Android City Conquest.