Найти в Дзене
KNL Games

Листинг кода 2D игры переделаный под WebGL

После завершения основной разработки, настало время для самого ответственного этапа — портирования на веб. В этом посте я делюсь переработанным кодом и ключевыми изменениями, которые пришлось внести, чтобы игра корректно работала в браузере. Что вы увидите в коде: Это не просто копия — это оптимизированная версия, готовая к развертыванию. Смотрите листинг и делитесь своими советами по оптимизации для веба! Скрипт для того чтобы делать скриншоты: 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.IsNull

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

Что вы увидите в коде:

  • Адаптация ввода: Переход с физических клавиатурных кодов на обработку событий браузера (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; // Сбрасываем скорость к начальной (если она менялась)

}

}

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