Использование массивов при программировании игр
Идеология. Есть ли польза от массивов при программировании игр? Вопрос праздный. Массивы необходимы и для шахмат, и для шашек, и для морского боя, и для крестиков-ноликов, и для многих других игр, в особенности для тех, где игра проходит на прямоугольном поле, расчерченном на квадраты. Возьмем для примера игру против компьютера в крестики-нолики на поле размером 3 на 3. Компьютеру приходится здесь рисовать на экране большие клетки, а в них – нолики (кружочки) после ваших ходов и крестики (пересекающиеся косые линии) после своих. Но этого умения недостаточно. Компьютеру ведь еще надо соображать, куда ставить крестики. А для этого нужно как минимум знать, где уже стоят крестики и нолики. А откуда он это знает? Если знание об этом хранится только на экране, то это очень неудобно, так как анализировать информацию о пикселях экрана трудно. Гораздо разумнее заранее организовать массив Dim a (3, 3) As Integer и записывать туда в нужные места нолики после ходов человека и, скажем, единички после ходов компьютера. Сразу же после записи в элемент массива нуля или единицы программа должна рисовать в соответствующем месте экрана кружок или крестик. Мыслить компьютер мог бы при помощи примерно таких операторов –
If a(1,1)=0 And a(1,2)=0 Then a(1,3)=1
Это очевидный защитный ход компьютера – на два кружочка в ряду он ставит в тот же ряд крестик.
Итак, сделаем вывод, что массив в памяти компьютера и поле для игры на экране в любой момент времени соответствуют друг другу, но компьютеру удобнее глядеть не на экран, а в память.
Проиллюстрируем идею использования массивов в играх подробнее, на специально придуманном примитивном примере (типа морского боя, но гораздо проще).
Задание на создание игры: Играют друг против друга два человека на квадратном поле размером 2 на 2:
Компьютер в игре участвует только как судья, а не как игрок.
Правила: Сначала первый игрок тайком от второго сообщает компьютеру, в каких двух клетках находятся его одноклеточные корабли (например, в правой верхней и правой нижней). Затем второй сообщает компьютеру, в какие клетки он производит два выстрела (тоже, конечно, наугад – например, в левую верхнюю и правую нижнюю). Если подбиты оба корабля – он выиграл, если подбит один – ничья, если ни одного – выиграл первый игрок.
Сначала запрограммируем эту игру без графики, а потом с графикой.
Поле боя должно быть показано на экране только один раз – после двух выстрелов, причем, если без графики, то в виде распечатки из 4 букв:
М к
О х
Здесь я использовал такие обозначения:
о - корабля здесь нет и сюда не стреляли
к - неподбитый корабль
х - подбитый корабль
м - мимо (стреляли и промахнулись)
Других вариантов быть не может. Вы видите, что приведенная распечатка отражает результат расстановки кораблей и выстрелов, описанных мной в качестве примера.
Вот программа без графики:
Dim a(2, 2) As String 'Поле боя
Dim i, j, Подбито As Integer
'Главная процедура:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
a(1, 1) = "о" : a(1, 2) = "о" 'Поначалу на поле боя кораблей нет
a(2, 1) = "о" : a(2, 2) = "о"
Устанавливаем_корабль(1)
Устанавливаем_корабль(2)
Подбито = 0 'Пока не стреляли
Выстрел(1)
Выстрел(2)
Показываем_поле_боя()
Debug.WriteLine(Подбито) 'Показываем исход битвы - количество подбитых кораблей
End Sub
Sub Устанавливаем_корабль(ByVal Номер_корабля As Integer)
i = InputBox("Первый игрок, назовите номер строки для корабля " & Номер_корабля)
j = InputBox("Первый игрок, назовите номер столбца для корабля " & Номер_корабля)
a(i, j) = "к"
End Sub
Sub Выстрел(ByVal Номер_выстрела As Integer)
i = InputBox("Второй игрок, назовите номер строки для выстрела " & Номер_выстрела)
j = InputBox("Второй игрок, назовите номер столбца для выстрела " & Номер_выстрела)
If a(i, j) = "к" Then 'Если попал, то
a(i, j) = "х" 'ставим крестик
Подбито = Подбито + 1 'и увеличиваем счетчик подбитых кораблей
ElseIf a(i, j) = "о" Then 'иначе если промахнулся, то
a(i, j) = "м" 'ставим м
End If
End Sub
Sub Показываем_поле_боя()
For i = 1 To 2
For j = 1 To 2
Debug.Write(a(i, j))
Next j
Debug.WriteLine("")
Next i
End Sub
Пояснения: Вы видите, что в качестве массива, представляющего поле для игры, я выбрал строковый массив
Dim a(2, 2) As String 'Поле боя
Теперь прочтите главную процедуру. Убедитесь, что она правильно отражает основной порядок действий в процессе игры. Затем разберитесь в процедурах Устанавливаем_корабль и Выстрел. Они с параметрами. Наконец, разберитесь в процедуре Показываем_поле_боя.
Программа не объявляет итогов боя, а всего лишь печатает количество подбитых кораблей. Определение и объявление победителя оставляю вам.
Обратите внимание, что игра мгновенно переделывается из игры на поле 2 на 2 в игру на поле, скажем, 30 на 30, простой заменой числа 2 в тексте программы на число 30. Этой возможностью мы наслаждаемся только благодаря использованию массива! В этом случае, конечно, придется в цикле записать во все клеточки поля букву «о». А если мы хотим при этом иметь больше двух кораблей и двух выстрелов, нам придется в главной процедуре обратиться в цикле к процедуре Устанавливаем_корабль и в цикле к процедуре Выстрел, что очень просто.
Если бы мы пренебрегли секретностью, то могли бы показывать поле боя после каждого хода игроков. Для этого достаточно в конец процедур Устанавливаем_корабль и Выстрел включить строку
Показываем_поле_боя()
Программа с графикой. Придумаем для простоты такую графику. Поле состоит из 4 цветных квадратов. Вот возможные цвета:
Голубой квадрат - корабля здесь нет и сюда не стреляли
Серый квадрат - неподбитый корабль
Красный квадрат - подбитый корабль
Зеленый квадрат - мимо (стреляли и промахнулись)
Замечательно, что от добавлении графики программа абсолютно не изменится за исключением единственной процедуры Показываем_поле_боя. Причем и в ней-то вся структура цикла останется неизменной. По большому счету выкинем только строку
Debug.WriteLine("")
как нужную только для текстового вывода, а строку, печатающую очередную букву из четырех:
Debug.Write(a(i, j))
заменим фрагментом, рисующим очередной квадрат из четырех. Вот новая процедура Показываем_поле_боя:
Sub Показываем_поле_боя()
Dim Размер As Integer = 100 'Размер квадрата
Dim Гр As Graphics = Me.CreateGraphics
Dim Кисть_для_воды As New SolidBrush(Color.LightBlue)
Dim Кисть_для_корабля As New SolidBrush(Color.Gray)
Dim Кисть_для_попадания As New SolidBrush(Color.Red)
Dim Кисть_для_промаха As New SolidBrush(Color.Green)
Dim Кисть As SolidBrush 'Текущая кисть для квадрата
Гр.Clear(Color.White) 'Стираем поле, нарисованное после предыдущего хода
For i = 1 To 2
For j = 1 To 2
Select Case a(i, j) 'Выбираем кисть для очередного квадрата
Case "о" : Кисть = Кисть_для_воды
Case "к" : Кисть = Кисть_для_корабля
Case "х" : Кисть = Кисть_для_попадания
Case "м" : Кисть = Кисть_для_промаха
End Select 'Рисуем очередной квадрат:
Гр.FillRectangle(Кисть, Размер * j, Размер * i, Размер, Размер)
Next j
Next i
End Sub
Пояснения: Предположим, наша процедура работает после каждого хода игроков. Мы могли бы написать ее так, чтобы после каждого очередного хода (поставили корабль или выстрелили) компьютер перерисовывал только тот квадрат, о котором шла речь. Остальные ведь остались неизменными – чего их перерисовывать? В этом случае компьютеру пришлось бы «меньше трудиться». Но я пошел по более простому и универсальному пути – после каждого хода все поле со всеми квадратами стирается и рисуется заново согласно содержимому массива a.
Кстати, строка
Гр.Clear(Color.White) 'Стираем поле, нарисованное после предыдущего хода
в нашем случае излишня. Ведь мы все равно заново перерисовываем все квадраты поля, поэтому предварительно стирать их не имеет смысла.
Крестики-нолики 3х3 – советы. В принципе вы уже готовы к программированию игры против компьютера в обычные крестики-нолики 3х3. Всю техническую сторону дела мы прошли. Остается логика, то есть объяснение компьютеру, куда ставить крестики. И вот с логикой-то у нас будет проблема. Вы скажете: Какая проблема? – ведь клеточек всего 9 штук! Один из возможных операторов уже написан:
If a(1,1)=0 And a(1,2)=0 Then a(1,3)=1
Напишу еще пару десятков подобных операторов – и дело с концом! – А пару сотен не хотите?! Вы только попробуйте перебрать все возможные варианты расстановки крестиков, ноликов и пустых клеток! Их вообще несколько тысяч. В этом случае, чтобы сократить программу, нужно применять в качестве индексов переменные величины и ломать голову над тем, какие писать процедуры, ветвления и циклы.
Поэтому любителям игр рекомендую для тренировки запрограммировать крестики-нолики не против компьютера, а как игру человека с человеком, где компьютер – лишь судья. Если получится, вот тогда можно замахнуться и на большее. Но и здесь идите постепенно. Рекомендую написать большую процедуру для правильной простановки крестика в произвольном одномерном массиве из трех клеток. У этой процедуры будет три параметра – по числу клеток. Затем заметьте, что в реальной игре 3х3 вас интересует только 8 рядов: 3 по горизонтали, 3 по вертикали и 2 по диагонали. Значит у вас будет 8 обращений к этой процедуре. Дальше думайте сами. Все это совсем не просто.
Массивы как объекты
Оказывается, массив – это объект. Объект класса Arrayпространства имен System. Как?! – скажете вы, – мы до сих пор прекрасно работали с массивами и, как говорится, «ни сном, ни духом»! Мы нигде не писали New, не пользовались свойствами и методами массивов. – Что ж, верно, многим программистам вполне можно работать с массивами и не подозревать, что это объекты. Авторы VB замаскировали этот факт (как мне кажется), чтобы не пугать программистов, переходящих с Visual Basic 6.0 на VB. Массивы-объекты рождаются в вашей программе «нечувствительно» для вас безо всякого New.
И все же, вот как можно создать массив при помощи New:
Dim a() As Integer = New Integer() {8, 1, 4, 3}
Нам будут полезны некоторые свойства и методы массивов (см. процедуру):
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Dim a() As Integer = {80, 60, 50, 90, 40, 20, 50, 70}
Dim t(,) As Integer = {{99, 99, 99, 99, 99}, {99, -8, -14, -19, -18}, {99, 25, 28, 26, 20}, {99, 11, 18, 20, 25}}
Debug.WriteLine(a.Length) 'Длина массива a (число элементов) = 8
Debug.WriteLine(t.Length) 'Длина массива t (число элементов) = 20
Debug.WriteLine(t.GetUpperBound(0)) 'Число строк (макс. индекс первого измерения) - 1 = 3
Debug.WriteLine(t.GetUpperBound(1)) 'Число столбцов (макс. индекс второго измерения) - 1 = 4
'Ищется первое вхождение числа 50 в одномерный массив a и находится его индекс (2):
Debug.WriteLine(Array.IndexOf(a, 50))
'Ищется последнее вхождение числа 50 в одномерный массив a и находится его индекс (6):
Debug.WriteLine(Array.LastIndexOf(a, 50))
Debug.WriteLine(Array.IndexOf(a, 55)) 'Ищется число 55 в массиве a и не находится (-1)
Array.Reverse(a) 'Все элементы массива a меняют порядок на обратный = {70, 50, 20, 40, 90, 50, 60, 80}
Array.Sort(a) 'Все элементы массива a сортируются по возрастанию = {20, 40, 50, 50, 60, 70, 80, 90}
Array.Clear(a, 4, 3) 'Обнуляется 3 элемента массива a, начиная с индекса 4= {20, 40, 50, 50, 0, 0, 0, 90}
End Sub
Из приведенных методов некоторые имеют несколько вариантов, которые я здесь не привожу.
Замечание. Учитывая, что массив – это объект, я призываю вас до поры не присваивать массив массиву целиком, без индексов, например, вот так:
Dim a() As Integer = {8, 1, 5, 2}
Dim b() As Integer
b = a
Присвоение, конечно, состоится, но совсем не такое, как вы ждали. Оно может привести к неожиданным для начинающих последствиям. Вот к каким, например. Продолжу фрагмент:
a(2) = 99
Debug.WriteLine(b(2))
Напечатается 99, а не 5, потому что массив – это объект. Почему? Расскажу позднее, в 27.2.
Массивы как параметры
До этого момента параметр процедуры или функции был для нас каким-то одним данным: это или одно число, или одна строка, или один объект. Но параметр может быть и массивом.
Задача: Имеется два массива, по три числа в каждом. Напечатать сумму элементов каждого массива. Использовать функцию sum, единственным параметром которой является суммируемый массив.
Программа:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim a() As Integer = {4, 10, 20}
Dim b() As Integer = {100, 40, 50}
Debug.WriteLine(sum(a))
Debug.WriteLine(sum(b))
End Sub
Function sum(ByVal c() As Integer) As Integer
sum = c(0) + c(1) + c(2)
End Function
Выполните программу в пошаговом режиме, глядя в окно Locals. Понаблюдайте, как массив c принимает в себя значения элементов массивов a и b.
Задание 105.
В школе два класса. В каждом – два-три десятка учеников. Каждый ученик получил отметку на экзамене по физике. Определить, какой из двух классов учится ровнее (будем считать, что ровнее учится тот класс, в котором разница между самой высокой и самой низкой отметкой меньше).
Указание: Создать функции Минимум(c), Максимум(c) и Разница(c).
Задание 106.
На двух метеостанциях (A и B) в течение года измерялась температура. Соответственно созданы два массива чисел длиной 365. Затем оказалось, что на обеих станциях термометры были испорчены: на станции A термометр все время показывал температуру на 2 градуса выше настоящей, а на станции B – на 3 градуса ниже. Написать процедуру с двумя параметрами, которая исправляет один произвольный массив и с ее помощью исправить оба массива. Один параметр процедуры – величина поправки, другой – массив температур.