Змейка на Canvas

Приветствую уважаемое хабра-сообщество. После прочтения поста «Создаём игру, используя canvas и спрайты» в день его выхода, решил углубить свои познания в Canvas. Так, как пока в работе не приходилось сталкиваться с этим элементом, пришлось пробежаться на скорую руку по API.
Конечно, рисование линий, прямоугольников, треугольников и полукругов весьма занимательное занятие. Но для приобретения реального опыта была поставлена задача – создать что-то функциональное и простое.

Вот так родилась идея написать собственную игру, всем знакомую змейку.

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

Поехали!
Код написан с применением библиотеки jQuery, ибо так удобнее. Все переменные и функции объявляются уже после события загрузки странички. Перечисляем переменные и некоторым задаём дефолтные настройки, также задаём размеры холста и определяем «тело» змейки. Которое будет многомерным массивом. Идея состоит в том, что тело змейки – набор секторов с площадью 9px на 9px, и начало координат каждого сектора является число кратное 10. 1px «справа» от сектора не будет зарисовываться, для визуального разделения.

// объявление переменных, задание им дефолтных значений
var canvas, context, first_x, first_y,
rabbit_pos = new Array(), rabbit_on_field = false,
start = true, state = false, g_over = false,
direction = 'right';

// определение слоя и его размеров
canvas = document.getElementById('mycanvas');
canvas.width = 310;
canvas.height = 310;
context = canvas.getContext('2d');

// определение цвета змейки
context.fillStyle = "#CE3429";
        
// определение дефолтных секторов змейки
var snake_sectors = [[10, 50], [20, 50], [30, 50], [40, 50]];

Функция вывода рамок игрового поля, которую вызовем сразу после объявления дефолтных настроек.

// создание игрового поля
function field(context){
    context.strokeStyle = "#546DEA";
    context.lineWidth = 1;
    context.strokeRect(7, 7, 295, 295);
}

Функция вывода змейки, в которой вызываются другие необходимые функции.

// вывод змейки
function snake(){
    if(state){
        // удаление "хвоста" при движении
        // при первом проходе координаты есть undefined
        context.clearRect(first_x, first_y, 9, 9);
        
        // определение "поведения" змейки: начало игры, пауза/старт
        if(start){
            // зырисовывается тело змейки в цикле, и объявляется о завершении запуска игры
            // для визуализации секторов, будем зарисовывать 9px из 10
            for(var i in snake_sectors){
                context.fillRect(snake_sectors[i][0], snake_sectors[i][1], 9, 9);
            }
            start = false;
        }
        else{
            // зарисовывать новый сектор змейки - "голову"
            context.fillRect(snake_sectors.slice(-1)[0][0], snake_sectors.slice(-1)[0][1], 9, 9);
        }
        
        // проверка существования кролика на поле и его вывод
        if(!rabbit_on_field){
            rabbit();
        }
        
        // присваивание переменным значений положения "головы" и "хвоста" 
        var last_x = snake_sectors.slice(-1)[0][0];
        var last_y = snake_sectors.slice(-1)[0][1];
        first_x = snake_sectors[0][0];
        first_y = snake_sectors[0][1];
        
        // определение поведения змейки при различных направлениях движения
        if(direction == 'right'){
            var next_x = last_x + 10;
            if(next_x > 290){
                // врезание в правое поле, конец игры
                window.setTimeout(game_over, 700);
                return false;
            }
            snake_sectors.push([next_x, last_y]);
        }
        if(direction == 'down'){
            var next_y = last_y + 10;
            if(next_y > 290){
                // врезание в нижнее поле, конец игры
                window.setTimeout(game_over, 700);
                return false;
            }
            snake_sectors.push([last_x, next_y]); // добавление нового элемента массива ("голова" змейки)
        }
        if(direction == 'up'){
            var next_y = last_y - 10;
            if(next_y < 10) {
                // врезание в верхнее поле, конец игры
                window.setTimeout(game_over, 700);
                return false;
            }
            snake_sectors.push([last_x, next_y]);
        }
        if(direction == 'left'){
            var next_x = last_x - 10;
            if(next_x < 10) {
                // врезание в левое поле, конец игры
                window.setTimeout(game_over, 700);
                return false;
            }
            snake_sectors.push([next_x, last_y]);
        }
        
        // проверка на совпадение последнего элемента массива с другими элементами
        // т.е. "врезание" змейки в себя
        for(var i = 0; i < snake_sectors.length - 1; i++){
            if(snake_sectors.slice(-1)[0][0] == snake_sectors[i][0] && snake_sectors.slice(-1)[0][1] == snake_sectors[i][1]){
                // конец игры
                window.setTimeout(game_over, 700);
                return false;
            }
        }
        
        // проверка на съедание кролика
        // и определение дальнейшей судьбы "хвоста"
        if(!is_catching()) snake_sectors.splice(0, 1);
        else rabbit();
        
        // таймер перезапуска функции, он же - скорость змейки
        setTimeout(snake, 200); // 200 ms
    }
}

По порядку. При state == true (true/false состояние игры старт/пауза), функция выполняет ряд действий, описание которых приведено в комментариях.
Ниже приведены все остальные функции, которые вызываются в snake().
Коротко по сути: вывод кролика (с генерацией случайных значений его координат и проверкой на существование таких в «теле» змейки);
// вывод кролика на игровое поле
function rabbit(){
    // задание координат кролику
    rabbit_pos[0] = math_rand();
    rabbit_pos[1] = math_rand();
    
    // проверка на совпадение сгенерированых координат с "телом" змейки 
    for(var i in snake_sectors){
        if(rabbit_pos[0] == snake_sectors[i][0]){
            rabbit_pos[0] = math_rand();
        }
        if(rabbit_pos[1] == snake_sectors[i][1]){
            rabbit_pos[1] = math_rand();
        }
    }
    
    // зарисовать сектор, и объявить о наличии живности на поле
    context.fillRect(rabbit_pos[0], rabbit_pos[1], 9, 9);
    rabbit_on_field = true;
}

функция-генератор случайных значений в заданном диапазоне;

// генерация случайных чисел в заданном диапазоне
// для определения координат кролика
function math_rand(){
    return Math.ceil((Math.random() * 2.9) * 10) * 10;
}

проверка на «съедение» кролика (совпадение координат «головы» с координатами кролика);

// проверка на "съедание" кролика
// т.е. совпадение координат "головы" змейки с координатами кролика
function is_catching(){
    if(rabbit_pos[0] == snake_sectors.slice(-1)[0][0] && rabbit_pos[1] == snake_sectors.slice(-1)[0][1])
        return true;
    
    else 
        return false;
}

объявление о конце игры и возврат к настройкам по умолчанию (для нового старта);

// "конец игры", функция вызываемая при наступлении событий -
// врезания змейки в себя или края игрового поля
function game_over(){
    // объявляем конец игры, эта переменная нужна для того,
    // чтоб заблокировать отклик на нажатие пробела во время заставки "Game Over"
    g_over = true;
    
    // чистим поле от треша
    context.clearRect(8, 8, 293, 293);
    
    // радуем пользователя о конце игры
    context.font='35px Verdana';
    context.strokeStyle="#DB733B";
    context.strokeText('Game Over!',47,160);
    
    // создаём некую разновидность анимации - задержка надписи "Game Over!"
    setTimeout(function(){context.clearRect(8, 8, 293, 293); g_over = false}, 1500);
    
    // задание дефолтных значений - снова начало игры
    snake_sectors = [[10, 50], [20, 50], [30, 50], [40, 50]];
    state = false;
    direction = 'right';
    start = true;
    rabbit_on_field = false;
}

отлавливание событий нажатия на «игровые» клавиши и соответствующие последствия;

// отлавливание событий нажатия на "игровые" клавиши
document.onkeydown = function(event){
	var keyCode;
	if(event == null){
		keyCode = window.event.keyCode; 
	}
	else{
	   keyCode = event.keyCode; 
	}

	switch(keyCode){
        // space/пробел
        case 32:
        // действие при нажатии пробела
        if(!g_over){
            // чередуем паузу со стартом(continue)
            state = (!state) ? true : false;
            // вызываем змейку
            snake();
        }            
        break;
        // left/стрелка влево
        case 37:
        // действие при нажатии "влево"
        if(direction == 'right') return;
        direction = 'left';
        break;
        
        // up/стрелка вверх
        case 38:
        // действие при нажатии "вверх"
        if(direction == 'down') return;
        direction = 'up';
        break;
        
        // right/стрелка вправо
        case 39:
        // действие при нажатии "вправо"
        if(direction == 'left') return;
        direction = 'right';
        break;
        
        // down/стрелка вниз
        case 40:
        // действие при нажатии "вниз"
        if(direction == 'up') return;
        direction = 'down';
        break;
        
        
        default:
        break;
	} 
}

Поиграть можно здесь.

Буду рад услышать здоровую критику в комментах. Благодарю за внимание!

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 16

    0
    В целом отлично, только поле мелковато, что ли (во всяком случае, мне захотелось увеличить масштаб, что я и сделал). В Опере после изменения масштаба страницы клавишами Ctrl и + клавиши «вверх/вниз/влево/вправо» помимо управления змейкой стали в соответсвующую сторону сдвигать экран. Впрочем, не жалуюсь, сия неожиданная «фича» добавила новый драйв в старую игру :)
      +2
      Почему-то вспомнилась гифка с изображением разработчика, который прочитал первый положительный отзыв на плэймаркет.
        0
        Боюсь, что не все с ней знакомы. :)
      –1
      Жаль, на планшета не поиграешь (
        +2
        Быстрый разворот на 180 градусов заканчивает игру (типа, голова сама в себя упёрлась) — баг или фича?
          0
          Похоже баг.
          0
          Заметил пару багов. При ширине окна меньше контейнера (950px) при горизонтальном повороте происходит сдвиг документа в Хроме и скролл по горизонтали в Firefox (походу не захватываются keypress евенты браузера). Медленный отклик из-за чего трудно играть. И да поддержку автора постом выше, при быстром развороте кролик съедает свою голову игра заканчивается. В любом случае спасибо за пост, отложу в коллекцию, давно хотел заняться изучением canvas'a.
            0
            Еще хотел добавить, что когда мне приходится пользоваться такой вещью как направление движение чего-то в двухмерной системе координат, я чаще всего пользуюсь такой конструкцией:

            function getDirection(e){ //здесь е - mouse event
                  return {
                        x : e.keyCode % 2 ? e.keyCode - 38 : 0, 
                        y : !(e.keyCode % 2) ? e.keyCode - 39 : 0 
                  } 
            }
            

            На выхлопе получаю объект вида {x:1,y:0}, что означает, положительный сдвиг по оси абсцисс и нулевой сдвиг по оси ординат. Очевидно, что область значений x,y — это {-1,0,1}. Так потом удобнее вычислять и репозиционировать объекты, которые двигаются.
            +1
            Код написан с применением библиотеки jQuery, ибо так удобнее.
            canvas = $('#mycanvas').get(0);
            $(document).ready(...)

            Тут только 2 вопроса… зачем? и почему?
            Поправьте, если где-то еще упустил использование jQ…
              0
              Дело в том, что код я переписывал с нуля несколько раз. И чем дальше, тем актуальность использования jQ ставала всё меньше, но так уж вышло, что пост я опубликовал именно в таком варианте. И вносить правки сейчас, уже не очень хорошая затея. Пусть так и останется
              бета-версия игры

              В конце-концов это же мой первый блин, ну не может он выйти идеально )
              0
              Можно я просто оставлю это здесь: js1k.com/2011-dysentery/demo/927. Есть 3 жизни и бесконечное кол-во уровней. Весь код занимает 1017 байт
                +1
                Мне нравится, придётся как-нибудь выкроить время, да разобраться в коде.
                  +1
                  Есть 3 жизни и бесконечное кол-во уровней.

                  Прочитал и подумал что у меня всё–таки одна жизнь, а вот уровней и правда бесконечность… как и игр. И что как–то неправильно её расходовать на прохождение этих бесконечных уровней.
                  Потом открыл ссылку. Понял, что не про ту жизнь подумал…

                  Кстати, сравнивать в байтах код автора поста и код по той ссылке не совсем корректно – там код минифицирован.
                    0
                    Спасибо, повеселили.
                    Само собой код сравнивать некорректно. Тот код готовился специально для конкурса js1k и там одно из условий уложится в 1кб :) Поэтому пришлось отказаться от многих фич :)

                    Если получится найти оригинальный код — выложу ссылку обязательно :)
                      0
                      Исходники: github.com/s-goryakin/snaky
                      Это не последние исходники, т.к. последние штрики добавлялись уже после минификации кода. По всем вопросам готов ответить в коментах :)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое