Автор Тема: Seaman - Управление камерой и персонажем. (Цикл уроков по Unity3d)  (Прочитано 23071 раз)

0 Пользователей и 1 Гость просматривают эту тему.

Май 29, 2015, 13:27:23 pm
Ответ #15

Mimi Neko

  • Администратор
  • Старожил форума

  • Оффлайн
  • *****

  • 2456
  • Репутация:
    153
    • Просмотр профиля
Расширяем CharController

Основную работу по управлению анимацией исполняет наш старый класс CharController. Естественно дополненный и расширенный.

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

Из состояния Stay.
в состояние Use при нажатии клавиши 'e';
в состояние Jump при нажатии клавиши 'space';
в состояние Move, если вектор движения не нулевой.

Из состояния Use.
в состояние Stay при отпускании клавиши 'e'.

Из состояния Move.
в состояние Stay, если векор движения нулевой;
в состояние Jump при нажатии клавиши 'space';
в состояние Sneak при нажатии клавиши 'c';
в состояние Fall, если персонаж не на земле (упали с обрыва при движении).

Из состояния Jump.
в состояние Fall по окончании анимации jumpstart;
в состояние Land, если персонаж на земле (приземлились до окончания анимации).

Из состояния Sneak.
в состояние Stay, если вектор движения нулевой;
в состояние Move, если клавиша 'c' не нажата;
в состояние Fall, если персонаж не на земле (упали с обрыва при движении).

Из состояния Fall.
в состояние Land при приземлении;

Из состояния Land.
в состояния Move, Sneak, Stay в зависимости от того, какое состояние было до падения или прыжка. Вот тут нам и пригодится PUSH и POP состояний.

Итак что нам потребуется?

1. Анализ на земле персонаж или нет. Это проще всего сделать опросив CharacterController. Поэтому нам нужно найти его и сохранить. Добавляем переменную:
//Ссылка на компонент - CharacterController
 private static CharacterController _unityController;

и в метод Awake строку для инициализации этой переменной
//Находим компонент - CharacterController
 _unityController = GetComponent("CharacterController") as CharacterController;

2. Анализ окончания анимации. Тут нужно сделать маленькое отступление. Можно было бы сделать анализ окончания анимации методом в классе CharController. Однако я подумал, что это довольно распространенная операция и вынес метод в класс State.
    ///
    /// По завершении клипа анимации с именем name вызывает Action. Если такой анимации нет, или она не проигрывается - ничего не делает
    ///
    /// anim - Анимация Юнити
    /// act - Вызываемое действие по окончании анимации
    /// name - Имя клипа, окончание которого ждем
    /// return - ничего...

    static public IEnumerator FinishAnim(Animation anim, Action<string> act, string name)
    {
        AnimationState desireAnim = null;

        //для всех клипов в анимации
        foreach (AnimationState ast in anim)
        {
            //если имя не то, или анимация не проигрывается - к следующей анимации
            if (ast.name != name || !anim.IsPlaying(ast.name)) continue;

            //нашли нужную анимацию
            desireAnim = ast;
            break;
        }
        //Если все просмотрим, а нужной анимации нет
        if(desireAnim == null)
        {
            //Выводим сообщение
            Debug.LogError("Ожидание завершения непроигрываемой анимации") ;

            //и выходим из корутины
            yield break;
        }
        //время, на которое нужно вызывать WaitForSecond. Этот метод возможно будет вызван не в начале анимации, а в ее середине
        //поэтому нужно определить в каком мы кадре анимации и уже от этого "плясать"
        var waitTime = desireAnim.length - desireAnim.time + Mathf.FloorToInt(desireAnim.time / desireAnim.length) * desireAnim.length;

        //Ждем нужное время
        yield return new WaitForSeconds(waitTime);

        //вызываем нужный метод, который обработает окончание анимации
        act("finishanim-" + desireAnim.name);
    }


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

Вызов метода выглядит так:
StartCoroutine(State.FinishAnim(_firstAnim, _EndLand, "jumpland"));
Как видно для его вызова нам нужна анимация. Поэтому нам нужна переменная, где мы будем хранить любую из анимаций нашего персонажа и ее инициализация в Awake:
private Animation _firstAnim;

  ...

        Animation[] allAnim = gameObject.GetComponentsInChildren<Animation>();
        _firstAnim = allAnim[0];

3. Анализ приземления после падения. Конечно можно просто проверять на земле ли персонаж, используя CharacterController. Однако у меня при этом анимация выглядела несколько неуклюже. Лучше, если в состояние Land мы будем переходить, когда персонаж еще немного не долетел до земли. Для этого нам понадобится еще один метод, который я тоже поместил в класс State из-за его универсальности.
     ///
    /// Анализ приземления объекта
    ///
    /// go - объект
    /// act - метод, вызываемый при обнаружении приземления
    /// duration - время, которое ждем приземления
    /// distance - расстояние от персонажа до земли, при котором приземление будет обнаружено
    /// return - ничего...

    static public IEnumerator FinishLanding(GameObject go, Action<string> act, float duration, float distance)
    {
        //Время начала анализа
        float t = Time.time;

        //флаг - вышло ли время ожидания приземления
        bool timelimit =false;

        //трансформ объекта. Можно было бы его сюда не выносить, однако получение трансформа достаточно затратная операция
        //а такие операции лучше не делать в цикле
        Transform start = go.transform;

        RaycastHit hit;

        //делаем
        do
        {
            //ждем 10 миллисекунд
            yield return new WaitForSeconds(0.01f);

            //если время ожидания не вышло - на следующий цикл
            if (Time.time - t <= duration) continue;

            //время вышло - устанавливаем флаг
            timelimit = true;

            //и выходим из цикла
            break;

        //продолжаем цикл пока не обнаружили какой-нибудь коллайдер прямо под ногами на расстоянии меньшем чем distance
        //начальная точка испускания луча - текущая позиция объекта. При падении объекта она будет меняться
        } while (Physics.Raycast(start.position, Vector3.down, out hit, distance) != true);

        //Вызываем метод обработки приземления и передаем ему флаг - вышло время, или приземлились
        act(timelimit ? "timelimit" : "finishlanding");
    }

Тут нужно учесть, что используется позиция объекта. А она совпадает с Пивотом в Максе. Т.е. если у Вас пивот вдруг будет в макушке персонажа, тогда distance нужно указывать чуть более высоты персонажа, а если в пятке, тогда просто немного больше нуля.

4. Начальное состояние нашего персонажа - Stay, поэтому нам нужно перейти в него при запуске скрипта. Добавляем метод Start:
    void Start()
    {
        //начальное состояние - Stay
        State.GoToState(tp, "Stay");
    }

Здесь просто вызывается метод класса состояний, который переводит персонаж в состояние Stay.

Рассмотрим это поподробнее. Что при вызове GoToState произойдет? Состояние Stay ищется в списке состояний нашего класса CharController. Если оно будет найдено ищем текущее состояние. В данном случае текущего состояния пока нет. Поэтому будет просто вызван метод, который мы укажем при регистрации состояний класса как метод входа в состояние. Какой делаем вывод? Правильно - нужно сначала проиниацилизировать все состояния класса. Метод _InitStates вызывается в конце метода Awake
     private void _InitStates()
    {
        //для всех возможных состояний
        foreach (string st in Enum.GetNames(typeof(StateMove)))

            //Регистрируем состояние в классе состояний
            State.AddState(new StateDesc(tp, st, _Begin, _End, _Push, _Pop));
    }

    //Перечисление - все возможные состояния движения персонажа
    public enum StateMove
    {
        Stay,
        Move,
        Sneak,
        Jump,
        Fall,
        Land,
        Climb,
        Use
    }

Здесь для всех состояний используются одни и те же методы обработки входа, выхода, push и pop. И хотя нам методы End и Push не понадобятся, я их включил для примера. Вы можете для каждого состояния использовать свои методы, а можете наоборот передавать вместо методов null, если Вам они не нужны.

Думаю на сегодня я завершу урок. Информации и так достаточно. В следующем уроке мы продолжим разбираться с нашим расширенным классом - контроллером персонажа. Напишем методы обработки событий входа в состояние и снятия состояния со стека.

До встреч.

Май 29, 2015, 14:04:27 pm
Ответ #16

Mimi Neko

  • Администратор
  • Старожил форума

  • Оффлайн
  • *****

  • 2456
  • Репутация:
    153
    • Просмотр профиля
Расширяем CharController 2


Рассмотрим, что нам нужно сделать когда мы входим в какое-либо состояние.
сохранить на всякий случай в какое состояние мы вошли (на самом деле мы в любой момент можем спросить у класса State, в каком мы состоянии в данный момент, но зачем нам лишний вызов?);
сообщить классам CharAnimator и CharMotor, что мы перешли в новое состояние. Класс CharAnimator при этом запустит нужную анимацию, а CharMotor совершит прыжок. (Да ранее мы посылали ему сообщение _DidJump, но зачем плодить сообщения, если мы все равно будем посылать одно - о смене состояния!)

Вспомним что у нас за входные параметры были в методе _SwitchState класса CharAnimator. Это была специальная структура SwAnim, определенная в классе CharController. Значит нам нужно ее сформировать при посылке сообщения.

Какой метод вызывается при переходе в новое состояние? В конце предыдущего урока мы написали метод регистрации состояний класса. Там была такая строка:

State.AddState(new StateDesc(tp, st, _Begin, _End, _Push, _Pop));
Четыре последних параметра - это методы, которые вызываются при входе в состояние, выходе из него, когда состояние кладется на стек и когда снимается со стека. Получается нам нужен метод с именем _Begin. Ну а если посмотреть класс State можно увидеть, что параметром всех этих методов может быть строка (public Action<string> Enter).

В результате получаем такой простой метод, который будет вызываться при входе в любое состояние класса CharController:
     private void _Begin(string s)
    {
        //Запоминаем какое у нас текущее состояние
        _currentState = s;

        //Переключаем анимацию (Переносим в CharAnimator!!!)
        SendMessage("_SwitchState", new SwAnim(s, move.normalized, _speed), SendMessageOptions.DontRequireReceiver);
    }

Методы _End и _Push пустые - в них ничего не делается. Достаточно того, что State делает внутри себя. Push просто сохраняет текущее состояние в стеке состояний, при этом не переключая состояния. А раз состояния не переключаются - нам в данном случае делать нечего. Метод End нужен будет, если нам придется производить какие-то действия перед выходом из состояния (аналогично Destroy и подобным методам - закрыть файлы, еще что-то подобное). Т.е. сейчас нам ни к чему. Метод _Pop полностью повторяет метод _Begin. Чем отличается простой переход в новое состояние от снятия состояния со стека? Только тем, что в первом случае мы прямо указываем новое состояние, а во втором мы его можем и не знать - оно снимается со стека. Поэтому мы можем записать:

   
    private void _End(string s) {}

    private void _Push(string s) { }

    private void _Pop(string s)
    {
        //Запоминаем какое у нас текущее состояние
        _currentState = s;

        //Переключаем анимацию (Переносим в CharAnimator!!!)
        SendMessage("_SwitchState", new SwAnim(s, move.normalized, _speed), SendMessageOptions.DontRequireReceiver);
    }

А можем сделать проще - изменить строку регистрации состояний на:
State.AddState(new StateDesc(tp, st, _Begin, null, null, _Begin));
и пока забыть об этих трех методах.

Кардинальные изменения претерпел метод _GetHandleInput. До сих пор он был чрезвычайно простой. Мы проверяли если нажата и прошло достаточно времени с предыдущего прыжка и персонаж на земле - тогда можно послать сообщение CharMotor сделать прыжок.

Сейчас все значительно усложняется. Во первых нам нужно проверять в каком мы состоянии, прежде чем сделать какие-то действия, т.к. переходы между состояниями не произвольны. Во вторых нужно вместо посылки сообщения о прыжке просто переходить в состояние Jump. (Вспомните - при переходе в состояние вызывается метод _Begin, а там уже есть посылка сообщения о смене состояния, которую и обработает наш CharMotor) И что я еще забыл(?) - нужно перед сменой состояния положить текущее состояние на стек, чтобы при приземлении можно было его восстановить. Ну и окночательно - нужно запустить корутину окончания анимации старта прыжка. Передать этой корутине специальный метод, который она вызовет, когда анимация закончится. Этот метод переведет наш класс в состояние Fall - перс будет в "свободном падении", т.е. какое то время по инерции будет набирать высоту, затем падать. Также передаем ей название анимации, окончание которой мы ждем.

В общем вот кусок метода, который обрабатывает клавишу Jump:

         //Если клавиша jump нажата - прыжок и сотояния Stay или Move. Т.е. прыжок можно сделать только из этих состояний
        if (Input.GetButton("Jump" ) &amp;&amp; (_currentState == "Stay" || _currentState == "Move" ))
        {
            //Запоминаем текущее состояние в стеке состояний
            State.PushState(tp);

            //Переходим в состояние Jump. При этом вызывается Begin. Получается, что в следующий раз мы сюда не попадем,
            // т.к. состояние уже будет не Stay и не Move
            State.GoToState(tp, "Jump");

            //Запускаем корутину ожидания конца анимации начала прыжка. После завершения анимации вызовет метод _EndJump
            StartCoroutine(State.FinishAnim(_firstAnim, _EndJump, "jumpstart"));
        }

Сразу напишем метод _EndJump. Он вызывается когда анимания старта прыжка проиграна и персонаж перешел в свободное падение. В нем нам нужно перейти в это состояние Fall, и запустить корутину, которая будет ждать приземления персонажа.

     //Вызывается по окончании анимации "начало прыжка"
    private void _EndJump(string s)
    {
        //В состояние Fall. (Это не значит, что мы падаем. При этом мы можем и подниматься по инерции)
        State.GoToState(tp, "Fall");

        //Запускаем корутину ожидания приземления. После завершения анимации вызовет метод _EndFalling.
        StartCoroutine(State.FinishLanding(gameObject, _EndFalling, 10, 0.3f));
    }

Здесь появилась еще одна переменная - tp. Это просто typeof(CharController). Я ленивый человек, и если можно написать два символа - tp вместо этого, то почему бы нет. Тем более что typeof(CharController) встречается несколько раз. Правда придется ввести эту переменную и инициализировать ее в Awake:

     public Type tp;

    ....

        tp = typeof (CharController);


______

Лирическое отступление. Оказывается эта переменная нам встретилась еще в предыдущем уроке! А я о ней ничего не сказал :(

______

Далее. Корутине, ожидающей приземления мы передаем:
 объект, на котором висит скрипт, чтобы та могла определить его позицию;
название метода _EndFalling, который будет вызван, если наш персонаж приземлится, или если выйдет время;
время ожидания приземления 10 секунд - если за это время персонаж не приземлился - значит слишком высоко - по идее тут нужно заканчивать игру;
расстояние от пивота персонажа до земли, при котором срабатывает корутина (у меня пивот в пятках, поэтому расстояние достаточно маленькое).

Метод _EndFalling вызывается также когда персонаж упал с обрыва. Рассмотрим его. Корутина ожидания приземления передает ему строку timelimit, если вышел лимит времени, или finishlanding, если приземлились. Первым делом мы анализируем ее. Если timelimit пока мы просто выведем в лог сообщение, т.к. персонаж у нас пока бессмертный. Если finishlanding - переходим в состояние Land и запускаем корутину ожидания анимации приземления.

     //Вызывается при приземлении после падения/прыжка. На входе - строка - результат падения.
     // Если падали слишком долго - "timelimit", если приземлились - "finishlanding"
    private void _EndFalling(string s)
    {
        //Проверяем результат падения
        switch (s)
        {
            //Слишком долго падали
            case "timelimit":
                Debug.Log("Бесконечное падение - окончание игры");
                break;

            //Приземлились
            case "finishlanding":

                //В состояние Land
                State.GoToState(tp, "Land");

                //Запускаем корутину ожидания конца анимации приземления.
                // После завершения анимации вызовет метод _EndLand
                StartCoroutine(State.FinishAnim(_firstAnim, _EndLand, "jumpland"));
                break;
        }
    }

Метод _EndLand вызывается только при приземлении. Так что самое время рассмотреть его. Он чрезвычайно простой. Мы просто восстанавливаем состояние из стека состояний и все!

     //Вызывается по окончании анимации приземления

    private void _EndLand(string s)
    {
        //Восстанавливаем сохраненное в стеке состояние
        State.PopState(tp);
    }

Ну, наверное на сегодня хватит. В следующий раз рассмотрим реакцию на клавишу Sneak, изменение скорости перемещения и скорее всего закончим рассмотрение расширенного класса CharController.

До встреч!

Май 29, 2015, 17:26:38 pm
Ответ #17

Mimi Neko

  • Администратор
  • Старожил форума

  • Оффлайн
  • *****

  • 2456
  • Репутация:
    153
    • Просмотр профиля
Расширяем CharController 3


Какие нажатия клавиш мы еще не рассмотрели?

Нажатие клавиши Sneak, клавиши Speed и клавиши SpeedLock.

Если нажата клавиша Sneak мы можем перейти в состояние с тем же именем, но только если двигаемся. Можно было бы переходить в Sneak и из состояния Stay, но тогда нужна анимация idle для режима подкрадывания. У меня ее нет, да и по моему это не нужно.

При соблюдении условий нужно просто перейти в это состояние:
     //Если нажата Sneak и состояние Move
     if (Input.GetButton("Sneak") &amp;&amp; _currentState == "Move" )
    {
            //Переходим в состояние Sneak
            State.GoToState(tp, "Sneak") ;
     }

Сразу же проверяем выход из этого состояния. Здесь желательно проанализировать - может мы одновременно с отпусканием клавиши Sneak прекратили движение? и в зависимости от вектора движения переходить или в Stay, или в Move:

        //Если Sneak не нажата и текущее состояние Sneak
        if (!Input.GetButton("Sneak") &amp;&amp; _currentState == "Sneak" )
        {
            //Переходим либо в Move, либо в Stay в зависимости от вектора движения
            State.GoToState(tp, move.magnitude != 0 ? "Move" : "Stay") ;
        }

Заодно тут же, хотя мы об этом и не говорили, проанализируем клавишу Use. Действие "на бегу" совершить нельзя, поэтому мы еще проверяем состояние - действуем только если Stay. Действия уже знакомые - переходим в состояние Use и запускаем корутину ожидания окончания анимации "action". На самом деле все должно быть несколько сложнее. Дело в том, что действий несколько. Открывание двери - одно, открывание сундука - другое. Возможно придумать еще действия. Нужно анализировать высоту объекта, с которым взаимодействуешь и из этого запускать разные анимации. Но это мы оставим на потом.
        //Если нажали 'E' и состояние Stay Т.е. использовать можем только если стоим...
        if (Input.GetKeyDown(KeyCode.E) &amp;&amp; _currentState == "Stay" )
        {
            //Переходим в состояние Use
            State.GoToState(tp, "Use") ;

            //Запускаем корутину ожидания конца анимации "действие".
            // После завершения анимации вызовет метод _EndUse
            StartCoroutine(State.FinishAnim(_firstAnim, _EndUse, "action")) ;
         }

При однократном нажатии на клавишу SpeedLock (у меня назначена на левый Shift) персонаж переходит в режим run и остается в нем. При нажатии и удержании клавиши Speed (левый Alt) действие SpeedLock инвертируется. Т.е. если персонаж шел - он побежит, если бежал - пойдет. Для обработки SpeedLock нам нужно запоминать ее состояние. Следовательно нужна переменная для этого - _isSpeedLock. Инверсия действия этой клавиши легко осуществляется логической операцией XOR - в C# это - ^ . По результатам это операции мы устанавливаем флаг скорости движения _speed = 0 или 1. Почему не просто булевая переменная? Потому что так проще в CharMotor.

Далее мы проверяем изменился ли этот флаг скорости, чтобы зря не посылать приказ CharMotor. Чтобы сделать такую проверку нужно сохранять этот флаг в специальной переменной - _lastspeed. Ну и в конце-концов посылаем стандартное сообщение _SwitchState, чтобы CharAnimator мог переключить анимации.

Конечный итог - наш новый метод обработки ввода с клавиатуры:
     private void _GetHandleInput()
    {
        //Если клавиша jump нажата - прыжок и сотояния Stay или Move. Т.е. прыжок можно сделать только из этих состояний
        if (Input.GetButton("Jump") &amp;&amp; (_currentState == "Stay" || _currentState == "Move" ))
        {
            //Запоминаем текущее состояние в стеке состояний
            State.PushState(tp);

            //Переходим в состояние Jump. При этом вызывается Begin. Получается, что в следующий раз мы сюда не попадем, т.к. состояние не Stay и не Move
            State.GoToState(tp, "Jump" ) ;

            //Запускаем корутину ожидания конца анимации начала прыжка. После завершения анимации вызовет метод _EndJump
            StartCoroutine(State.FinishAnim(_firstAnim, _EndJump, "jumpstart" )) ;
    }

        //Если нажата Sneak и состояние Move
        if (Input.GetButton("Sneak") &amp;&amp; _currentState == "Move" )
        {
            //Переходим в состояние Sneak
            State.GoToState(tp, "Sneak" ) ;
    }

        //Если Sneak не нажата и текущее состояние Sneak
        if (!Input.GetButton("Sneak") &amp;&amp; _currentState == "Sneak" )
        {
            //Переходим либо в Move, либо в Stay в зависимости от вектора движения
            State.GoToState(tp, move.magnitude != 0 ? "Move" : "Stay" ) ;
    }
        //Если нажали 'E' и состояние Stay Т.е. использовать можем только если стоим...
        if (Input.GetKeyDown(KeyCode.E) &amp;&amp; _currentState == "Stay" )
        {
            //Переходим в состояние Use
            State.GoToState(tp, "Use" ) ;

            //Запускаем корутину ожидания конца анимации "действие". После завершения анимации вызовет метод _EndUse
            StartCoroutine(State.FinishAnim(_firstAnim, _EndUse, "action" )) ;
    }
        //Если нажата SpeedLock
        if (Input.GetButtonDown("SpeedLock" ))

            //Инвертируем флаг
            _isSpeedLock = !_isSpeedLock;

        //0 - ходьба; 1 - бег
        _speed = _isSpeedLock ^ Input.GetButton("Speed") ? 1 : 0;

        //Проверяем сменилась ли скорость
        if (_speed == _lastspeed) return;

        //Сохраняем новую скорость
        _lastspeed = _speed;

        //Посылаем сообщение - что скорость сменилась, чтобы можно было сменить анимацию.
        SendMessage("_SwitchState", new SwAnim(_currentState, move.normalized, _speed), SendMessageOptions.DontRequireReceiver);
    }


Какие перходы состояний мы еще не рассмотрели?
Если стояли и пошли
Если двигались и остановились и на земле
Если двигались и упали

Эти изменения состояний зависят не от специальных клавиш, а от вектора движения и условия - на земле ли персонаж. Поэтому я вынес их проверку в отдельный метод, который вызывается после обработки ввода с клавиатуры - _CheckStates. Действуем прямо по написанному.

1. Если стояли и пошли. Т.е. если состояние Stay и вектор движения != 0. Тогда мы просто переходим в состояние Move.

2. Если двигались и остановились и на земле. Двигались - значит состояние Move или состояние Snake. Остановились - значит вектор движения == 0. На земле - если найденный нами в Awake _unityController вернет isGrounded - true. В этом случае мы просто переходим в состояние Stay.

-------------

Отступление. Заметьте - как нам облегчает жизнь наш класс State. Нам уже не надо посылать сообщения каждый раз о смене анимации, делать массу "лишних" телодвижений. Просто - "перешли в новое состояние" и все!

--------------

3. Если двигались и упали. Т.е. или Stay, или Move и unityController вернет isGrounded - false. Здесь работы немного больше. После приземления нам нужно восстановить предыдущее состояние. Поэтому кладем состояние на стек. Далее переходим в состояние Fall и запускаем корутину ожидания приземления.

Полностью метод:
     private void _CheckStates()
    {
        //Если стояли и пошли
        if(_currentState == "Stay" &amp;&amp; move.magnitude != 0)

            //В состояние Move
            State.GoToState(tp, "Move" ) ;

        //Если двигались и остановились и на земле
        if ((_currentState == "Move" || _currentState == "Sneak") &amp;&amp; move.magnitude == 0 &amp;&amp; _unityController.isGrounded)<br />            //В состояние Stay<br />            State.GoToState(tp, "Stay" ) ;

        //Если двигались и упали!!!
        if((_currentState == "Move" || _currentState == "Sneak" ) &amp;&amp; !_unityController.isGrounded)
        {
            //Сохраняем предыдущее состояние в стеке состояний
            State.PushState(tp);

            //В состояние Fall. Т.о. в следующем кадре мы сюда не попадем!!! Состояние же теперь будет не Move или Sneak!
            State.GoToState(tp, "Fall" ) ;

            //Запускаем корутину ожидания приземления. После завершения анимации вызовет метод _EndFalling.
            StartCoroutine(State.FinishLanding(gameObject, _EndFalling, 10, 0.3f)) ;
        }
    }


Осталось только чуток изменить метод Update:
     void Update()
    {
        //Если камеры нет - ничего не делаем
        if (Camera.mainCamera == null) return;
        if (!IsControllable) return;

        //Обрабатываем введенные игроком данные
        _GetMoveInput();
        _GetHandleInput();

        //Проверяем изменение состояния
        _CheckStates();

        //Говорим CharMotor, что пора двигаться
        SendMessage("_DidMove", new SwAnim(_currentState, move, _speed), SendMessageOptions.DontRequireReceiver);
    }


Завтра рассмотрим измененный класс CharMotor и небольшой перерыв, пока я готовлю Climb.

До встреч!


Май 29, 2015, 17:37:00 pm
Ответ #18

Mimi Neko

  • Администратор
  • Старожил форума

  • Оффлайн
  • *****

  • 2456
  • Репутация:
    153
    • Просмотр профиля
Обновленный CharMotor


Прежде чем перейти к классу CharMotor, пара фраз о вводе с клавиатуры. В процессе тестирования обнаружился баг. Нажимаем W(вперед), затем не отпуская W нажимаем Alt(скорость), затем также ничего не отпуская - Space(прыжок). Все нормально, все прыгает. Однако после отпускания любой клавиши также продолжает прыгать. Иногда прыгать перестает, но продолжает бежать, даже если ничего уже не нажимаешь. Попытка переназначить на другие клавиши (Alt все-же специфическая клавиша - думал из-за нее) - ничего не дало. Устраняется баг заменой Input.GetButton на Input.GetKeyDown(KeyCode.Space) для прыжка и Input.GetKey(KeyCode.R) для скорости. Думаю в самой игре можно использовать Input.GetKey(KeyCode.LeftAlt), но в редакторе это глючит - Alt используется еще и для других целей.

Итак. Что у нас изменилось в CharMotor.

1. Мы теперь управляем скоростью. Поэтому ему нужно передавать не только вектор движения, но и с какой скоростью бежит персонаж. Для этого мы ввели структуру SwAnim. Передаем ее методу _DidMove:
    private const float _RUN_SCALE = 2f;
    private const float _SQUATT_SCALE = 0.5f;

    //показывает на сколько нужно умножать вектор смещения в зависимости от состояния - ходьба, бег, подкрадывание...
    private float _scale;

 
    private void _DidMove(CharController.SwAnim move)
    {
        //Скорость бег/ход
        int run = move.speed;

        //Если режим Move
        if (move.state == "Move" )

            //Прибавляем к скорости 2. Получаем Sneak ход = 0, Sneak бег = 1, Move ход = 2, Move бег =3
            run += 2;

        //Устанавливаем скорость
        _SetSpeed(run);

        //Поворачиваем персонаж соответственно камере
        _RotateChar(move.move);

        //Двигаем персонаж
        _ProcessMotion(move.move);
    }

 
    private void _SetSpeed(int run)
    {
        //Если не на земле
        if (!_unityController.isGrounded) return;

        switch (run)
        {
            case 0:
                _scale = _SQUATT_SCALE;
                break;

            case 1:
                _scale = 1;
                break;

            case 2:
                _scale = 1;
                break;

            case 3:
                _scale = _RUN_SCALE;
                break;

            default:
                _scale = 1;
                break;
        }
    }


Вначале формируем параметр, который показывает скорость движения в зависимости от состояния персонажа и преданной скорости. Затем вызываем метод, который собственно и устанавливает масштаб скорости, т.е. коэффициент, на который будем умножать вектор движения. Естественно ввели нужную переменную и константы.

До сих пор у нас присутствовал такой "баг" - если мы двигались, прыгнули, и в процессе прыжка отпустили клавишу движения - персонаж переставал двигаться и просто опускался в том месте где отпустили клавишу. Это неправильно - персонаж в полете должен продолжать движение по инерции. Для этого мы будем постоянно сохранять наш вектор движения, а в полете использовать не тот, который приходит от контроллера, а сохраненный.

     private void _SwitchState(CharController.SwAnim sw)
    {
        if (sw.state == "Jump" )
        {
            //Если с предыдущего прыжка пршло слишком мало времени
            if (_lastJumpTime + _JUMP_REPEAT_TIME > Time.time) return;

            //Если не на земле
            if (!_unityController.isGrounded) return;

            //Нужно сделать прыжок - устанавливаем начальную вертикальную скорость
            _verticalVelocity = _JUMP_SPEED;

            //Запоминаем время прыжка
            _lastJumpTime = Time.time;
        }
        _ProcessMotion(sw.move);
    }

Он почти повторяет предыдущий _DidJump. Только проводится анализ состояния, в которое переходим, т.к. вызывается при переходе в любое состояние.

Вот в-общем то все программирование в третьей части цикла уроков. Осталось создать персонажа, наложить на него нужные анимации, написанные нами скрипты и получить что-то вроде Этого
« Последнее редактирование: Май 29, 2015, 20:19:55 pm от Mimi Neko »