Тренировочная арена — это не «домашняя CS2», а песочница, в которой вы сами задаёте тайминги появления целей, физику отскоков и условия поражения. Я перешёл к Unity именно от серверной инфраструктуры: месяцами настраивал демо-записи на Synology, писал плагины для анализа матчей, подкручивал hitbox’ы и тикрейт. Стало очевидно, что кастомный тренажёр — естественное продолжение этого инженерного подхода. Мы берём под контроль не сервер, а саму среду, в которой тренируем реакцию, и делаем первый прототип, который можно запустить уже сегодня.
Ниже мы пройдём путь от чистой сцены до рабочего MVP: пол, стены, цели, движение, стрельба, счёт и таймер. Никаких многопользовательских иллюзий — только одиночный тренажёр, который вы будете «собирать» под конкретную механику, будь то разминка аима перед матчем или отработка выхода из угла.
Почему киберспортсмену нужен свой тренажер в Unity
Готовые тренажёры вроде Aim Lab или встроенных режимов в игре удобны стартом, но жёстко ограничены сценариями. Добавить специфичный паттерн движения цели, изменить задержку перед появлением, эмулировать конкретный угол с карты Dust II — всё это либо невозможно, либо требует ожидания обновлений от разработчиков.
Своя арена на Unity снимает эти ограничения:
- Контроль параметров. Скорость цели, размер хитбокса, частота спавна и физика полёта снаряда меняются прямо в редакторе за пару кликов — почти как правка конфига сервера.
- Точная адаптация под задачу. Отрабатываете «пик» из-за ящика? Строите тот самый ящик и замеряете дистанцию. Хотите усложнить — добавляете вертикальное смещение или задержку между появлениями.
- Инженерное понимание игры. Настраивая коллизии в Unity, вы лучше начинаете чувствовать, почему на сервере иногда «не регистрируются» попадания. Анализ демок потом становится глубже: вы сопоставляете свои настройки физики с тем, что отдаёт серверный движок.
- Экономия времени. Не нужно ждать ночных патчей или просить моддеров. Изменили сцену, нажали Play — и через пять секунд тестируете новый сценарий.
Главный принцип: мы делаем именно тренажёр, а не полноценный шутер. Сетевой код, графика, матчмейкинг — оставьте для больших проектов. Сейчас важна чистая механика, быстрая итерация и стабильный FPS.
Шаг 1. Установка и настройка Unity
Этот этап похож на развёртывание сервера: неправильно выбранная версия или забытый компонент могут превратить отладку в ад. Поэтому берём стабильные сборки, как при установке DSM на Synology — только LTS.
Что нужно скачать
- Unity Hub — центр управления версиями и проектами. Качайте строго с unity.com.
- Unity Editor. В Hub выбирайте LTS-релиз: например, Unity 2022.3 LTS или Unity 6000.0.x. Экспериментальные версии иногда ломают коллизии или заставляют переписывать скрипты — как бета-патчи, портящие демки.
- Компоненты при установке:
- Microsoft Visual Studio Community (или Rider) — для C#.
- Documentation — офлайн-справка выручает, когда забыл сигнатуру метода.
- Android Build Support опционален; для ПК-арены не нужен.
Создание первого проекта
- В Unity Hub нажмите New Project.
- Выберите шаблон 3D Core (URP улучшает производительность, но для старта Core проще).
- Назовите проект, например
CS2_Arena_Trainer. - Укажите путь на диске с запасом места — Unity генерирует много временных файлов, как серверные логи с включённым verbose.
- Нажмите Create.
После загрузки вы увидите основные панели: Scene View (трёхмерный редактор), Game View (окно игрока), Hierarchy (дерево объектов), Project (файлы) и Inspector (свойства). Интерфейс можно подогнать под себя, как расположение панелей в админке SourceMod.
Шаг 2. Создание базовой арены: стены, платформы, цели
Графика на этом этапе вторична — важна точная геометрия. Все объекты создаются через меню GameObject.
1. Создание пола и стен
- Пол:
GameObject → 3D Object → Cube.- В Inspector задайте Scale:
X: 20, Y: 1, Z: 20— просторная площадка. - Position:
Y: -0.5, чтобы верхняя грань была на нуле. - Переименуйте в
Floor.
- В Inspector задайте Scale:
- Стены: Создайте четыре куба.
- Длинные стены: Scale
X: 20, Y: 10, Z: 1; короткие:X: 1, Y: 10, Z: 20. - Разместите по краям пола, образуя замкнутую коробку.
- Названия типа
Wall_Left.
- Длинные стены: Scale
Пересечение геометрии здесь критично — если коллайдеры накладываются некорректно, игрок может застрять или провалиться. Позже мы выставим физику, как при настройке clip-плоскостей на серверных картах.
2. Добавление платформ и укрытий
Для тренировки перемещения (stratedge, strafejump) нужны препятствия. Создайте кубы размером около 4×2×4, поднимите по Y (например, Y: 1) и расставьте в подобии ключевых точек карты: угол B на Dust II, площадка A. Цель — получить знакомый пространственный ритм.
3. Создание целей (Target)
Цель — это объект, который мы будем «поражать».
- Создайте сферу (
Sphere), назовитеTarget. - Размер:
X: 0.5, Y: 0.5, Z: 0.5— примерно как голова на средней дистанции. - Материал: в окне Project
Right Click → Create → Material, назовитеTarget_Mat, выберите ярко-красный в Albedo. Перетащите материал на объект. - Разместите 5-10 целей на разных высотах и дистанциях, имитируя разброс противников.
Для удобства позже мы превратим цель в префаб, чтобы спавнить её через код — чисто серверный подход к сущностям.
Шаг 3. Настройка физики и коллизий
Без правильной физики игрок провалится сквозь пол, а пули не заметят цели. Здесь принцип схож с настройкой hitbox’ов в плагинах: коллизии должны быть точными и с минимальными накладными расходами.
1. Добавление компонентов физики
- Для пола и стен — статические коллайдеры. Выделите объект,
Add Component → Physics → Box Collider. Убедитесь, что Is Trigger выключен (триггеры нужны только для событий без физического взаимодействия). - Для игрока — используем Character Controller (подходит для FPS-управления, не даёт «катиться» как мячу).
- Создайте пустой объект
Player(GameObject → Empty). Add Component → Physics → Character Controller.- Настройки: Center:
Y: 1; Height:2; Radius:0.4; Slope Limit:45; Step Offset:0.3.
- Создайте пустой объект
2. Камера и управление
Игрок — это связка «пустой объект + камера». Перетащите Main Camera из Hierarchy внутрь объекта Player, затем обнулите её локальную позицию. Теперь при движении Player камера перемещается синхронно, как pov-сущность в демке.
Шаг 4. Скриптинг: движение игрока и стрельба
Код должен быть читаемым и отказоустойчивым — как скрипты SourceMod, где лишняя итерация в цикле может уронить тикрейт. Пишем на C#.
1. Создание скрипта движения
В окне Project: Right Click → Create → C# Script, назовите PlayerMovement. Откройте в среде разработки и вставьте:
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public CharacterController controller;
public float speed = 12f;
public float mouseSensitivity = 2f;
private float xRotation = 0f;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
void Update()
{
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
Vector3 move = transform.right * x + transform.forward * z;
controller.Move(move * speed * Time.deltaTime);
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -90f, 90f);
Camera.main.transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
transform.Rotate(Vector3.up * mouseX);
}
}
Сохраните, вернитесь в Unity, выделите Player и добавьте компонент PlayerMovement. В полях скрипта увидите скорость и чувствительность — их можно менять на лету.
2. Скрипт стрельбы
Новый скрипт PlayerShooting:
using UnityEngine;
public class PlayerShooting : MonoBehaviour
{
public GameObject bulletPrefab;
public Transform firePoint;
public float bulletForce = 20f;
void Start()
{
if (firePoint == null)
firePoint = Camera.main.transform;
}
void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}
void Shoot()
{
GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
Rigidbody rb = bullet.GetComponent<Rigidbody>();
if (rb != null)
rb.AddForce(firePoint.forward * bulletForce, ForceMode.Impulse);
bullet.tag = "Bullet";
Destroy(bullet, 2.0f);
}
}
3. Создание префаба пули
- Создайте Sphere (или Capsule), назовите
Bullet. Add Component → Rigidbody. Mass: 1, Drag: 0, Angular Drag: 0.- Уменьшите Scale:
0.1, 0.1, 0.1, задайте белый материал. - Перетащите объект из Hierarchy в окно Project — это сохранит префаб.
- В скрипте
PlayerShooting(на объектеPlayer) в поле Bullet Prefab перетащите созданный префаб. - Убедитесь, что в методе
Shootесть строкаbullet.tag = "Bullet";.
Если пуля не реагирует, проверьте наличие Rigidbody и корректность тега — отсутствие компонента равносильно выключенной физике на сервере.
Шаг 5. Логика целей: реакция на удар и спавн
Цели должны знать, что в них попали, а спавнер — поддерживать нужную интенсивность тренировки. Это аналог серверного менеджера раундов, только для одного игрока.
1. Скрипт цели (Target)
using UnityEngine;
public class TargetLogic : MonoBehaviour
{
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Bullet"))
{
// Если есть ScoreManager, увеличиваем счёт
ScoreManager scoreManager = FindObjectOfType<ScoreManager>();
if (scoreManager != null)
scoreManager.AddScore(1);
Destroy(gameObject);
}
}
}
Добавьте скрипт на объект Target (или на префаб цели). У пули установите тег Bullet через Inspector (создайте тег при необходимости).
2. Спавнер целей (Target Spawner)
Скрипт на пустом объекте Spawner:
using UnityEngine;
public class TargetSpawner : MonoBehaviour
{
public GameObject targetPrefab;
public float spawnInterval = 2f;
public int maxTargets = 10;
public Vector3 spawnAreaMin = new Vector3(-8, 1, -8);
public Vector3 spawnAreaMax = new Vector3(8, 3, 8);
private float timer;
private int currentTargets;
void Start()
{
timer = spawnInterval;
}
void Update()
{
timer -= Time.deltaTime;
if (timer <= 0f && currentTargets < maxTargets)
{
SpawnTarget();
timer = spawnInterval;
}
}
void SpawnTarget()
{
Vector3 pos = new Vector3(
Random.Range(spawnAreaMin.x, spawnAreaMax.x),
Random.Range(spawnAreaMin.y, spawnAreaMax.y),
Random.Range(spawnAreaMin.z, spawnAreaMax.z));
Instantiate(targetPrefab, pos, Quaternion.identity);
currentTargets++;
}
public void TargetDestroyed()
{
currentTargets--;
}
}
Присоедините скрипт к Spawner, в Target Prefab перетащите префаб цели. В TargetLogic в момент удаления вызывайте spawner.TargetDestroyed() (через FindObjectOfType<TargetSpawner>()).
Шаг 6. Оптимизация и тестирование
Запуск и отладка — как мониторинг серверных метрик: смотрим FPS, отслеживаем «лаги» и устраняем узкие места.
1. Запуск в режиме игры
Нажмите Play. Проверьте:
- Игрок двигается (WASD), камера поворачивается плавно.
- При нажатии LMB летит пуля, физика корректна.
- Цель уничтожается при попадании.
- Спавнер добавляет новые цели с заданным интервалом.
- Счётчик FPS (отображается в окне Game) держится выше 60.
Если что-то не работает — возвращаемся к компонентам и коллизиям, как при диагностике тикрейта: отключаем лишнее, проверяем по одному.
2. Оптимизация
Когда FPS проседает, действуем как с перегруженным сервером: снижаем число динамических объектов. Уменьшите maxTargets, используйте простые фигуры, избегайте множественных коллайдеров на одном объекте. Пол и стены оставляйте со статическими коллайдерами (флажок Static в Inspector), а для целей — динамические, но не более одного Box/Sphere Collider. Позже можно внедрить Object Pool для пуль, но на старте достаточно уничтожать их через Destroy.
3. Сохранение и запуск
Чтобы получить исполняемый файл:
- File → Build Settings.
- Выберите PC, Mac & Linux Standalone.
- Нажмите Add Open Scenes (ваша сцена должна быть единственной).
- Build — укажите папку.
Готовый .exe запускается без редактора. Сохраните его на NAS (Synology) как резервную копию — версионирование тренировочных сборок помогает отслеживать, как менялась сложность сценариев.
Шаг 7. Расширение функционала: счетчик очков и таймер
Метрики превращают арену в аналитический инструмент. Как и на сервере, где статистика раундов необходима, здесь мы добавляем простой UI.
1. Счетчик очков
Скрипт ScoreManager на пустом объекте GameManager:
using UnityEngine;
using UnityEngine.UI;
public class ScoreManager : MonoBehaviour
{
public Text scoreText;
private int score;
void Start()
{
UpdateUI();
}
public void AddScore(int amount)
{
score += amount;
UpdateUI();
}
void UpdateUI()
{
if (scoreText != null)
scoreText.text = "Score: " + score;
}
}
В Hierarchy создайте UI → Text, назовите ScoreText. Перетащите его в поле Score Text скрипта ScoreManager. В TargetLogic, в OnCollisionEnter, уже есть вызов scoreManager.AddScore(1) — он должен найти менеджер через FindObjectOfType.
2. Таймер
Скрипт TimerManager (на том же GameManager):
using UnityEngine;
using UnityEngine.UI;
public class TimerManager : MonoBehaviour
{
public Text timerText;
public float roundTime = 60f;
private float currentTime;
void Start()
{
currentTime = roundTime;
}
void Update()
{
if (currentTime > 0)
{
currentTime -= Time.deltaTime;
UpdateUI();
if (currentTime <= 0)
{
currentTime = 0;
// Здесь можно остановить игру
Debug.Log("Time's up!");
}
}
}
void UpdateUI()
{
if (timerText != null)
timerText.text = "Time: " + Mathf.CeilToInt(currentTime);
}
}
Создайте ещё один UI Text для таймера и перетащите в поле скрипта. Теперь у тренировки есть временной лимит, как у раунда на сервере.
Типовые ошибки и как их избежать
| Ошибка | Причина | Решение |
|---|---|---|
| Игрок проваливается сквозь пол | Нет коллайдера или Character Controller отсутствует | Добавьте Box Collider на пол, Character Controller на игрока. Проверьте, что Is Trigger выключен. |
| Пуля не летит | Нет Rigidbody на префабе пули, либо не назначен Bullet Prefab в скрипте | Добавьте Rigidbody, Mass > 0. Убедитесь, что префаб перетащен в поле скрипта PlayerShooting. |
| Цель не исчезает | Не совпадает тег пули или отсутствует Collider на цели | Убедитесь, что пуля имеет тег Bullet, цель имеет коллайдер и скрипт TargetLogic. В коде проверяется CompareTag. |
| FPS падает ниже 60 | Слишком много динамических целей или сложная геометрия | Уменьшите maxTargets, используйте примитивы, отключите лишние Rigidbody у статических объектов. |
| Камера беспорядочно вращается | Не ограничен угол поворота по вертикали | В PlayerMovement переменная xRotation уже ограничена Mathf.Clamp. Проверьте, что код выполняется. |
Если цель не реагирует на попадание, но коллайдер есть — вероятно, вы забыли добавить тег пуле или не вызвали bullet.tag = "Bullet" в Shoot(). Лишний раз проверьте, что тег именно строкой, без опечаток.
Чек-лист: что должно быть в вашей арене
- Физика: игрок ходит, не проваливается, может подниматься по ступенькам.
- Стрельба: пуля вылетает из камеры, обладает скоростью, исчезает через 2 секунды.
- Цели: автоматически спавнятся, при попадании уничтожаются и уменьшают счётчик активных целей.
- Счёт: очки увеличиваются при каждом попадании.
- Таймер: время тренировки ограничено, отображается на экране.
- Производительность: FPS стабильно 60+ в режиме Play.
- Сборка: проект успешно билдится в отдельный .exe.
Если все пункты выполнены — у вас готовый тренажёр. Дальше можно итерировать, как с серверными конфигами: менять зону спавна, добавлять движущиеся цели, записывать демки тренировок для последующего анализа.
FAQ: Ответы на частые вопросы
1. Можно ли сделать эту арену многопользовательской?
Технически да, но это тянет за собой Networked Physics, синхронизацию объектов, матчмейкинг. Для тренировочного инструмента это избыточно. Одиночная арена с чёткими метриками даёт гораздо больше пользы. Если хочется тренироваться с напарником, используйте стандартные серверы CS2/Dota 2 — Unity здесь закрывает именно индивидуальные сценарии.
2. Как добавить звуки стрельбы и ударов?
Добавьте компонент AudioSource на объект игрока и на префаб пули или цели. В скрипт PlayerShooting в методе Shoot вызывайте GetComponent<AudioSource>().PlayOneShot(shootSound);, предварительно назначив аудиоклип в инспекторе. Для попадания — аналогично в TargetLogic. Файлы .wav или .mp3 должны быть импортированы в проект.
3. Почему цели появляются в случайных местах, а не в углах?
В TargetSpawner используется Random.Range по заданным границам. Если нужны фиксированные позиции — замените на массив точек и выбирайте из него. Например, создайте публичный массив Transform[] spawnPoints и в SpawnTarget выбирайте spawnPoints[Random.Range(0, spawnPoints.Length)].
4. Как изменить скорость цели, если я сделаю её движущейся?
Добавьте цели компонент Rigidbody и в её скрипте в Update задавайте rb.velocity = transform.forward * moveSpeed; или используйте Transform.Translate. Скорость выставляйте через публичную переменную, чтобы регулировать из инспектора.
5. Можно ли использовать эту арену для CS2?
Арена не эмулирует физику Source 2, сетевую задержку или особенности хитбоксов конкретной игры. Это тренажёр реакции и точности, нацеленный на изолированную механику аима. Для тактической подготовки он не подходит, но как разминка перед матчем — отличный инструмент. Я часто совмещаю: 10 минут на арене, затем анализ демок с боевого сервера.
Заключение
Собранный прототип — это не просто «ещё один тренажёр». Это песочница, где вы сами управляете каждым параметром тренировки. Как инженер, я ценю возможность быстро менять условия, не дожидаясь обновлений, и версионировать сборки на Synology, чтобы отслеживать прогресс. Минимально жизнеспособный продукт готов: физика работает, пули попадают, счёт растёт, таймер тикает.
Что дальше:
- Усложняйте механику: движущиеся по паттернам цели, препятствия с изменяемой высотой.
- Улучшайте визуал: скачайте бесплатные ассеты, замените кубы на простые модели.
- Внедряйте аналитику: записывайте свои тренировки (через встроенные средства или OBS) и сопоставляйте с результатами из демок реальных матчей.
- Пробуйте сетевые элементы только после того, как одиночный функционал станет стабильным.
Главный принцип — маленькие шаги. Сделали одну цель, одну пулю, один счётчик. Убедились в работе — добавляете следующий слой. Так вы не потеряете мотивацию и получите именно тот инструмент, который заточен под ваш скилл. А Synology в этом цикле станет хранилищем версий и архивом тренировочных сборок — как для демок, так и для будущих проектов.
