Делаем классический Lode Runner на Game Maker.
Этот урок касается следующих вопросов:- Загрузка уровня из внешнего файла;
- Отображение уровня через тайлы и создание объектов (золото, герой, бандиты);
- Взаимодействие героя с уровнем без использования объектов (лестницы, кирпичики - это не объекты);
- Перемещение героя;
- Позиционирование положения на лестнице с использованием погрешности;
- Сбор золота;
- Появление выхода с уровня, когда всё золото собрано.
Если вы хотите просто взять исходный код, изменить его и использовать где-то, то этот урок не для вас. Мне хочется, чтобы вы поняли именно принцип.
Итак, приступим.
Наш уровень имеет размер 32 клетки по горизонтали и 22 клетки по вертикали. Для начала рассмотрим, как эти данные будут храниться в памяти.
Это проще всего сделать созданием массива по размеру уровня:
level[32,22]=0
И будем заносить в соответствующую ячейку массива следующие значения:
1 - кирпичик
2 - твёрдый пол (который нельзя копать)
3 - "фальшивый" кирпичик
4 - лестница
5 - верёвка
6 - золото
7 - бандит
8 - герой
9 - выход с уровняв итоге у нас получится что-то вроде:
00000000000000000000000900000000
00706000000000000000000900000000
12212212221411111120000900000000
00000000000455555555555900000000
00000000000400000000000900000000
00000000000400001140000906000000
00000000000400701140001111141111
00000000000400001140000000040000
00000000000400001140000000040000
00000000000400001140000000640000
11141111111100001111111141111111
00040000000000000000000040000000
00040000000000000000000040000000
00040000000000000000000040000000
11111111111111400000000040000000
00000000000000400000000040000000
00000000000000400000000040000000
00000000007060455555555540060700
00000041111111110000000011111114
00000040000000000000000000000004
00000040000000000080600000000004
11111111111111111111111111111111
Все уровни у нас будут закодированы таким образом, и мы будем загружать их из внешнего файла.
Первый уровень у меня готов и он хранится в файле
level1.lvl размером 704 байта.
Загрузка файла.
Будем загружать его в коде создания комнаты
if (file_exists(file_name)) // существует ли файл
{
file1=file_bin_open(file_name,0) //открыть на чтение
siz=file_bin_size(file1) //считываем размер файла
a=0;b=0
while (siz>0) //пока не прочитаем все байты, по порядку записываем данные в массив
{
level[a,b]=file_bin_read_byte(file1)
if (a=31) {a=0;b+=1}
else {a+=1}
siz-=1
}
file_bin_close(file1)
}
else
{
show_message('файл <'+file_name+'> не найден')
game_end()
exit
}
Всё, уровень находится в массиве. Я называю это "картой уровня".
Отображение уровня через тайлы и создание объектов.
Для данных о герое и бандитах создадим массивы
man[4,3]=0 //массив, данные для четырёх бандитов
player[1,3]=0 //массив, данные героя
В этом массиве описывается, что происходит с человечком
первые два байта содержат координаты человечка, а третий - описывает его состояние:
1-влево; 2-вправо; 3-вверх; 4-вниз; 5-падение; 6-закопан; 0-отсутствуетИтак, начинаем строить уровень...
a=0;b=0
while (b<22)
{
//отрисовка тайлов
readtile=level[a,b]
zz=10; //всё рисуем в слое 10, кроме выхода с уровня, который в слое 9
switch (readtile) //заносим в массив данные о золоте и человечках, а также создаём
//соответствующие им объекты
{
case 9: zz=9;break
case 8: player[0]=a;player[1]=b;player[2]=6;readtile=0;instance_create(a*tile_size,b*tile_size,obj_player);
obj_player.image_speed=0;obj_player.depth=-1;break
case 7: man[mans,0]=a;man[mans,1]=b;man[mans,2]=5;readtile=0;
instance_create(a*tile_size,b*tile_size,obj_band);obj_band.image_speed=0;break
case 6: gold+=1;readtile=0;instance_create(a*tile_size,b*tile_size,obj_gold);break
}
tile_add(background0,readtile*tile_size,0,tile_size,tile_size,a*tile_size,b*tile_size,zz)
if (a<31) {a+=1}
else {a=0;b+=1}
}
tile_layer_hide(9)
Здесь мы создаём объекты:
obj_player (герой),
obj_gold (золото),
obj_band (бандит).
В конце делаем слой, в котором находится выход с уровня, невидимым.
Взаимодействие героя с уровнем.
Для игры введём несколько глобальных переменных
mans=0 //количество бандитов на уровне
gold=0 //количество золота на уровне
tile_size=16 //размер тайла у нас будет 16 пикселей
На героя должна действовать гравитация. Нам не подойдут стандартные средства GM, потому что пол, лестницы и верёвки не являются объектами, это всего лишь тайлы.
Поэтому мы будем работать с картой уровня, которая у нас находится в массиве
level[]//падаем вниз?
step=2
if ((y+step)<(21*tile_size)) //чтобы не вылезти за пределы экрана
{
x1=floor(obj_player.x/tile_size) //вычисляем координаты героя внутри карты уровня
y1=floor((obj_player.y)/tile_size)
aa=level[x1,y1] //берём значение ячейки из карты
if (y1<21) //если не долетел до дна, то пробуем его сделать ниже
{
x1=floor(x/tile_size);x2=floor((x+tile_size-1)/tile_size) //смотрим на карте левый (x1) и правый (x2) край героя
y1=floor((y+step+tile_size-1)/tile_size);y2=floor(y/tile_size) // то же самое, но низ (y1) и верх (y2)
zz=frac(y/tile_size)
aa=level[x1,y1];ab=level[x2,y1] //берём из карты левый и правый угол снизу
ba=level[x1,y2];bb=level[x2,y2] //и левая и правая сторона сверху
//проверяем, попадаем ли на лестницу или верёвку
if ((ba=4||bb=4||aa=4||ab=4)||((ba=5||bb=5)&&(zz=0)))
//то есть: если любой из краёв героя попадает на лестницу
//либо верхний край спрайта попадает на верёвку и при этом он строго наверху, тогда
{
if (player[2]=5) {player[2]=6} // если герой падает (5), то меняем его состояние на "закопан"
}
else
//нет, тогда проверяем наличие пола или лестницы под ногами
{
if !((aa=1||aa=2||aa=4)||(ab=1||ab=2||ab=4))
{
y=y+2;player[2]=5;changeanim=true // падаем на 2 пикселя и говорим, что анимацию нужно поменять на "падает"
}
else {if (player[2]=5) {player[2]=6}} // иначе меняем состояние героя "падает" на "закопан"
}
}
zz нужен для того, чтобы определить, висит ли герой на верёвке. Если он хоть на один пиксел ниже, то мы падаем.
Сразу стоит пояснить, что большую часть кода можно оптимизировать. Например, конструкции типа if (aa||aa||aa)||(ab||ab||ab) можно разложить на кучу if. Это немного поднимет скорость выполнения, но при этом понизит читабельность кода (а точнее, его понимаемость). Поэтому здесь нет никакой оптимизации.Перемещение героя. Позиционирование положения на лестнице с использованием погрешности.
Несколько пунктов, на которых можно остановиться.
- Если мы лезем по лестнице и нам нужно резко залезть на верёвку, это может быть довольно сложно сделать, потому что скорость... Поэтому мы упрощаем игроку задачу: если верёвка оказывается чуть-чуть выше, герой как бы "подпрыгнет" к ней.
- То же самое с залезанием на лестницы: если мы чуть-чуть левее или чуть-чуть правее, герой также сам выравнивается на средину лестницы. Если этого не сделать, то играть становится гораздо менее удобно.
Весь этот код делаем в
Step.
Итак, идём влево.
if player[2]=5 {exit} // если падаем, то на нажатие кнопки не реагируем, выходим
step=4 // задаёт скорость перемещения
if (x-step)<0 {exit} // если в результате вылезем за левый край комнаты, то не реагируем, выходим
x1=floor((x-step)/tile_size);x0=floor((x+tile_size-1)/tile_size)
y1=floor(y/tile_size);y2=floor((y+tile_size-1)/tile_size)
aa=level[x1,y1];ab=level[x1,y2]
a0=level[x0,y1]
zz=frac(y/tile_size)
if ((aa=1)||(aa=2))||((ab=1)||(ab=2)) { step=0 } //стоим, если стена
if (aa=5&&(zz<0.5)) //автоприлипание к верёвке
{
y=(floor(y/tile_size))*tile_size
}
else
{
if (a0=4&&goingup) {step=0} //если в этот момент лезем вверх по лестнице, то не реагируем на нажатие
}
x-=step
if (step) //смотрим, нужно ли менять анимацию
{
changeanim=true;
if !(player[2]=1) {image_index=4}
player[2]=1 //устанавливаем статус "двигаемся влево"
Для чего нужна эта строка:
if (a0=4&&goingup) {step=0} //если в этот момент лезем вверх по лестнице, то не реагируем на нажатие
Представим ситуацию, что мы бежим влево и нам нужно забежать на лестницу. для этого мы начинаем одновременно давить клавиши влево и вверх. Но если мы не отпустим клавишу "влево" когда мы попали на лестницу (то есть отпустим её немного позже, чем надо было), тогда герой остановится, потому что сместится от центра лестницы.
Чтобы этого не произошло, мы как раз и делаем данную проверку.
Теперь сделаем обработчик нажатия клавиши вверх.
goingup=false
if player[2]=5 {exit} //если падаем, то выход
step=2
if (y-step)<0 {exit} //если вылазим за экран, то выход
x1=floor(x/tile_size);x2=floor((x+tile_size-1)/tile_size) //x1-левый край, x2 - правый край героя
y1=floor((y-step)/tile_size);y2=floor((y+tile_size-1)/tile_size) //y1-верхний край, y2 - нижний край
aa=level[x1,y1];ab=level[x2,y1] // aa-левый верхний угол, ab - правый верхний угол
ba=level[x1,y2];bb=level[x2,y2] // ba-левый нижний угол, bb - правый нижний угол
zz=frac(x/tile_size) // позиция внутри клетки по координате x
if !((ba=4)||(bb=4)) {exit} // если не лестница, то выходим
if (aa=1||aa=2||ab=1||ab=2) {exit} // или если упираемся в кирпич
//автоприлипание к лестнице
if ((zz=0||zz=0.25)&&(aa=4||ba=4)) { x=(floor(x/tile_size))*tile_size } //если левый край на лестнице, либо чуть правее, тогда ставим ровно
else {
if ((zz=0.75)&&(ab=4||bb=4)) { x=(floor(x/tile_size))*tile_size+tile_size } //если правый край чуть-чуть левее, тогда ставим ровно
else {
step=0 }} //иначе не двигаемся вверх
y-=step
goingup=true
if (step)
{
changeanim=true;
if !(player[2]=3) {image_index=7} //меняем изображение героя
player[2]=3 //устанавливаем статус "двигаемся вверх"
}
Так как в конце обработки у нас устанавливается переменная
goingup в
true, что говорит о том, что герой двигается по лестнице, то в событие отпускания клавиши "вверх" нам нужно сбрасывать эту переменную, иначе мы не сможем слезть с лестницы :)
Подобным же образом делается обработка нажатий вправо и вниз.
Сбор золота. Появление выхода с уровня, когда всё золото собрано.
Прописываем в столкновение с золотом:
instance_destroy() // золото подобрали. оно исчезает
score+=250 // прибавляем очки
sound_play(snd_gold) // проигрываем звук
gold=instance_number(obj_gold)
if !(gold) // если золото осталось, то выходим
{
sound_play(snd_exit) // иначе проигрываем звук
tile_layer_show(9) // и делаем видимым слой с выходом
// теперь в карте уровня нужно заменить все значения 9 на 4, чтобы герой мог ходить по вновь появившейся лестнице
for (b=0;b<22;b+=1) {
for (a=0;a<32;a+=1) {
if (level[a,b]=9) {level[a,b]=4}
}
}
}
Теперь нам нужно сделать проверку, чтобы игрок мог перейти на следующий уровень. Сделаем это в Begin Step.
if ((y=0)&&(instance_number(obj_gold)=0))
{
sound_play(snd_win)
transition_kind = 3
room_goto_next()
}
Ещё не забыть сделать смену анимации главного героя. Я разместил этот код в
Step End.
//нужно ли менять анимацию
if (changeanim)
{
//меняем спрайт в зависимости от направления движения
switch (player[2])
{
case 1: if (image_index=6) {image_index=4} else {image_index+=1};break
case 2: if (image_index=3) {image_index=1} else {image_index+=1};break
case 3:
case 4: if (image_index=8) {image_index=7} else {image_index+=1};break
case 5:
case 6: image_index=0;break
}
changeanim=false
}
Здесь отсутствует отдельный вид анимации, когда человечек лезет по верёвке. Попробуйте сделать это сами :)
Кадры анимации - 9-11 - это перемещение по верёвке вправо (и добавить состояние 7), а 12-14 - это перемещение по верёвке влево (состояние 8).
В заключение.
Вы можете спросить, почему всё так заморочено, почему просто не сделать всё это через объекты? Потому что на каждый объект нужно довольно много ресурсов, которые GM довольно медленно обрабатывает. Так получается гораздо быстрее. В качестве оптимизации это можно делать не через массивы, а через сетки, с которыми GM работает гораздо быстрее, чем с массивами.
Как ещё одна оптимизация (но уже по объёму, а не по скорости), можно убрать избыточность массива, который загружается из файла "level1.lvl". Там на одну клетку тратится один байт. Но у нас используется всего 10 значений, что укладывается в 4 бита. Итого, мы можем хранить в одном байте два значения - одно в младших 4х битах. а второе - в старших 4х битах. Тогда размер сразу уменьшится в два раза.
Для более подробного изучения смотрите пример. Если его запустить, также снизу будут выводиться технические данные.
Белые девять символов - это пространство вокруг героя, взятое из карты уровня. Символ "М" там означает границу уровня.
Две цифры под "Gold" - это смещение героя от края тайла.
img_ind - это номер текущего спрайта.
motion - отражает состояние героя.
chanim - разрешено или не разрешено менять анимацию.
Не стал их убирать, потому что могут более наглядно продемонстрировать внутреннюю работу игры.
На этом закончу. Желаю успехов в разработке собственных игр. И до встречи!Dmi7ry. 23 июля 2011