Часть 1 » Часть 2 » Часть 3 » Часть 4 » Часть 5 » Часть 6 » Часть 7 // Конец )


Сегодня добавим в игру кое-что вкусное для змеи и реализуем систему обработки столкновений.


Печенье


Печенье (biscuit) – это главная цель игры. Задача игрока заключается в том, чтобы, направляя змею, позволить ей съесть как можно больше подобных угощений. В нашей реализации печенье – это всегда один и тот же спрайт, предназначение которого заключается в том, чтобы появиться на экране, быть съеденным и появиться снова в другом месте.

То, как именно реализовано появление печенья в новой позиции, является одним из факторов, определяющих сложность игры. В нашем случае это – полностью случайный процесс. Создадим печенье.

  1. Необязательный шаг. Создайте новый файл – «biscuit.js» в папке src. Если вы это сделали – добавьте его в список в файле «project.json». Список должен выглядеть так, как показано ниже.

    "jsList" : [
        "src/resource.js",
        "src/biscuit.js",
        "src/game.js"
    ]
    


  2. Добавьте в biscuit.js нижеприведённый код для создания спрайта Biscuit. Если вы пропустили первый шаг, то разместите его в game.js

    var Biscuit = cc.Sprite.extend({
        winSize: 0,
        ctor: function(snakeParts) {
            /* Вызов метода суперкласса */        
            this._super(asset.SnakeBiscuit_png);          
            /* Настройка winSize */
            this.winSize = cc.view.getDesignResolutionSize();
            /* Установка позиции спрайта */
            this.randPosition(snakeParts);
        },    
    });

    Обратите внимание на то, что в конце конструктора вызывается метод randPosition(). При создании спрайт размещается на экране случайным образом.

  3. Добавьте реализацию метода randPosition.

    randPosition: function(snakeParts) {            
            var step = 20;
            var randNum = function(range) {
                /* Возвратим случайную позицию в пределах диапазона range */
                return Math.floor(Math.random() * range);        
            };
            /* Диапазон возможных координат, где может располагаться печенье */
            var range = {
                x: (this.winSize.width / step) - 1,
                y: (this.winSize.height / step) - 1             
            }                          
            /* Возможная позиция */
            var possible = {
                 x: randNum(range.x) * step,
                 y: randNum(range.y) * step
            }                        
            var flag = true;
            var hit = false;
            
            /* Если нужна дополнительная попытка */
            while (flag) {            
                /* Для каждого фрагмента змеи */
                for (var part = 0; part < snakeParts.length; part++) {
         /* Проверяем сгенерированные координаты на столкновение с любым фрагментом змеи */
    if (snakeParts[part].x == possible.x && 
        snakeParts[part].y == possible.y) 
        {
                        /* Если столкновение произошло, установим переменную hit */
                        hit = true;
                    }
                }
                /* Если было обнаружено столкновение */
                if (hit == true) {                
                    /* Попытаемся снова */
                    possible.x = randNum(range.x) * step;
                    possible.y = randNum(range.y) * step;                
                    hit = false;
                } else { /* В противном случае */
                    /* Новая позиция найдена */
                    flag = false;
                    this.x = possible.x;
                    this.y = possible.y;
                }            
            }        
        },    

Первая часть метода довольно проста. Используя шаг, нацело делящий ширину и высоту (20), мы создали нечто вроде невидимой сетки, по которой движется змея, и в соответствии с которой выполняется размещение на игровом поле печенья. Игровой экран разбит на клетки размером 20х20 пикселей.

Объект range принимает количество этих клеток, поэтому у нас имеются сведения о максимальном количестве клеток по осям x и y. Причём, для того, чтобы печенье не перешло за край экрана, что не очень хорошо выглядит, уменьшено количество доступных клеток по ширине и высоте на единицу.

После того, как максимум известен, генерируется случайное число в доступном диапазоне и умножается на размер шага для того, чтобы определить координаты x и y клетки, в которую мы хотим переместить печенье.

Затем полученные координаты проверяются на предмет их совпадения с одним из фрагментов тела змеи. Делается это для того, чтобы печенье не появилось прямо на змее. Если оказалось так, что в предложенной позиции печенье пересечётся со змеёй – генерируются новые координаты и проверка проводится снова. Делается это до тех пор, пока не будет получена подходящая позиция, в которую и будет перемещён спрайт, символизирующий угощение.

Добавляем печенье на игровой экран


Код, реализующий печенье, готов. Осталось лишь добавить его на игровой экран.

  1. Добавьте новый член класса в слой SnakeLayer. Он будет хранить ссылку на объект Biscuit.

    biscuit: null, // ссылка на печенье

  2. Добавьте новый метод, updateBisquit, который предназначен для обновления состояния печенья.

    updateBiscuit: function() {
    /* Если печенье уже есть на игровом экране */
    if (this.biscuit) {
    /* Переместить его */
    this.biscuit.randPosition(this.snakeParts);        
    /* Если нет */
    } else {
    /* Создать новый спрайт */
    this.biscuit = new Biscuit(this.snakeParts);
    /* и добавить к сцене в качестве объекта-потомка */
    this.addChild(this.biscuit);
    }
    },

  3. Добавьте вызов updateBiscuit в метод ctor слоя SnakeLayer.

    ctor: function () {
    ...                
    /* Дополнение для работы с печеньем */
    this.updateBiscuit();	
    ...
    }, 

  4. Запустите проект в эмуляторе и взгляните на печенье, которое управляемая игроком змея скоро сможет попробовать!


    Печенье и змея

Сейчас, даже если змею направить на печенье, съесть она его пока не может. Исправим это.

Обнаружение столкновений


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

Учитывая особенности архитектуры игры, можно каждый такт выполнять простую проверку на равенство для координат головы змеи, фрагментов её тела и печенья. Это возможно потому что объекты ShakeParts и Biscuit могут располагаться только в координатах, делимых нацело на шаг перемещения (20 пикселей). Как результат, сравнивать придётся целые числа. Если бы пришлось работать с дробными величинами, понадобилась бы гораздо более сложная система.

Займёмся реализацией системы обнаружения столкновений.

  1. Добавьте в SnakeLayer следующий метод.

    checkCollision: function() {
    var winSize = cc.view.getDesignResolutionSize();
    var head = this.snakeParts[0];
    var body = this.snakeParts;
    /* Проверка столкновения с границей экрана */
    if (head.x < 0) {
    head.x = winSize.width;
    } else if (head.x > winSize.width) {
    head.x = 0;
    }
    if (head.y < 0) {
    head.y = winSize.height;
    } else if (head.y > winSize.height) {
    head.y = 0;
    } 
    /* Проверка столкновения с собой */
    for (var part = 1; part < body.length; part++) {
    if (head.x == body[part].x && head.y == body[part].y) {
    /* Запуск сцены GameOver */
    }
    }    
    /* Проверка столкновения с печеньем */
    if (head.x == this.biscuit.x && head.y == this.biscuit.y) {
    /* Обновление позиции печенья */
    this.updateBiscuit();
    /* Увеличение длины змеи */
    this.addPart();
    }
    },

  2. Добавьте вызов нового метода в метод update объекта SnakeLayer.

    update: function(dt) {
    	
    /* Набор значений, задающих направление перемещения */
    	
    var up = 1;
    	
    /* Перемещаем объект только если истёк интервал */
    	
    if (this.counter < this.interval) {
    this.counter += dt;   
    } else {
    this.counter = 0;
    
    /* Перемещаем змею */
    this.moveSnake(this.curDir);                        
    	
    /* Проверяем, столкнулась ли голова змеи с границей экрана, её телом или с печеньем */
    	
    this.checkCollision();            
    }	
    },

Метод checkCollision выполняет три проверки. Он проверяет, пересек ли объект ShakeHead одну из границ экрана, и, если это случилось, перемещает змею так, чтобы она продолжала движение с другой стороны экрана. Он проверяет, не столкнулась ли голова змеи с одним из фрагментов её тела. Сейчас мы ничего не предпринимаем, если это случится, но уже в следующей части разберёмся с этим. И, наконец, проводится проверка на столкновение змеи с печеньем. Если это произошло, печенье переносится в новое место, а змея растёт.

Так как теперь змея, поедая печенье, растёт, можно убрать код, задающий её начальную длину, из метода ctor. Вот цикл, который больше не нужен.

ctor: function () {
...	
for (var parts = 0; parts < 10; parts++) {
this.addPart();
}	
}, 
   

Исправление недочётов управления


В управлении змеёй имеется фундаментальный недочёт. Она может наползти сама на себя. И, если только это не самое начало, и змея не состоит из одной лишь головы, она не должна, даже при нажатии соответствующей кнопки, двигаться в направлении, противоположном тому, в котором двигается в текущий момент. Это позволит предотвратить неожиданный проигрыш, когда змея начнёт двигаться в направлении собственного тела и, естественно, её голова столкнётся с одним из его фрагментов.

Для того, чтобы это исправить, нужно отслеживать текущее состояние змеи, а именно – направление, в котором она двигается.

  1. Добавьте в SnakeLayer новый член класса nextDir.

    var SnakeLayer = cc.Layer.extend({
        ...
        curDir: 0,
        nextDir: 0,
        ...
    });

  2. Уберите параметр dir из moveSnake.

    var SnakeLayer = cc.Layer.extend({
        ...
        moveSnake: function() {
            ...                    
        },
        update: function(dt) {
            ...
            this.moveSnake(this.nextDir);
            this.moveSnake();                       
            ...
        }   
    });

  3. Замените все ссылки curDir на nextDir.

    var SnakeLayer = cc.Layer.extend({
        ...
        ctor: function () {            
            /* Регистрируем обработчики событий клавиатуры */
            cc.eventManager.addListener({
    ...	
                    /* Обрабатываем нажатия на клавиши */
                    if (keyMap[keyCode] !== undefined) {
                        //targ.curDir = keyMap[keyCode]; // СТАРАЯ СТРОКА
    
    targ.nextDir = keyMap[keyCode];  // НОВАЯ СТРОКА
                    }                           
                }            
            }, this);
            
            /* Регистрация прослушивателя событий касания */
            cc.eventManager.addListener({
                onTouchMoved: function(touch, event) {
    ...
    /* Если нет изменений */
    	
    if (delta.x !== 0 && delta.y !== 0) {
    if (Math.abs(delta.x) > Math.abs(delta.y)) {                    
    /* Определяем направление посредством знака */
    //targ.curDir = Math.sign(delta.x) * right;  // СТАРАЯ СТРОКА
    targ.nextDir = Math.sign(delta.x) * right;   // НОВАЯ СТРОКА
    } else if (Math.abs(delta.x) < Math.abs(delta.y)) {
    /* Определяем направление посредством знака */
    //targ.curDir = Math.sign(delta.y) * up; // СТАРАЯ СТРОКА                        	
    targ.nextDir = Math.sign(delta.y) * up;  // НОВАЯ СТРОКА                      
    }                            
    }            
    }                  
            }, this);
        },    
        moveSnake: function() {
            ...                     
            /* Старый блок кода */
    /* Перемещаем голову змеи в текущем направлении */    
            //if (dirMap[dir] !== undefined) {
            //   dirMap[dir]();    
            //}
    /* Новый блок кода */
            /* Перемещаем голову змеи в текущем направлении */    
            if (dirMap[this.curDir] !== undefined) {
                dirMap[this.curDir]();    
            }
      ...
        },
        ...
    });

  4. Добавьте в moveSnake блок кода, который предотвращает «наползание» змеи на саму себя. Этот блок кода будет проверять два условия:

    a. НЕ противоположно ли новое направление движения (nextDir) текущему направлению (curDir)?
    b. Состоит ли змея из одной только головы (длина равна 1)?

    Если любое из утверждений верно, производится замена curDir на nextDir

    var SnakeLayer = cc.Layer.extend({
        ...
        moveSnake: function() {
            
            /* Меняем направление движения, если оно не является противоположным текущему или если змея состоит лишь из головы */
            if ((this.nextDir * -1) != this.curDir || this.snakeParts.length == 1)  {            
                this.curDir = this.nextDir;
            }
            
            /* Перемещаем голову в текущем направлении */    
            if (dirMap[this.curDir] !== undefined) {
                dirMap[this.curDir]();    
            }        	
      ...
        },
        ...
    });

Выводы


Подведём итоги сегодняшнего занятия. Вы узнали следующее.
  • Как объект Biscuit сам себя позиционирует на экране.
  • Как работает системе определения столкновений
  • Как исправить управление для того, чтобы предотвратить неожиданное «наползание» змеи на саму себя.

Вот, что вы смогли сделать, освоив материал.

  • Создать новый спрайт, символизирующий угощение.
  • Реализовать простую систему определения столкновений.
  • Ограничить перемещение змеи тремя направлениями.

Внесённые изменения позволили сделать наш вариант классической Snake почти готовой игрой. Змеёй можно управлять без риска неожиданно проиграть, она исправно ест печенье, растёт. Но проекту всё ещё чего-то не хватает. А именно, это главного меню, системы подсчёта очков, настройки сложности игры. Об этом – в следующий раз.


Часть 1 » Часть 2 » Часть 3 » Часть 4 » Часть 5 » Часть 6 » Часть 7 // Конец )

Комментарии (1)


  1. super-guest
    19.04.2016 01:02

    Такие фотки призваны стимулировать рефлексы, чем вызывать интерес. Но здесь уж перебор — статью читать не хочется. Хочется либо есть такие вкусняшки, ли узнать как приготовить :)