Статьи

Дым и зеркала хорошего обратного отсчета, часть 2

Конечный продукт
Что вы будете создавать

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

Это будет продолжаться с первой части , поэтому мы начнем с номера семь.

Готов? Пошли!

Это простой трюк, который даже не требует от вас лжи игроку. Вы просто говорите «когда таймер заканчивается» и показываете тикающие часы. То, что вы не упоминаете, это единица, которая показана.

Если вы показываете «10», игрок интуитивно понимает, что это 10 секунд, но число может уменьшиться медленнее, чем секунды. Например, если вы измените значение с множителем 0,5 , оно истечет через 20 секунд вместо 10 .

Вы можете посмотреть, как это работает в моей игре Ludum-Dare « Каждые десять секунд котенок тонет» .

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

Тема джема требовала использования 10 секунд , но 10 фактических секунд — слишком малое количество времени для достижения чего-то значимого.

Это также работает лучше, если единицы времени не упоминаются, и игроку просто дают приблизительное представление о «пока это не произойдет».

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

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

Это можно увидеть на одном уровне Starfox 64, где игрок должен защищать позицию от приближающегося гигантского корабля.

Миникарта в Starfox 64
Миникарта в Starfox 64 . Атакующий материнский корабль (черный круг) движется к основанию посередине, давая игроку грубые тикающие часы того, сколько осталось времени, чтобы его уничтожить

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

Таймер по сути адаптируется на лету, процесс которого скрыт за дымом и зеркалами.

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

Как только вы это сделаете, вы также можете адаптировать частоту по истечении определенного времени. Если сигнал звучит каждые пять секунд, в течение последних 30 секунд таймера игрок будет уведомлен без необходимости смотреть на число и мысленно вычислять, сколько осталось.

Точно так же это помогает, если персонаж комментирует прогресс. Кто-то скажет: «Мы уже на полпути!» дает вам хорошо структурированную часть информации, которую гораздо легче понять, чем анализировать ее из показаний.

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

В дополнение к цвету и звуку, это сделает его намного более сочным.

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

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

Если у вас есть положительный таймер, это забавный элемент, просто позволить ему продолжать работать и использовать этот элемент в качестве рекорда. Игра Devil Daggers делает аналогичную вещь, где основной целью является «Выжить 500 секунд».

Геймплей от Devil Daggers
Геймплей от Devil Daggers

Как только правила таймера установлены, они должны оставаться примерно согласованными, причем чем больше свободы, тем менее точным является таймер. Главный вопрос должен звучать так: «Как это скрытое правило приносит пользу опыту?»

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

Это будет продолжено с использованием базы кода из предыдущего урока и будет основано на ней. Если вы еще не проверили это, сделайте это сейчас! Вы также можете скачать готовый исходный код в правом верхнем углу этой статьи.

Когда мы последний раз оставляли наш таймер, это выглядело так:

Наше время, когда мы последний раз покидали его

Теперь новые дополнения сделают его более интересным. Мы продолжим со countdown.cs-script , который должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using System.Collections;
 
public class Countdown : MonoBehaviour {
 
    float timer = 60f;
    public AudioClip soundBlip;
     
    void Update (){
        if (timer > 0f){
            timer -= Time.deltaTime;
        } else {
            timer = 0f;
        }
         
        if (timer < 10f) {
            GetComponent<TextMesh>().color = Color.red;
        } else if (timer < 20f) {
            GetComponent<TextMesh>().color = Color.yellow;
        }
         
        GetComponent<TextMesh>().text = timer.ToString(«F2»);
    }
}

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

Во-первых, нам нужен приятный звуковой эффект. Вы можете найти аудиофайл в исходных файлах или создать хороший, используя бесплатный инструмент BFXR . Как только у вас есть, скопируйте его в папку активов.

Добавьте компонент AudioSource к вашему объекту обратного отсчета через Component> Audio> AudioSource . Добавьте эту переменную в скрипт обратного отсчета:

1
public AudioClip soundBlip;

Вам также необходимо присвоить звуковой файл переменной soundBlip :

Выберите Blip01

И добавьте этот блок в функцию Update :

1
2
3
if (Mathf.Round(timer * 100) / 100 % 1f == 0) {
    GetComponent<AudioSource>().PlayOneShot(soundBlip);
}

Это проверит, находится ли таймер на отметке полной секунды, и если это так, воспроизведите звуковой файл.

Фактическая работа этого кода немного сложнее. Сначала он округляет таймер с плавающей запятой до двух десятичных знаков, затем делит его на 1 и проверяет, есть ли остаток. Если остаток равен нулю, это означает, что таймер достиг полной секунды, и можно воспроизвести звуковой сигнал.

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

Это добавит множитель, который уменьшит или ускорит скорость тикающих часов. Добавьте эту переменную в начале:

1
float multiplier = 0.6f;

Теперь найдите строку, которая уменьшает таймер — вот эта:

1
timer -= Time.deltaTime;

И адаптируйте его так, чтобы он выглядел так:

1
timer -= Time.deltaTime * multiplier;

Если множитель ниже 1,0, таймер теперь будет работать медленнее, а если выше 1,0, он будет работать быстрее. Установка на 1.0 ничего не даст.

Попробуйте поэкспериментировать, чтобы найти хорошую ценность! Я чувствую, что чуть более низкая скорость, например 0,85f , заставит игрока больше ощущать тикающие часы.

Теперь, когда у нас есть компонент множителя, мы можем изменить его во время игры!

Перейти к изменяющему цвет блоку кода:

1
2
3
4
5
if (timer < 10f) {
    GetComponent<TextMesh>().color = Color.red;
} else if (timer < 20f) {
    GetComponent<TextMesh>().color = Color.yellow;
}

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

1
2
3
4
5
6
7
8
9
if (timer < 10f) {
    GetComponent<TextMesh>().color = Color.red;
    multiplier = 0.6f;
} else if (timer < 20f) {
    GetComponent<TextMesh>().color = Color.yellow;
    multiplier = 0.8f;
} else {
    multiplier = 1.0f;
}

Когда таймер станет желтым через 20 секунд, он будет работать со скоростью 80%. Как только он станет красным, он снизится до 60% от обычной скорости. В противном случае скорость будет установлена ​​на 100%.

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

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

1
2
3
4
5
6
7
8
9
private bool isBlinking = false;
private IEnumerator Blink() {
    isBlinking = true;
    float startScale = transform.localScale.x;
    transform.localScale = Vector3.one * startScale * 1.4f;
    yield return new WaitForSeconds(0.3f);
    transform.localScale = Vector3.one * startScale;
    isBlinking = false;
}

Это IEnumerator , который является типом функции, которая может содержать команды ожидания. Нам нужна переменная isBlinking чтобы мы не вызывали ее несколько раз.

После запуска он увеличит размер объекта в 1,4 раза, подождет 0,3 секунды, а затем снова уменьшит его до исходного размера.

Мы называем это с помощью этого специального кода:

1
2
3
4
5
6
7
8
9
if (Mathf.Round(timer * 100) / 100 % 1f == 0) {
    GetComponent<AudioSource>().PlayOneShot(soundBlip);
 
    if (timer < 10f) {
        if(!isBlinking) {
            StartCoroutine(Blink());
        }
    }
}

IEnumerator должен быть инициирован путем вызова его через StartCoroutine , иначе он не будет работать.

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

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

Сначала нам понадобится еще одна функция IEnumerator :

1
2
3
4
5
6
7
8
9
private bool isZeroBlinking = false;
private IEnumerator ZeroBlink() {
    isZeroBlinking = true;
    GetComponent<Renderer>().enabled = false;
    yield return new WaitForSeconds(1.5f);
    GetComponent<Renderer>().enabled = true;
    yield return new WaitForSeconds(1.5f);
    isZeroBlinking = false;
}

Это включит и выключит таймер с интервалом в 1,5 секунды. Запустите его в блоке, который уже проверяет, равен ли таймер нулю.

1
2
3
4
5
6
7
8
if (timer > 0f){
    timer -= Time.deltaTime * multiplier;
} else {
    timer = 0f;
    if(!isZeroBlinking) {
        StartCoroutine(ZeroBlink());
    }
}

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

Адаптируйте условия в блоке, чтобы проверить, прошла ли секунда, а также чтобы проверить, является ли текущее время больше нуля:

1
2
3
4
5
6
7
8
9
if (Mathf.Round(timer * 100) / 100 % 1f == 0 && timer > 0f) {
    GetComponent<AudioSource>().PlayOneShot(soundBlip);
     
    if (timer < 10f && timer > 0f) {
        if(!isBlinking) {
            StartCoroutine(Blink());
        }
    }
}

Это обеспечит регулярное мигание и нулевое мигание, не мешая друг другу.

Весь скрипт countdown должен выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using UnityEngine;
using System.Collections;
 
public class Countdown : MonoBehaviour {
     
    float timer = 5f;
    float multiplier = 0.6f;
    public AudioClip soundBlip;
     
    void Update (){
        if (timer > 0f){
            timer -= Time.deltaTime * multiplier;
        } else {
            timer = 0f;
            if(!isZeroBlinking) {
                StartCoroutine(ZeroBlink());
            }
        }
     
        if (timer < 10f) {
            GetComponent<TextMesh>().color = Color.red;
            multiplier = 0.6f;
        } else if (timer < 20f) {
            GetComponent<TextMesh>().color = Color.yellow;
            multiplier = 0.8f;
        } else {
            multiplier = 1.0f;
        }
     
        if (Mathf.Round(timer * 100) / 100 % 1f == 0 && timer > 0f) {
            GetComponent<AudioSource>().PlayOneShot(soundBlip);
             
            if (timer < 10f && timer > 0f) {
                if(!isBlinking) {
                    StartCoroutine(Blink());
                }
            }
        }
     
        GetComponent<TextMesh>().text = timer.ToString(«F2»);
    }
     
    private bool isBlinking = false;
    private IEnumerator Blink() {
        isBlinking = true;
        float startScale = transform.localScale.x;
        transform.localScale = Vector3.one * startScale * 1.4f;
        yield return new WaitForSeconds(0.3f);
        transform.localScale = Vector3.one * startScale;
        isBlinking = false;
    }
     
    private bool isZeroBlinking = false;
    private IEnumerator ZeroBlink() {
        isZeroBlinking = true;
        GetComponent<Renderer>().enabled = false;
        yield return new WaitForSeconds(1.5f);
        GetComponent<Renderer>().enabled = true;
        yield return new WaitForSeconds(1.5f);
        isZeroBlinking = false;
    }
}

Это сделает работу. Конечно, вы можете сделать его более элегантным и объединить команды в более эффективный код.

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

А теперь иди и вставь это в игру!