Menu
CoddyTech

Герой

Часть раздела Архитектура игр в Unity путешествия по Unity на Coddy. Урок 41 из 50.

В этой главе мы создадим Arena Survivor — последнюю игру нашего путешествия. Волны врагов заполняют огороженную арену, и ты отбиваешься от них мечом: одни враги идут прямо на тебя, другие — сферы — выжидают, а затем бросаются в атаку. После победы над волной остается усиление, HUD показывает здоровье и номер волны, а когда здоровье заканчивается, забег завершается и лучший результат сохраняется. В игре используются инструменты из этого раздела там, где они подходят: абстрактный Enemy с двумя разновидностями, IDamageable для всего, что можно повредить, волны в виде ресурса ScriptableObject, подбор предметов через интерфейс, HUD, который только прослушивает события C#, и сохранение в JSON. Все двенадцать скриптов уже есть в проекте. Пока код есть только у Hero, и каждый урок добавляет что-то новое.

Меч героя поражает то, что находится перед ним, поэтому для взмаха нужно проверить, находится ли цель «впереди». Vector3.Dot(a, b) перемножает соответствующие части двух векторов и складывает результаты: a.x * b.x + a.y * b.y + a.z * b.z. Для двух направлений длиной 1 это косинус угла между ними: 1, если они указывают в одну сторону, 0.866 при 30 градусах, 0.707 при 45, 0 при прямом угле и -1, если они указывают в противоположные стороны. Поэтому условие «в пределах 30 градусов от направления, куда я смотрю» превращается в «скалярное произведение не меньше 0.866» — угол вычислять не нужно. Этот робот-часовой поднимает тревогу, если игрок находится в пределах 10 единиц и попадает в его 30-градусное поле зрения:

public Transform player;
public float range = 10f;

void Update()
{
    Vector3 toPlayer = player.position - transform.position;
    toPlayer.y = 0f;
    bool near = toPlayer.magnitude <= range;
    bool inView = Vector3.Dot(transform.forward, toPlayer.normalized) >= 0.866f;
    if (near && inView)
    {
        Debug.Log("Alarm!");
    }
}

Оба направления должны иметь длину 1. У transform.forward она всегда такая, но toPlayer имеет длину, равную расстоянию, поэтому сначала его нормализуют; иначе игрок, находящийся вдвое дальше, дал бы вдвое большее скалярное произведение. Перед этим значение y устанавливается в 0, поэтому игрок на верхнем переходе учитывается по тому, где он находится на карте, а не по высоте. Знак позволяет отличить переднюю сторону от задней: если скалярное произведение больше 0, цель находится где-то в передней половине, если меньше 0 — где-то позади.

challenge icon

Задание

Средне

Начни Arena Survivor с мечом героя. Перемещение реализовано так: действие Move перемещает героя с помощью SimpleMove и поворачивает его в сторону движения. Вокруг него стоят пять тренировочных манекенов с тегом Enemy. В Inspector для Hero заданы reach (2) и swingCooldown (0.5), а также slash, который появляется во время взмаха.

  • Пробел или щелчок левой кнопкой мыши запускает взмах не чаще одного раза в swingCooldown секунд: нажатие до окончания перезарядки ничего не делает. Считывай нажатия клавиш с помощью Keyboard.current и Mouse.current и вызывай Swing().
  • В Swing() после появления slash уничтожай каждый объект с тегом Enemy, который находится в пределах reach и перед героем. В пределах досягаемости находится любой коллайдер, который Physics.OverlapSphere обнаруживает вокруг героя в радиусе reach. Перед героем — значит, что скалярное произведение направления взгляда героя и направления на объект по земле не меньше 0.5: вплоть до 60 градусов в любую сторону.

Оставь остальные скрипты без изменений. Нажми Play, подойди к манекенам и нанеси удар, затем нажми Check.

Попробуйте сами

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

public class Hero : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    [SerializeField] private float reach = 2f;
    [SerializeField] private float swingCooldown = 0.5f;
    [SerializeField] private GameObject slash;

    private CharacterController controller;
    private InputAction moveAction;

    void Start()
    {
        controller = GetComponent<CharacterController>();
        moveAction = InputSystem.actions.FindAction("Move");
    }

    void Update()
    {
        Vector2 input = moveAction.ReadValue<Vector2>();
        Vector3 move = new Vector3(input.x, 0f, input.y);
        controller.SimpleMove(move * speed);
        if (move != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(move);
        }

        // Нажатие Space или левой кнопки мыши выполняет удар не чаще одного раза в swingCooldown секунд
    }

    void Swing()
    {
        StartCoroutine(ShowSlash());
        // уничтожить все объекты с тегом Enemy в пределах reach перед героем
    }

    IEnumerator ShowSlash()
    {
        slash.SetActive(true);
        yield return new WaitForSeconds(0.1f);
        slash.SetActive(false);
    }
}
quiz iconПроверьте себя

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

Все уроки раздела Архитектура игр в Unity