Hello, Keyboard!

1

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




Движение с постоянной скоростью.

На этом примере мы сможем меньше думать о красоте и вместо этого раз и навсегда отработаем прием качественной обработки клавиатуры.

Сначала, как всегда, весь рулон:


import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import flash.geom.Point;

stage.frameRate = 30;

// Это наш ГэГэ. Он может перемещаться как в
// горизонтальном, так и вертикальном направлениях.
var _gg:Sprite;

// Модуль скорости движения.
var _velValue:Number = 3.0;

// Направление, задаваемое игроком.
var _direction:Point = new Point();

// А тут у нас вспомогательные переменные, для работы с клавой.
var _keyL:Boolean = false;
var _keyR:Boolean = false;
var _keyU:Boolean = false;
var _keyD:Boolean = false;

// Рисуем демо сцену
createDemoScene();

// Слушаем необходимые события.
stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);

/** Рисуем простенькую демо сцену. */
function createDemoScene():void
{
        _gg = new Sprite();
        _gg.graphics.beginFill(0xff3300);
        _gg.graphics.drawCircle(0, 0, 10);
        _gg.graphics.endFill();
        
        _gg.x = stage.stageWidth / 2;
        _gg.y = stage.stageHeight / 2;
        addChild(_gg);
}

/** Весь движняк, как обычно. */
function onEnterFrame(event:Event):void
{
        // Вычисляем скорость на основании вектора направления.
        var velocity:Point = _direction.clone();
            velocity.normalize(_velValue);

        // И, собственно, движемся в заданном направлении.
        _gg.x += velocity.x;
        _gg.y += velocity.y;
}

/** Когда наступили на кнопочку на клавиатуре.
    Обрабатываются сразу стрелочки и WASD.

    Здесь все просто: куда только что нажали,
    туда мы и летим. Таким образом здесь у нас
    организован приоритет последней нажатой
    кнопки */
function onKeyDown(event:KeyboardEvent):void
{
        switch (event.keyCode)
        {
                case Keyboard.LEFT:
                case Keyboard.A:
                        _keyL = true;
                        _direction.x = -1;
                        break;
                case Keyboard.RIGHT:
                case Keyboard.D:
                        _keyR = true;
                        _direction.x = 1;
                        break;
                case Keyboard.UP:
                case Keyboard.W:
                        _keyU = true;
                        _direction.y = -1;
                        break;
                case Keyboard.DOWN:
                case Keyboard.S:
                        _keyD = true;
                        _direction.y = 1;
                        break;
        }
}

/** Когда кнопочку отпустили, соответственно.
    
    Здесь же при отпускании одной из парных
    кнопок (пары вправо-влево и вверх-вниз)
    мы проверяем, не нажата ли в это время
    вторая кнопка из пары. */
function onKeyUp(event:KeyboardEvent):void
{
        switch (event.keyCode)
        {
                case Keyboard.LEFT:
                case Keyboard.A:
                        _keyL = false;
                        _direction.x = _keyR ? 1 : 0;
                        break;
                case Keyboard.RIGHT:
                case Keyboard.D:
                        _keyR = false;
                        _direction.x = _keyL ? -1 : 0;
                        break;
                case Keyboard.UP:
                case Keyboard.W:
                        _keyU = false;
                        _direction.y = _keyD ? 1 : 0;
                        break;
                case Keyboard.DOWN:
                case Keyboard.S:
                        _keyD = false;
                        _direction.y = _keyU ? -1 : 0;
                        break;
        }
}

В результате у нас получается такое управление:

(не забываем тыкнуть в мувик)

Итак у нас есть простейший пример кода, который рисует демосцену, слушает как мы нажимаем и отпускаем кнопки клавиатуры и, наконец, реализует реакцию управляемого нами ГэГэ на наши манипуляции. Обработчики нажатия и отпускания клавиш организованы таким образом, что одновременное нажатие клавиш движения в противоположных направлениях отрабатывается наиболее корректно. Здесь мы не имеем приоритета движения вправо перед движением влево, как в том же зомботроне, например.

Чивоэ? Задротство, говорите? Оно и есть! Идем дальше.

Движение с ускорением.

Вот мы и научились четко определять, куда именно игрок желает послать управляемый объект. Давайте теперь научимся делать это с некоторым ускорением. Такое движение придется кстати, если нужно управлять каким-то космическим кораблем, скажем, или той же коровой на льду.

Вот код:

import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import flash.geom.Point;

stage.frameRate = 30;

// Это наш ГэГэ. Он может перемещаться как в
// горизонтальном, так и вертикальном направлениях.
var _gg:Sprite;

// Неизменная величина ускорения.
var _accValue:Number = 1.0;

// Сопротивление окружающей среды, чтобы кораблик останавливался.
var _envResistance:Number = 0.9;

// Тут будем хранить вектор скорости.
var _velocity:Point = new Point();

// Направление, задаваемое игроком.
var _direction:Point = new Point();

// А тут у нас вспомогательные переменные, для работы с клавой.
var _keyL:Boolean = false;
var _keyR:Boolean = false;
var _keyU:Boolean = false;
var _keyD:Boolean = false;

// Рисуем демо сцену
createDemoScene()

// Слушаем необходимые события.
stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);

/** Рисуем простенькую демо сцену. */
function createDemoScene():void
{
        _gg = new Sprite();
        _gg.graphics.beginFill(0xff3300);
        _gg.graphics.drawCircle(0, 0, 10);
        _gg.graphics.endFill();
        
        _gg.x = stage.stageWidth / 2;
        _gg.y = stage.stageHeight / 2;
        addChild(_gg);
}

/** Весь движняк, как обычно. */
function onEnterFrame(event:Event):void
{
        // Вычисляем теперь уже ускорение на основании
        // вектора направления.
        var acceleration:Point = _direction.clone();
            acceleration.normalize(_accValue);

        // Ускоряемся.
        _velocity = _velocity.add(acceleration);
        
        // Испытываем сопротивление среды
        _velocity.x *= _envResistance;
        _velocity.y *= _envResistance;
        
        // И наконец движемся
        _gg.x += _velocity.x;
        _gg.y += _velocity.y;
}

/** Когда наступили на кнопочку на клавиатуре. */
function onKeyDown(event:KeyboardEvent):void
{
        switch (event.keyCode)
        {
                case Keyboard.LEFT:
                case Keyboard.A:
                        _keyL = true;
                        _direction.x = -1;
                        break;
                case Keyboard.RIGHT:
                case Keyboard.D:
                        _keyR = true;
                        _direction.x = 1;
                        break;
                case Keyboard.UP:
                case Keyboard.W:
                        _keyU = true;
                        _direction.y = -1;
                        break;
                case Keyboard.DOWN:
                case Keyboard.S:
                        _keyD = true;
                        _direction.y = 1;
                        break;
        }
}

/** Когда кнопочку отпустили, соответственно. */
function onKeyUp(event:KeyboardEvent):void
{
        switch (event.keyCode)
        {
                case Keyboard.LEFT:
                case Keyboard.A:
                        _keyL = false;
                        _direction.x = _keyR ? 1 : 0;
                        break;
                case Keyboard.RIGHT:
                case Keyboard.D:
                        _keyR = false;
                        _direction.x = _keyL ? -1 : 0;
                        break;
                case Keyboard.UP:
                case Keyboard.W:
                        _keyU = false;
                        _direction.y = _keyD ? 1 : 0;
                        break;
                case Keyboard.DOWN:
                case Keyboard.S:
                        _keyD = false;
                        _direction.y = _keyU ? -1 : 0;
                        break;
        }
}

И вот результат:

(не забываем тыкнуть в мувик)

Как можно видеть, код не сильно изменился. Просто теперь мы задаем кнопками не скорость движения, а ускорение. А уже скорость либо изменяется на величину этого ускорения, либо медленно «тает», если в соответствующем направлении от игрока не поступает никаких указаний.

Назвать такую модель управления «честной» с точки зрения физики мы не можем. Но выглядит правдоподобно — и то хорошо!

Летающий на демках девайс любезно предоставил камрад dark256.

UPD
Предупреждая возможное недоумение на предмет того, что это мы так весело обсуждали все утро в каментах, сообщаю, что код был несколько изменен. Я не сделал этого сразу, из-за кажущейся мне поначалу вероятности запутать читателя. Однако в таком виде код мало того, что приучает к отделению логики от управления, так еще и воспринимается, на мой взгляд, легче.
  • +28

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

+1
Плюсую!
Вроде все просто, а постоянно сделать забываешь.
Теперь буду отсюда копипастить.
  • ryzed
  • ryzed
+2
У тебя диагональное движение ускоренное получается. Надо x и y рассчитывать из вектора направления и скорости.
0
Ну не то, что бы нужно :) можно — да. Зависит от сеттинга, которого мои примеры намеренно лишены.

Скажем, если речь идет о космическом корабле с двигателями, направленными соответствующим образом, то все правильно — по диагонали корабль будет двигаться быстрее, потому что вектора тяги ложатся на катеты, давая в результате гипотенузу :)
+1
(много буков)
Ты не прав :) Нету такой техники или животных, которые бы бегом наискось разгонялись в корень из двух раз быстрее за счет более полного использования ног :) Мощность двигателя (копыт) — довольно характерная величина и она ограничивает предельное ускорение именно по модулю (независимо от направления), а не отдельно каждую составляющую.
Если делать как предлагаешь ты — игроки очень быстро начинают ощущать, что по диагоналям гонять быстрее, в итоге получается как в думе: где тоже все бегали наискось, зажимая вперед и стрейф одновременно :)

Также: модель управления с ускорением не всегда хороша для экшинов с кнопочным управлением. Потому, что это управление третьего порядка. Поясню: игрок, в конечном итоге, хочет управлять координатами ГГ, помещая его в то или иное место пространства. Наиболее резкое и отзывчивое управление — управление «первого порядка» — когда нажатие кнопки напрямую задает кординаты (пример — волк, ловящий яйца). Управление «второго поядка» — нажатие кнопки задает скорость, а координаты меняются уже опосредовано через постепенную добавку к ним значения скорости. Управление «третьего порядка» — это управление ускорением. Оно наиболее физически-правдоподобно, но являет собой самую долгую связь от пожелания игрока до фактического изменения координат ГГ.

Исключительно важно минимизировать время задержки — от нажатия кнопки до желаемого изменения координат ГГ. Если просто задрать пределы для ускорений и скоростей, то управление может получиться отзывчивым НО не точным: игроку будет сложно задавать и отрабатывать малые перемещения. Поэтому в ход идут всякие хаки: в частности, используется смешанная модель управления: когда в начальный момент времени, когда игрок только нажал кнопку, скажем, «в бок» — ему в этом направлении пинком дается некая минимальная скорость, обеспечивающая почти мгновенное смещение ГГ в этом направлении на минимальную актуальную величину, скажем на пол корпуса. А далее, если кнопка все еще нажата, к уже имеющейся скорости плюсуется ускорение.
Способ торможения ГГ тоже важен: после отпускания кнопки, управляемая игроком штука должна как можно быстрее занять определенную поззицию, без существенных заносов и вылетов относительно координат, в которых игрок отпустил кнопку.
+1
Нету такой техники или животных, которые бы бегом наискось разгонялись
Несколько двигателей. На каждое направление по одному.
По диагонали работают сразу два.
0
В реальном мире нет никаких диагоналей и осей координат :)
У аппаратов с множеством двигателей (космический корабль) мелкие боковые моторы используются только для ориентирования (поворота), а для движения — используются маршевые двигатели. У судов с повышенной маневренностью (некоторые буксиры, некоторые спасательные катера) — расположенный внизу водомет может вращаться на 360 градусов, обеспечивая тягу в нужную сторону.
Но самое главное — когда по диагонали ускорение и\или скорость выше — это косяк, это быстро приучает игрока гонять только по диагоналям, что не правильно :)
0
Прям rocketjump какой-то )
0
Все верно, это тоже пример бага, который стал фичей :)
В думе — косой бег и ускорение об стенку (чудеса колижн-детектора, который выпердоливая игрока из стены подкидывал немножко вдоль вектора движения), в квейке — косой бег убрали, зато случайно добавили рокетждамп. :)
0
В реальном мире нет никаких диагоналей и осей координат :)
В реальном мире нет такого «рубленного» управления механизмами. А где оно есть (а оно таки кое-где есть) — там есть и «такие диагонали и оси координат» :) и скорость там складывается.
+1
Да, интересно…
А кто помнит физику? Как там по проекциям сил? )
Если на объект воздействуют две перпендикулярных силы, то какой будет результирующая по величине? Корень из суммы квадратов сил? )
Без учета трения и гравитации.
0
А, пардон, выше расписано…
0
Ну также и суммируются, см. сложение векторов :)
Для частного случая, если силы под 90 градусов, модуль будет определяться по теореме Пифагора: корень из суммы квадратов.
0
Ты раздул из мухи слона :)

Во-первых, уменьшение в полтора раза (~1.4) при диагональном движении — это вопрос одного if, который никак не влияет на суть поста. Если это настолько критичный момент — я с радостью добавлю эту «фичу» в код прямо сейчас.

Во-вторых, нельзя говорить об исключительной важности соблюдения принципов одного типа управления при реализации другого. Ты верно подметил про «не всегда». Так вот не всегда «важно минимизировать время задержки — от нажатия кнопки до желаемого изменения координат ГГ» — мы таки не всегда в играх не координатами управляем :)
0
Я не ругал, я комментировал :)

Кстати по-нормальному там не if делается, а вычисляется вектор «хотения» игрока (куда он хочет двинуть перса с учетом всех нажатых кнопок) и это вектор нормализуется до единичной длины — перед тем, как сношать этим вектором скорость или там ускорение :)

мы таки не всегда в играх не координатами управляем

— Ну да, есть еще стрельба :)
Управление же движением преследует, в конечном итоге, именно управление координатами: поместить героя в определенное место пространства, чтобы он что-то там собрал, либо что-бы не собрал вражеские выстрелы в прежнем месте. А вот например на скорость перемещения персонажа как на самобытный параметр игроку глубоко наплевать, как и на ускорение. Это если не рассматривать всякие там рэйсинги )
0
Кстати по-нормальному там не if делается, а вычисляется вектор «хотения» игрока (куда он хочет двинуть перса с учетом всех нажатых кнопок) и это вектор нормализуется до единичной длины
А ты теперь возьми и на листочке нормализуй вектор, заведомо являющийся биссектрисой произвольного квадранта, и увидишь, на что нужно поделить скорости по X и по Y, чтобы суммирующая скорость стала равна единице ;)

Ну да, есть еще стрельба :)
Ну да, есть еше вормикс, танкионлайн, прочие игры, где ты не управляешь координатой объекта ;)
0
Обертка в виде универсальной нормализации вектора позволяет легко прикрутить управление тачем или мышью, а делить на корень из двух — это стремненький хак для клавиатуры или d-пада :]
0
Тач и мышь вы найдете на третьем этаже в отделе «тачи и мыши» :)
0
И да, делить на корень из двух — это не хак, а логичная оптимизация, допустимая благодаря невозможности других случаев в данном контексте :)
0
Держи плюс!
0
Спасибо! :)
+3
Теплый ламповый пост не про деньги :)
0
Коль скоро интерес к обсуждению «диагонального вопроса» не убывает, счел уместным добавить пару строк несложного кода, призванного решить эту задачу :)
0
Хороший пост, поставил плюс.

На мой вкус неплохо бы добавить обработку потери фокуса, раз уж обещаешь «освоить аккуратное и точное считывание пожеланий игрока с помощью клавиатуры».
0
Спасибо!
Обработка потери приложением фокуса — это отдельная задача, имеющая лишь косвенное отношение к теме поста. Обещание продемонстрировать точный анализ состояния клавиш управления я сдержал на 146% :)
0
Ну как. Если фокус потеряется при зажатой клавише, то после возврата фокуса твоя программа будет считать, что клавиша зажата, хотя её давно уже отпустили. Т.е. анализ уже не совсем точный. Но в целом согласен, это чуть в сторону от темы, но не далеко.

Еще мелкий баг в примерах. В коде в статье всё ок, но скомпилированные примеры чуть сложнее, они меняют картинку в зависимости от того, куда кораблик летит. И бак такой, если зажать кнопку «влево», потом нажать «вправо», то кораблик полетит вправо, но картинка останется от движения влево. Если сделать наоборот, т.е. вначале нажать «вправо», а потом «влево» всё ок. С вверх/вниз тоже самое.
0
Чтобы не было проблем с фокусом, надо делать игры клава + мышь )) Вообще, такая комбинация управления дает значительно более высокий эффект управления процессом и обратной связи для игрока, чем просто клавиатура или просто мышь, особенно в экшинах.
0
Чтобы не было проблем с фокусом надо ставить игру на паузу при потере фокуса, обрабатывать отпускание всех нажатых клавиш, и снимать с паузы по клику, чтобы гарантированно получить фокус обратно.

В этих примерах было бы минимально достаточно только обрабатывать отпускание всех нажатых клавиш при потере фокуса.

А управление определятся скорее геймдизайном.
0
Механика экшн-игр, сделаных только под кнопки, ощущается очень старой и отнють не ламповой…
0
Но почему бы не захотеть сделать старую не ламповую механику? :-)
ИМХО тут нет предмета для спора.

Я вообще считаю, что лучше одной мышкой без клавы обходиться. И что?
+1
Все верно ты говоришь. Но важно не упускать из виду тот факт, что это некий тутор, помогающий «освоить аккуратное и точное считывание пожеланий игрока с помощью клавиатуры», а не демонстрация готовой библиотеки.

Именно поэтому, к слову, я не стесняюсь таких недопустимых в принципе вещей, как задание значений скорости (ускорения) прямо в обработчике клавиатуры — не годится (на мой вкус) в реальном проекте так мешать управление с логикой. Разумеется, должен быть некий единичный вектор, который однозначно определяет сочетание кнопок. А уже в логической части следует производить с этим вектором все необходимые манипуляции. Однако я посчитал это неуместным усложнением, которое может отвлечь от самой сути — чуткой и однозначной реакции на нажатие и отпускание клавиш управления. Возможно я ошибся и стоило (стоит) сделать все «как надо», насколько это допускает код в одном кадре (опять же, для наглядности и удобной копипасты «для поковирать»).
0
Таки изменил код согласно соображениям из второго абзаца.
Вроде даже яснее стало — напрасно боялся :)
0
Спасибо, актуально! Как раз хотел реализовать движение наподобие Chase Ace 2. :)
0
Для гурманов, есть прикольный класс KeyPoll: github.com/richardlord/Actionscript-Toolkit/blob/master/src/net/richardlord/input/KeyPoll.as.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.