Към съдържанието
PN Scripts

Игри

Усещане при удар в Unity: замръзване и тресене на камерата, без да чупите мащаба на времето

  • PN Scripts
  • 9 мин четене

Ударът, който попада, има нужда от миг, за да се усети. Бойните и екшън игрите спират действието за няколко стотни от секундата, когато ударът попадне (hit-stop, замръзване на кадрите), и леко разтърсват камерата. В Unity и двете обикновено се добавят за един следобед с Time.timeScale и случайно отместване на камерата и и двете водят до грешките, които играчите забелязват първи: игра, която остава замръзнала след два бързи удара, забавен каданс, който свършва по-рано, камера, която трепери или се измества. Тук показваме как да направите двата ефекта така, че да не чупят останалата част от играта. Частите на чист C# са пуснати на .NET 8, а всеки скрипт е компилиран спрямо библиотеките на Unity 6; самият редактор на Unity не е пускан.

#Замръзването, което всеки пише първо

using System.Collections;
using UnityEngine;

public sealed class NaiveHitStop : MonoBehaviour
{
    public void Hit(float seconds) => StartCoroutine(HitStopNaive(seconds));

    IEnumerator HitStopNaive(float seconds)
    {
        float previous = Time.timeScale;
        Time.timeScale = 0f;
        yield return new WaitForSeconds(seconds);   // scaled time: never ends at time scale 0
        Time.timeScale = previous;
    }
}

То има три проблема, в реда, в който ги срещате.

  • Никога не свършва. WaitForSeconds брои мащабирано време, а документацията на Unity казва ясно, че корутините, които чакат с него, не продължават, докато мащабът на времето е нула. Първият удар замразява играта завинаги. WaitForSecondsRealtime оправя този проблем.
  • Застъпени удари. С чакането в реално време втори удар 50 ms след първия запомня мащаба, който намира, тоест 0, и връща 0, когато свърши. Симулирахме точно това: след два удара мащабът на времето остана 0. Играта отново е замръзнала, само че по-рядко, което я прави по-трудна за възпроизвеждане.
  • Няма граница. Пушка или огнехвъргачка може да извиква Hit() на всеки кадър. Тогава дори правилно замръзване спира играта, докато бутонът е натиснат.

#Часовник за замръзване, който върви в реално време

Решението е замръзването да е малък часовник със собствено състояние, задвижван от реалното време, а не списък от корутини, всяка от които мисли, че притежава мащаба на времето. Той запомня мащаба на играта веднъж, когато замръзване започва от нищото; замръзване, което идва по време на друго, го удължава; а веригата от замръзвания има максимална дължина, след която за кратко новите замръзвания се пренебрегват:

using System;

public sealed class HitStopClock
{
    public float MaxChain = 0.3f;   // longest freeze in one chain of hits, real seconds
    public float Cooldown = 0.12f;  // after a capped chain, new freezes wait this long

    float _left, _chain, _sinceCapped = float.MaxValue;
    bool _capped;

    public float BaseScale { get; private set; } = 1f;
    public float Scale { get; private set; } = 1f;
    public bool Active => _left > 0f;

    public void Freeze(float seconds, float scale, float currentTimeScale)
    {
        if (!Active && _sinceCapped < Cooldown) return;
        if (!Active)
        {
            BaseScale = currentTimeScale;   // the game's own scale, remembered once
            Scale = scale;
            _chain = 0f;
        }
        else
        {
            Scale = Math.Min(Scale, scale);
        }
        _left = Math.Max(_left, Math.Min(seconds, MaxChain - _chain));
        _capped = _chain + _left >= MaxChain - 1e-6f;
    }

    // Call once a frame with real (unscaled) time; returns the time scale to use now.
    public float Step(float realDt, float currentTimeScale)
    {
        if (!Active)
        {
            if (_sinceCapped < float.MaxValue) _sinceCapped += realDt;
            return currentTimeScale;
        }
        float used = Math.Min(realDt, _left);
        _left -= used;
        _chain += used;
        if (Active) return Scale;
        _sinceCapped = _capped ? 0f : float.MaxValue;
        return BaseScale;
    }

    public float Release()
    {
        _left = 0f;
        return BaseScale;
    }
}

В него няма типове на Unity, затова можахме да го пуснем на .NET при 60 кадъра в секунда:

  • замръзване от 0,08 s, докато играта вървеше на забавен каданс 0,5, продължи 5 кадъра и върна 0,5, а не 1;
  • два удара през 50 ms дадоха едно замръзване от около 0,15 s и след това нормална скорост;
  • удар на всеки кадър в продължение на две секунди нито веднъж не замрази играта за повече от около 0,3 s наведнъж, а между веригите играта продължаваше.

Компонентът около него е кратък. Той чете Time.unscaledDeltaTime, което продължава да тече, докато мащабът на времето е нула, и връща мащаба на играта, ако бъде изключен по средата на замръзване, например когато сцената се освобождава:

using UnityEngine;

public sealed class HitStop : MonoBehaviour
{
    readonly HitStopClock _clock = new HitStopClock();

    public void Freeze(float seconds, float scale = 0f)
    {
        _clock.Freeze(seconds, scale, Time.timeScale);
        if (_clock.Active) Time.timeScale = _clock.Scale;
    }

    void Update()
    {
        float now = _clock.Step(Time.unscaledDeltaTime, Time.timeScale);
        if (now != Time.timeScale) Time.timeScale = now;
    }

    void OnDisable()
    {
        if (_clock.Active) Time.timeScale = _clock.Release();
    }
}

Дръжте замръзванията кратки. Обичайни са стойности между 0,03 и 0,12 секунди; лекият удар взема долния край, а критичният горния. Ако играта ви има меню за пауза, което също задава мащаба на времето, дайте на паузата отделен флаг и оставете часовника да отговаря само за замръзването, иначе двете ще се отменят едно друго.

#Какво още спира при мащаб нула

Замразяването на Time.timeScale замразява всичко, което върви на мащабирано време, и обикновено точно това искате, с няколко изключения, за които да решите нарочно:

  • Физиката. FixedUpdate не се извиква при мащаб нула, така че телата спират точно където са. Това е смисълът на замръзването.
  • Анимацията. Аниматорите в режима по подразбиране също замръзват. Аниматор на интерфейса, например лента за живот, която намалява, може да ползва режима Unscaled Time, за да продължи да се движи.
  • Частиците. Искрата в точката на удара изглежда по-добре, ако се играе по време на замръзването; основният модул на системата от частици има превключвател за немащабирано време.
  • Тресенето на камерата. Ако тресенето върви на мащабирано време, то спира за времето на замръзването, точно когато ударът трябва да се усеща най-силно. Пускайте го на немащабирано време.

#Тресене на камерата, което не я измества

Другата половина на ефекта е тресенето, а повечето му грешки идват от Random.insideUnitSphere и от писането в собствената позиция на камерата. Случайните отмествания се сменят напълно на всеки кадър, така че тресенето изглежда като шум на счупен екран и става по-лошо при висока честота на кадрите. Писането в позицията на камерата се бори с всичко, което мести камерата: скрипт за следване, Cinemachine, филмова сцена.

Моделът с „травма“, популяризиран от лекция на GDC за тресенето на камерата, избягва и двете. Ударите добавят „травма“ между 0 и 1, травмата намалява равномерно с времето, а силата на тресенето е травмата на квадрат. Малък удар, който добавя 0,2 травма, дава сила 0,04 и едва мърда камерата; тежък, който я вдига до 0,8, дава 0,64. Плавният шум вместо случайни числа кара движението да изглежда като ударена камера, а не като повреда. Сложете скрипта на собствен дъщерен обект между рамката на камерата и самата камера и той може просто да притежава своята локална позиция:

using UnityEngine;

// On its own child object between the camera rig and the Camera.
public sealed class TraumaShake : MonoBehaviour
{
    public float decay = 1.4f;       // trauma lost per second
    public float maxOffset = 0.4f;   // units at full trauma
    public float maxRoll = 3f;       // degrees at full trauma
    public float frequency = 18f;    // bumps per second
    [Range(0f, 1f)] public float intensity = 1f;   // the player's "screen shake" setting

    float _trauma, _time;

    public void AddTrauma(float amount) => _trauma = Mathf.Clamp01(_trauma + amount);

    void LateUpdate()
    {
        float dt = Time.unscaledDeltaTime;   // keeps shaking during hit-stop
        _time += dt * frequency;
        float s = _trauma * _trauma * intensity;
        transform.localPosition = new Vector3(Noise(0f), Noise(10f), 0f) * (maxOffset * s);
        transform.localRotation = Quaternion.Euler(0f, 0f, Noise(20f) * maxRoll * s);
        _trauma = Mathf.Max(0f, _trauma - decay * dt);
    }

    float Noise(float channel) => Mathf.PerlinNoise(_time, channel) * 2f - 1f;
}

Понеже скриптът за следване мести родителя, а тресенето мести само дъщерния обект, никой не презаписва другия и когато травмата стигне нула, дъщерният обект е точно в нулата. Малкото завъртане прави повече от голямото преместване: три градуса наклон се четат като силен удар, а половин единица преместване при пълна травма вече е много за 2D игра.

Запазете полето intensity. Тресенето на екрана е едно от нещата, които играчите най-често искат да намалят, за удобство или заради морска болест, а плъзгач в настройките, който подава тази стойност, не струва нищо.

#Как да съберете един удар

При удар извиквайте двете от едно място и ги мащабирайте със силата на удара: травмата расте със силата, а замръзването по-бавно, например с корен квадратен, така че критичният удар да се усеща по-тежък, без да спира играта два пъти по-дълго. Добавете кратко проблясване на целта и малко отблъскване и ударът се чете ясно дори в натоварена сцена. За игрите в браузъра същите идеи важат с други инструменти; вижте ръководството ни за усещането в браузърна игра.

#Пакет, който го прави

Събрахме тези части в Tresko, пакет за усещане при удар в Unity: тресене на камерата с травма на немащабирано време, замръзване, което връща мащаба на времето на играта, със застъпени замръзвания и ограничена верига, свиване и разтягане на пружина, проблясване при удар без копия на материали, отблъскване, което изминава точно разстояние при всяка честота на кадрите, „удар“ по мащаб или позиция, 18 криви за плавност и едно извикване Impact() за всичко. Математиката е ядро на чист C#, проверено с 23 теста на .NET 8; пакетът е компилиран спрямо библиотеките на Unity 6000.5.4, но редакторът на Unity не е пускан. Обявен е като „Очаквайте скоро“.

#Често задавани въпроси

Защо играта ми остава замръзнала след удар?

Или корутината чака с WaitForSeconds, което не продължава, докато мащабът на времето е нула, или втори удар е запомнил замръзналия мащаб и го е върнал. Ползвайте реално време и запомняйте мащаба на играта само когато замръзване започва от нищото.

Колко дълго трябва да трае замръзването?

Няколко кадъра: между около 0,03 и 0,12 секунди, повече за по-тежките удари. Ограничете общото време за веригите от удари, за да не може продължителната щета да спре играта.

Защо тресенето на камерата спира по време на замръзването?

Върви на мащабирано време. Ползвайте Time.unscaledDeltaTime за тресенето.

Работи ли това с Cinemachine?

Сложете тресенето на собствен обект, който Cinemachine не управлява, или ползвайте собствената система за импулси на Cinemachine за тресенето и часовника за замръзването. Важното е само едно нещо да пише във всяка трансформация.

#Източници

Коментари

Коментари

Напишете първия коментар.

Оставете коментар

Следваща стъпка

pnscripts.com/bg/contact

Пишете на екипа

Попитайте за статия или ни разкажете за проект, който искате да изградим. Отговаряме до един работен ден.

Пишете ни

Семейството PN Scripts

Другите сайтове на PN Scripts

Хостингът, игрите и блогът имат свои сайтове, поддържани от същата компания.

  • pnscripts.com

    PN Scripts

    Софтуерно инженерство

    Уеб и мобилни приложения, API и игри по поръчка, плюс нашите продукти и плъгини с отворен код.

  • games.pnscripts.com

    Игри

    Игри и игрови сървъри

    Домът на игрите на PN Scripts: безплатни браузърни игри, които се играят веднага, а игровите сървъри предстоят.

  • hosting.pnscripts.com

    Хостинг

    Хостинг и инфраструктура

    Споделен хостинг, KVM VPS, самостоятелни сървъри и домейни от същата компания, която изработва проекта ви.

  • blog.pnscripts.com

    Блог

    Статии и бележки

    Практични статии за разработка на софтуер, хостинг, отворен код и игри, писани от хората, които ги създават.

    Вие сте тук