Создание классической змейки gms2 и стиль программирования
1) скрипты сборки вполне можно заменить объектом контроллером и событием create или же creation code комнаты.
2) ну скажем вместо instance_destroy(value.id) не составит труда написать with(other.id){instance_destroy();}
Приведенные выше примеры совместимости достаточно детские, точно также вы можете мыслить и по поводу макросов, в старом gms или game-maker макросы просто пишутся в специальном меню (которое было достаточно неудобным, но все же макро-переменные это полезная возможность).
Для того чтобы комфортно чувствовать себя в этом уроке, нужно владеть основами работы с gms2, уметь создавать спрайты и понимать что такое события в объектах.
Исходный текст статьи для прочтения в offline: файл.txt
//-----------------------
Начнем мы пожалуй с объявления файла macro в категории scripts:
Первые два макроса я делаю в основном для совместимости с например java-script кодом, когда копирую нужные фрагменты кода из своей библиотеки на этом языке. Чтобы снова не писать весь код. А также эти аббревиатуры более привычны.
Следующие четыре, слова просто всегда удобнее + макросы в gms2 выделяются красным цветом, что помогает понять, что эти значения чем-то особенные. Просто вкусы объявления кода, можно например это записать в локальные или глобальные переменные. Это уже как ваша душа пожелает.
Следующим файлом будет script "__init__":
Код
gml_pragma("global", "__init__()");
// global preCompiler options // глобальные опции перед сборкой global.GAME_SPEED = 60;
game_set_speed(global.GAME_SPEED, gamespeed_fps);
math_set_epsilon(1/10000); randomize();
room_speed = global.GAME_SPEED;
gml_pragma запустит ваш скрипт по сути на старте сборки проекта, т.е. этот скрипт запустится перед созданием первой комнаты, это важно знать, потому что попытка воспользоваться свойством комнат room_width выдаст ошибку, скрипт __init__ присутствует в коде любого проекта над которым я работаю, тут объявляются наиболее общие опции. math_set_epsilon нужен чтобы операции с числами с плавающей точкой были одинаковы на VM (Virtual Machine) так и при трансляции и компиляции из С++ (YYC Compiler) ну и заодно удостовериться, что все будет одинаково для любых других платформ. И да, вызов room_speed не вызывает ошибку.
Файл "__game__":
Код
gml_pragma("global", "__game__()");
// global preCompiler options // глобальные опции перед сборкой
global.TILE_WIDTH = 16; global.TILE_HEIGHT = 16;
Я создаю файл __game__ для скрипта сборки уникального для данного игрового проекта. Т.е. в другом проекте там будет что-то другое. Именно поэтому я разделил это на два скрипта. Потому что __init__ просто перебрасывается между любыми моими проектами, а __game__ это индивидуальные инструкции.
Размер игрового тайла или ячейки, будет 16х16. А точнее это по сути спрайт квадрата.
Но по сути это всего-лишь 4 квадратных спрайта размера 16х16: 1) Спрайт головы змейки 2) Белый спрайт части тела змейки (нужен именно белый или серый, объяснения получите позже) 3) Спрайт еды для змейки (у меня это просто голубой квадрат) 4) Спрайт смерти змейки, у меня это спрайт состоящий из 6 кадров, 3 белых и 3 красных, на самом деле можно было бы воспользоваться свойством image_speed и выставить скорость скажем 0.3, но я решил просто сделать спрайт, чтобы отрегулировать скорость анимации вручную. На ваше усмотрение.
Примечание: Центр спрайта должен оставаться в верхнем левом углу. Для данного проекта нам не нужно его менять.
Создадим 4 объекта:
o_null - его я добавляю по привычке в каждый проект, раньше нулевой object_index и значение noone (вспомните макрос null) совпадали и это порождало неприятные ошибки. Я называю это ошибкой нулевого объекта, можно выразиться эпичнее ZERO_ERROR_OBJECT. В данный момент noone = -4, а не 0.
p_node - тут сразу нужно рассказать о префиксе p_, приставка p_ во всех моих объектах обозначает объект, который никогда не будет создан, объекты с такой приставкой служат лишь с целью быть родителями какой-либо группы объектов. И иногда служат интерфейсом, но не в данном проекте. Т.е. он так и останется пустой, мы не напишем ни строчки кода в нем. Просто создайте его и не открывайте больше. Также как o_null.
o_head - это будет голова нашей змейки и фактически главный управляющий игрой объект. Почти весь игровой код будет сосредоточен здесь. Естественно на ваше усмотрение будет добавлять или не добавлять в игру что-то еще. Разделить ли логику, отправив что-то в какой-нибудь controller. Я решил сделать именно так, чтобы сделать немного быстрее рабочий вариант.
o_node - часть тела змейки Установите parent в p_node. пожалуй кода в этом объекте так мало, что я опишу его сразу же здесь: создайте событие create:
_prevX и _prevY это контролируемые нами из головы змейки значения предыдущей позиции x, y _owner - тут будет храниться id объекта, который является "хозяином" для данного узла, например чтобы взять позицию хозяина и контролировать его направление
image_blend - это встроенная переменная gml, в данном случае мы просто генерируем случайный цвет узлу, именно для этого нам и нужен был полностью белый квадрат, чтобы сверху накладывался любой случайный цвет.
(вспомните, так как в __init__ уже вызывается randomize() нет нужды более помнить об этом, мы используем irandom не вспоминая об этом, randomize нужен для генерации нового seeds для методов генерирующих случайные числа, т.е. чтобы при перезапуске игры у пользователя не генерировался один и тот же набор случайных событий)
o_food - в нем также нет никакого кода
А теперь приступаем к самому интересному, написанию кода самой игры:
Событие Create:
Код
_width = 16; _height = 16;
_speed = 16;
_owner = null;
_newDirection = 0;
var pobj = null; var obj = null; for(var i = 1; i<5; i++){
А нечего страшного тут нет, мы создаем таким образом первые три сегмента. var i = 1, я начал цикл с единицы из-за того что x-width * i, перемножение на ноль даст ноль и таким образом первый узел создастся прямо в голове, вот почему я сделал именно 1. else if(!instance_exists(pobj)){ obj._owner = self.id; pobj = obj; } если вдруг pobj будет равен null мы первого хозяина узлу назначим нашу голову змейки ну и предыдущим объектом станет самый первый узел
scr_snCreateFood(); -- создает еду в случайном месте комнаты
var posX = irandom_range(0, room_width-global.TILE_WIDTH); var posY = irandom_range(0, room_height-global.TILE_HEIGHT);
//данный цикл проверяет чтобы в ячейке куда ставится еда, не было узлов, вы можете догадаться тут не проверяется o_head, т.е. еда вполне может появиться в голове //это легко исправить, оставлю это задачу вам (подсказка можно создать еще один parent, либо решить задачу с использованием p_node и для o_head) while(collision_rectangle(posX+1, posY+1, posX+global.TILE_WIDTH, posY+global.TILE_HEIGHT, p_node, false, false)){ posX = irandom_range(0, room_width-global.TILE_WIDTH); posY = irandom_range(0, room_HEIGHT-global.TILE_HEIGHT); }
//оператор div это аналог floor(posX / global.TILE_WIDTH) не правда ли хорошо, когда есть такой оператор на уровне языка posX = posX div global.TILE_WIDTH * global.TILE_WIDTH; posY = posY div global.TILE_HEIGHT * global.TILE_HEIGHT;
var obj = instance_create_layer(posX, posY, "Main", o_food);
Пожалуй важно упомянуть /// @description scr_snCreateFood(null) /// @function scr_snCreateFood /// @param null Вы могли заметить никак не используемый параметр null, я это взял из стиля написания кода в Си. Там вместо null был void, он в header.h обозначал, что аргументы в функции отсутствуют. Также java-doc стиль нужен gms2 чтобы показывать ваши функции в подсказках внизу при редактировании кода. Важно, @desctiption по сути не показывается, важны только @function и @param, однако я предпочитаю в @description написать полный вид функции.
alarm[0]: - нужно чтобы сказать, что наша голова может двигаться, по сути данный таймер регулирует весь игровой "тик".
Прежде чем я расскажу о событии step нам придется предварительно написать несколько скриптов:
f_snChangeDirection: Как раз в нем нам и пригодились объявленные ранее макросы Вкратце скрипт нужен чтобы вы не могли войти в себя при управлении змейкой и поменять направление на нужное по течению игрового сценария когда игрок будет нажимать на кнопки wasd. Однако далее будет еще одна предосторожность помогающая в этом. Т.е. этого скрипта будет недостаточно. К примеру в змейку, если ошибиться все еще можно будет войти в себя, если игрок будет быстро нажимать на клавиши. И я имею ввиду не ту ситуацию, когда вы запутались, а когда вы ползете влево, но быстро нажали вверх и вправо, таким образом вы сможете войти в себя, хотя и не могли бы ползти вправо, когда ползете влево. Надеюсь вы меня поняли, если нет, попробуйте с нуля написать данную игру переоформив step по своему.
case dir_down: if(this.direction == dir_up) return this.direction; break;
case dir_up: if(this.direction == dir_down) return this.direction; break;
case dir_left: if(this.direction == dir_right) return this.direction; break;
case dir_right: if(this.direction == dir_left) return this.direction; break;
} return input_direction;
scr_pos_to_dir: В данном случае мы просто меняем позиции головы по отношению к вашему направлению(direction) ну и длина это скорость Данный скрипт движения подойдет практически для любой игры, т.е. вы сможете просто менять direction и добавлять нужную скорость. Я не стал использовать встроенную speed по очевидным причинам. Встроенный speed не всегда удобно контролировать. По крайней мере не для каждой игры. Хотя и не стоит забывать о встроенной возможности, но в этой задаче это незачем.
var len, hspd, vspd; len = (spd); hspd = lengthdir_x(len, dir); vspd = lengthdir_y(len, dir); x += hspd; y += vspd;
scr_snHeadKill() - название говорит само за себя, тут мы убиваем нашу змейку мы останавливаем змейку и меняем спрайт ее головы на тот самый спрайт из шести кадров. alarm[0] = -1 фактически означает "остановить таймер 0"
//тот самый момент когда мы проверяем направление непосредственно перед движением direction = f_snChangeDirection(_newDirection);
//меняем позицию в зависимости от направления scr_pos_to_dir(_speed, direction);
//перебираем все узлы и меняем им позицию отсюда сразу после того как передвинулась наша голова //по сути сегменты змейки, просто меняют свое положение на предыдущее положение их _owner var obj = null;
for(var i = 0; i<instance_number(o_node); i++){ obj = instance_find(o_node, i);
//_вспоминаем alarm[0] именно там мы и меняем _move на true if(_move){
_move = false; scr_snMove();
} //origin left_top //проверяем все что может убить нашу змейку //знаете почему именно x+1? мне пришлось так сделать чтобы узлы не сталкивались с границами змейки при движении например вверх или вниз //дело в frame системе движка, но не критично, я просто по сути обрезал прямоугольник на 1 пиксель программно, можно также например вручную поменять маску спрайтов //что было бы даже логичнее, но мне было важно это указать в коде, чтобы он мне сказал "эй парень, тут ты когда-то сказал обрезать, помни об этом" //дело каждого, делайте как вам удобнее if(collision_rectangle(x+1, y+1, x + _width-1, y + _height-1, p_node, false, true)){ scr_snHeadKill(); } //ну и понятно проверяем выход за границы игрового экрана if(x<0){ x = 0; scr_snHeadKill(); } if(x>room_width){ x = room_width-_width; scr_snHeadKill(); } if(y<0){ y = 0; scr_snHeadKill(); } if(y>room_height){ y = room_height-_height; scr_snHeadKill(); }
Последнее событие, событие столкновения, для него также напишем один последний скрипт (привыкайте разделять вашу игру на множество маленьких модулей, так она выглядит гораздо проще, чем если вываливать весь код в события объектов):
var obj = null; var newObj = null; for(var i = 0; i<instance_number(o_node); i++){ obj = instance_find(o_node, i); } //перебираем все узлы, obj и будет нашим последним узлом, за которым мы и добавим наш следующий
var posX = 0; var posY = 0;
var dX = lengthdir_x(global.TILE_WIDTH, -direction); var dY = lengthdir_y(global.TILE_HEIGHT, -direction); posX = obj.x+dX; posY = obj.y+dY;
//вспомните скрипт scr_pos_to_dir, фактически это копипаст от туда, только с заменой на -direction, мне нужно было сделать именно так, т.е. таким образом //в независимости от направления он поместит следующих узел за последним
var newObj = instance_create_layer(posX, posY, "Main", o_node); newObj._owner = obj.id; //и назначит хозяином, последний узел из перебора
событие столкновения с o_food:
Код
scr_snAddNode();
instance_destroy(other.id);
scr_snCreateFood();
При столкновении с едой, мы ее уничтожаем, добавляем новый узел и создаем новую еду, конец!
Если все получилось, то у вас должно быть что-то такое:
Очень надеюсь урок был кому-то понятен. Это мой первый опыт написания подобного. Понятно, что в данном уроке требуется определенный, но небольшой уровень понимания кода. Однако, если вы будете даже просто перепечатывать, у вас получится это понять чисто по наитию.
Могу ошибаться, но согласно правилам я вроде бы нечего не нарушил. Данный материал полностью уникален. Т.е. если я и буду писать данную статью в будущем где-то еще, я напишу ее заново и по другому. Также я не воспользовался правом залить файлы на гугл-диск. Чтобы не нагружать лишний раз сервер сайта лишними файлами. Картинку из гугл диска материал воспринимать отказался, пришлось загрузить сюда.
Категория: Создание игр | Добавил: GWÁLÐ (08 Сентября 2017)
| Автор: Норман Ридус
Также если вы считаете, что данный материал мог быть интересен и полезен кому-то из ваших друзей, то вы бы могли посоветовать его, отправив сообщение на e-mail друга:
Игровые объявления и предложения:
Если вас заинтересовал материал «Создание классической змейки gms2 и стиль программирования», и вы бы хотели прочесть что-то на эту же тему, то вы можете воспользоваться списком схожих материалов ниже. Данный список сформирован автоматически по тематическим меткам раздела.
Предлагаются такие схожие материалы:
Если вы ведёте свой блог, микроблог, либо участвуете в какой-то популярной социальной сети, то вы можете быстро поделиться данной заметкой со своими друзьями и посетителями.
Если код поворота головы поменять на это direction=point_direction(x,y,mouse_x,mouse_y)
if direction < image_angle and image_angle-direction<180 { image_angle -= 3 } if direction > image_angle and direction-image_angle<180 { image_angle += 3 } if direction < image_angle and image_angle-direction>180 { image_angle += 3 } if direction > image_angle and direction-image_angle>180 { image_angle -= 3 } move_towards_point(mouse_x,mouse_y,3)