После завершения основной разработки, настало время для самого ответственного этапа — портирования на веб. В этом посте я делюсь переработанным кодом и ключевыми изменениями, которые пришлось внести, чтобы игра корректно работала в браузере.
Что вы увидите в коде:
- Адаптация ввода: Переход с физических клавиатурных кодов на обработку событий браузера (keydown, mousedown).
- Оптимизация рендеринга: Изменения, связанные с Canvas/WebGL, для обеспечения производительности в браузере.
- Управление ресурсами: Как я адаптировал загрузку ассетов под асинхронную модель веб-загрузки.
- Решение проблем с Canvas/Viewport: Код, который гарантирует правильное масштабирование на разных экранах.
Это не просто копия — это оптимизированная версия, готовая к развертыванию. Смотрите листинг и делитесь своими советами по оптимизации для веба!
Скрипт для того чтобы делать скриншоты:
using System.IO; // Необходимо для работы с файловой системой
using UnityEngine;
using System;
public class ScreenshotTaker : MonoBehaviour
{
[Header("Настройки Скриншота")]
public KeyCode screenshotKey = KeyCode.F12;
public string fileNamePrefix = "PingPongShot_";
public int resolutionMultiplier = 1;
void Update()
{
if (Input.GetKeyDown(screenshotKey))
{
TakeScreenshotToDocuments();
}
}
void TakeScreenshotToDocuments()
{
string folderPath = GetDocumentsPath();
// Проверка на случай, если не удалось получить путь к Документам
if (string.IsNullOrEmpty(folderPath))
{
Debug.LogError("Не удалось определить путь к папке Документов. Сохранение отменено.");
return;
}
// Добавляем подпапку для проекта, чтобы не засорять основную папку Документов
string finalFolderPath = Path.Combine(folderPath, "UnityGameScreenshots", Application.productName);
// 1. Создаем папку, если она не существует
if (!Directory.Exists(finalFolderPath))
{
Directory.CreateDirectory(finalFolderPath);
}
// 2. Формируем уникальное имя файла
string timeStamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
string fullPath = Path.Combine(finalFolderPath, fileNamePrefix + timeStamp + ".png");
// 3. Захват кадра
// ScreenCapture.CaptureScreenshot работает асинхронно и сохраняет файл по указанному пути
ScreenCapture.CaptureScreenshot(fullPath, resolutionMultiplier);
// 4. Сообщение пользователю
Debug.Log($"Скриншот сохранен в Документы: {fullPath}");
}
/// <summary>
/// Получает системный путь к папке "Документы" для текущего пользователя.
/// </summary>
private string GetDocumentsPath()
{
// Специальные папки, которые определяют расположение системных папок
try
{
// Для Windows: Environment.GetFolderPath(SpecialFolder.MyDocuments)
// Для macOS/Linux: Path.Combine(Environment.GetFolderPath(SpecialFolder.Personal), "Documents")
// SpecialFolder.Personal обычно соответствует папке пользователя (/Users/UserName/ или C:\Users\UserName)
string personalPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
// На большинстве систем, Documents находится внутри Personal
string documentsPath = Path.Combine(personalPath, "Documents");
// Дополнительная проверка для систем, где Documents может быть просто Personal
if (!Directory.Exists(documentsPath))
{
// Если "Documents" не найдена, используем "Personal" как запасной вариант
return personalPath;
}
return documentsPath;
}
catch (Exception e)
{
Debug.LogError($"Ошибка при получении пути к Документам: {e.Message}");
return null;
}
}
}
Скрипт для изменения разрешения при изменении размера окна браузера:
using UnityEngine;
public class CameraScaler2D : MonoBehaviour
{
private Camera cam;
// Эталонное соотношение сторон (16:9)
private const float ReferenceAspectRatio = 16f / 9f;
// Эталонный размер камеры (чтобы высота мира была 10 единиц)
private const float ReferenceOrthoSize = 5.0f;
void Awake()
{
cam = GetComponent<Camera>();
if (cam.orthographic == false)
{
Debug.LogError("CameraScaler2D предназначен для ортографических камер.");
}
cam.orthographicSize = ReferenceOrthoSize;
}
void Update()
{
AdjustCameraOrthoSize();
}
void AdjustCameraOrthoSize()
{
float currentAspectRatio = (float)Screen.width / (float)Screen.height;
if (currentAspectRatio != ReferenceAspectRatio)
{
if (currentAspectRatio < ReferenceAspectRatio)
{
// Экран уже (Letterboxing): уменьшаем размер, чтобы уместиться по ширине
float newSize = ReferenceOrthoSize * (currentAspectRatio / ReferenceAspectRatio);
cam.orthographicSize = newSize;
}
else // currentAspectRatio > ReferenceAspectRatio
{
// Экран шире (Pillarboxing): оставляем размер 5.0, фиксируем высоту
cam.orthographicSize = ReferenceOrthoSize;
}
}
else
{
cam.orthographicSize = ReferenceOrthoSize;
}
}
}
Изменённый GameManager:
using UnityEngine;
public class GameManager : MonoBehaviour
{
public Pad padPrefab;
public Ball ballPrefab;
public static Vector2 bottomLeft;
public static Vector2 topRight;
private Camera mainCamera;
private int lastScreenWidth = 0;
private int lastScreenHeight = 0;
private void Awake()
{
mainCamera = Camera.main;
if (mainCamera == null)
{
Debug.LogError("GameManager не смог найти Main Camera.");
}
}
private void Start()
{
RecalculateScreenBounds();
if (ballPrefab != null) Instantiate(ballPrefab);
if (padPrefab != null)
{
Pad padVar1 = Instantiate(padPrefab);
Pad padVar2 = Instantiate(padPrefab);
padVar1.Init(true); // Инициализация
padVar2.Init(false);
}
}
private void Update()
{
// Проверка на изменение разрешения
if (Screen.width != lastScreenWidth || Screen.height != lastScreenHeight)
{
RecalculateScreenBounds();
lastScreenWidth = Screen.width;
lastScreenHeight = Screen.height;
// Уведомляем все объекты о новых границах
NotifyObjectsOfBoundaryChange();
}
}
public void RecalculateScreenBounds()
{
if (mainCamera == null) return;
// Расчет границ на основе текущего размера ортографической камеры
bottomLeft = mainCamera.ScreenToWorldPoint(new Vector2(0, 0));
topRight = mainCamera.ScreenToWorldPoint(new Vector2(Screen.width, Screen.height));
}
private void NotifyObjectsOfBoundaryChange()
{
// Находим все пады и заставляем их сбросить свою позицию X к краю
Pad[] pads = FindObjectsOfType<Pad>();
foreach (var pad in pads)
{
pad.SnapToBoundaryX();
}
}
}
Скрипт Pad:
using UnityEngine;
public class Pad : MonoBehaviour
{
[SerializeField] private float speed;
private float height;
private string inpt;
public bool isRightPad;
// Желаемый отступ от края экрана в мировых единицах (например, 1.0f)
private const float DesiredXOffset = 1.0f;
void Start()
{
height = transform.localScale.y;
// Убеждаемся, что позиция установлена корректно при старте
SnapToBoundaryX();
}
public void Init(bool isRight)
{
isRightPad = isRight;
inpt = isRight ? "PadRight" : "PadLeft";
transform.name = inpt;
// Позиционируем сразу, используя актуальные границы
SnapToBoundaryX();
}
void Update()
{
float moveInput = Input.GetAxis(inpt);
float move = moveInput * Time.deltaTime * speed;
// 1. Блокировка ввода, если он пытается нарушить Y-границы
if (moveInput != 0)
{
float potentialY = transform.position.y + move;
if (move < 0 && potentialY < GameManager.bottomLeft.y + height / 2f) move = 0;
else if (move > 0 && potentialY > GameManager.topRight.y - height / 2f) move = 0;
}
transform.Translate(move * Vector2.up);
// 2. Принудительное ограничение X и Y, чтобы исправить дрейф после изменения аспекта
ClampPositionToScreenBounds();
}
// Сбрасывает позицию X на нужный отступ от текущего края
public void SnapToBoundaryX()
{
if (GameManager.topRight == Vector2.zero) return;
float targetX;
if (isRightPad)
{
targetX = GameManager.topRight.x - DesiredXOffset;
}
else
{
targetX = GameManager.bottomLeft.x + DesiredXOffset;
}
// Сбрасываем X, сохраняя текущий Y
transform.position = new Vector3(targetX, transform.position.y, transform.position.z);
}
private void ClampPositionToScreenBounds()
{
float currentY = transform.position.y;
float currentX = transform.position.x;
// Ограничение Y (по границам камеры)
float minY = GameManager.bottomLeft.y + height / 2f;
float maxY = GameManager.topRight.y - height / 2f;
// Ограничение X (по фиксированному отступу)
float minX = GameManager.bottomLeft.x + DesiredXOffset;
float maxX = GameManager.topRight.x - DesiredXOffset;
float newY = Mathf.Clamp(currentY, minY, maxY);
float newX = Mathf.Clamp(currentX, minX, maxX);
if (transform.position.y != newY || transform.position.x != newX)
{
transform.position = new Vector3(newX, newY, transform.position.z);
}
}
}
Скрипт Ball:
using UnityEngine;
public class Ball : MonoBehaviour
{
[SerializeField]
private float speed = 5f; // Установим начальную скорость
private float radius;
private Vector2 direction;
// Ссылка на GameManager для доступа к границам поля
private GameManager gameManager;
private void Awake()
{
// Находим GameManager в сцене
gameManager = FindObjectOfType<GameManager>();
if (gameManager == null)
{
Debug.LogError("GameManager not found in the scene!");
enabled = false; // Отключаем скрипт, если GameManager не найден
return;
}
}
private void Start()
{
// Инициализация направления. Можно сделать случайным.
// direction = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f)).normalized;
// Если хотим стартовать всегда в одном направлении, например, вправо
direction = new Vector2(1f, Random.Range(-0.5f, 0.5f)).normalized; // Начальное направление немного вверх/вниз
radius = transform.localScale.x / 2f; // Убедимся, что делим на 2f для float
}
private void Update()
{
// Двигаем мяч
transform.Translate(direction * speed * Time.deltaTime);
// Проверка столкновения с верхним и нижним краями
if (transform.position.y <GameManager.bottomLeft.y + radius && direction.y < 0)
{
// Если мяч ниже нижнего края и движется вниз, отскочить вверх
direction.y = -direction.y;
// Слегка корректируем позицию, чтобы мяч не застревал
transform.position = new Vector3(transform.position.x, GameManager.bottomLeft.y + radius, transform.position.z);
}
else if (transform.position.y > GameManager.topRight.y - radius && direction.y > 0)
{
// Если мяч выше верхнего края и движется вверх, отскочить вниз
direction.y = -direction.y;
// Слегка корректируем позицию
transform.position = new Vector3(transform.position.x, GameManager.topRight.y - radius, transform.position.z);
}
// Проверка столкновения с левым и правым краями (гол)
if (transform.position.x < GameManager.bottomLeft.x + radius)
{
// Мяч ушел за левую границу -> правый игрок победил
Debug.Log("Правый игрок победил!");
ResetBall(); // Сброс мяча для новой игры
}
else if (transform.position.x > GameManager.topRight.x - radius)
{
// Мяч ушел за правую границу -> левый игрок победил
Debug.Log("Левый игрок победил!");
ResetBall(); // Сброс мяча для новой игры
}
}
private void OnTriggerEnter2D(Collider2D other)
{
// Проверяем, что объект является ракеткой (Pad)
if (other.CompareTag("Pad"))
{
Pad pad = other.GetComponent<Pad>();
if (pad != null)
{
bool isRightPad = pad.isRightPad;
// Логика отражения от ракетки
if (isRightPad && direction.x > 0) // Если это правая ракетка и мяч летит вправо
{
direction.x = -direction.x; // Меняем направление по X
}
else if (!isRightPad && direction.x < 0) // Если это левая ракетка и мяч летит влево
{
direction.x = -direction.x; // Меняем направление по X
}
// Увеличиваем скорость при каждом ударе об ракетку (опционально)
// speed *= 1.05f;
}
}
}
// Функция для сброса мяча в центр после гола
private void ResetBall()
{
transform.position = Vector3.zero; // Возвращаем мяч в центр
// Сбрасываем направление, можно сделать случайным или всегда в одном направлении
direction = new Vector2(1f, Random.Range(-0.5f, 0.5f)).normalized;
speed = 5f; // Сбрасываем скорость к начальной (если она менялась)
}
}
Из-за того что появился новый сценарий для изменения масштаба камеры, приходится пересчитывать координаты объектов. Я выкладываю этот код, может кому то захочется посмотреть как он работает.