Змейка на Canvas

в 18:09, , рубрики: canvas, javascript, игра, метки: , ,

Приветствую уважаемое хабра-сообщество. После прочтения поста «Создаём игру, используя 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 = $('#mycanvas').get(0);
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;
	} 
}

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

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

Автор: habro_user

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js