Danro/Assets/Scripts/Behaviour/PlayerSkinOverride.cs
Danil Iarosh 716f55c138
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Add player skin override and update game assets
- Добавлен компонент подмены спрайтов игрока перед началом игры.
- Обновлены сцена уровня, префабы, шейдеры, настройки импорта и данные призов.
- Зафиксирован embedded-пакет Unity MCP 9.6.8 и обновлены связанные файлы проекта.
2026-09-22 13:21:18 +03:00

227 lines
8.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.U2D.Animation;
namespace RR.Game2D.Behaviour
{
/// <summary>
/// Заменяет спрайты созданного игрока спрайтами из совместимого prefab-скина.
/// Компонент действует только в собственной сцене и не изменяет prefab игрока.
/// </summary>
[DisallowMultipleComponent]
public sealed class PlayerSkinOverride : MonoBehaviour
{
[Header("Скин игрока")]
[Tooltip("Prefab с той же иерархией SpriteRenderer + SpriteSkin, что и у игрока.")]
[SerializeField] private GameObject skinPrefab;
private GameManager _subscribedManager;
private GameObject _lastAppliedPlayer;
private string _lastReportedProblem;
private void OnEnable()
{
SubscribeToPlayerSpawn();
TryApplyToCurrentPlayer();
}
private void Start()
{
// Повторная попытка нужна, если компонент включился раньше GameManager.
SubscribeToPlayerSpawn();
TryApplyToCurrentPlayer();
}
private void OnDisable()
{
UnsubscribeFromPlayerSpawn();
_lastAppliedPlayer = null;
}
private void SubscribeToPlayerSpawn()
{
GameManager manager = GameManager.Instance;
if (_subscribedManager == manager) return;
UnsubscribeFromPlayerSpawn();
if (manager == null) return;
_subscribedManager = manager;
_subscribedManager.PlayerSpawned += OnPlayerSpawned;
}
private void UnsubscribeFromPlayerSpawn()
{
if (_subscribedManager != null)
_subscribedManager.PlayerSpawned -= OnPlayerSpawned;
_subscribedManager = null;
}
private void TryApplyToCurrentPlayer()
{
GameObject player = _subscribedManager != null ? _subscribedManager.PlayerObj : null;
if (player == null || player.scene != gameObject.scene) return;
ApplySkin(player);
}
private void OnPlayerSpawned(GameObject player)
{
if (player == null || player.scene != gameObject.scene) return;
ApplySkin(player);
}
private void ApplySkin(GameObject player)
{
if (_lastAppliedPlayer == player) return;
if (HasAnotherOverrideInScene())
{
ReportProblem("В сцене найдено несколько активных PlayerSkinOverride. Скин не применён.", true);
return;
}
if (skinPrefab == null)
{
ReportProblem("Prefab скина не назначен. Используется стандартный скин игрока.", false);
return;
}
if (skinPrefab.scene.IsValid())
{
ReportProblem("В поле Skin Prefab должен быть назначен prefab-ассет, а не объект сцены.", true);
return;
}
if (!TryCollectParts(skinPrefab, "скина", true, out Dictionary<string, SpriteRenderer> skinParts))
return;
if (!TryCollectParts(player, "игрока", false, out Dictionary<string, SpriteRenderer> playerParts))
return;
if (skinParts.Count != playerParts.Count)
{
ReportProblem(
$"Структура скина несовместима с игроком: частей в скине — {skinParts.Count}, " +
$"частей у игрока — {playerParts.Count}. Скин не применён.",
true);
return;
}
foreach (KeyValuePair<string, SpriteRenderer> skinPart in skinParts)
{
if (!playerParts.ContainsKey(skinPart.Key))
{
ReportProblem(
$"У игрока отсутствует визуальная часть \"{skinPart.Key}\". Скин не применён.",
true);
return;
}
}
// Замена выполняется только после полной проверки, чтобы не получить частично применённый скин.
foreach (KeyValuePair<string, SpriteRenderer> skinPart in skinParts)
playerParts[skinPart.Key].sprite = skinPart.Value.sprite;
_lastAppliedPlayer = player;
_lastReportedProblem = null;
}
private bool TryCollectParts(
GameObject root,
string rootDescription,
bool requireSprite,
out Dictionary<string, SpriteRenderer> parts)
{
parts = new Dictionary<string, SpriteRenderer>();
SpriteSkin[] spriteSkins = root.GetComponentsInChildren<SpriteSkin>(true);
if (spriteSkins.Length == 0)
{
ReportProblem(
$"В иерархии {rootDescription} \"{root.name}\" не найдено ни одного SpriteSkin.",
true);
return false;
}
foreach (SpriteSkin spriteSkin in spriteSkins)
{
SpriteRenderer spriteRenderer = spriteSkin.GetComponent<SpriteRenderer>();
string relativePath = GetRelativePath(root.transform, spriteSkin.transform);
if (spriteRenderer == null)
{
ReportProblem(
$"На части {rootDescription} \"{relativePath}\" отсутствует SpriteRenderer.",
true);
return false;
}
if (requireSprite && spriteRenderer.sprite == null)
{
ReportProblem(
$"На части {rootDescription} \"{relativePath}\" не назначен Sprite.",
true);
return false;
}
if (!parts.TryAdd(relativePath, spriteRenderer))
{
ReportProblem(
$"В иерархии {rootDescription} повторяется путь \"{relativePath}\".",
true);
return false;
}
}
return true;
}
private bool HasAnotherOverrideInScene()
{
PlayerSkinOverride[] overrides = FindObjectsByType<PlayerSkinOverride>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None);
foreach (PlayerSkinOverride candidate in overrides)
{
if (candidate != this && candidate.gameObject.scene == gameObject.scene)
return true;
}
return false;
}
private static string GetRelativePath(Transform root, Transform child)
{
if (root == child) return string.Empty;
Stack<string> names = new();
Transform current = child;
while (current != null && current != root)
{
names.Push(current.name);
current = current.parent;
}
return string.Join("/", names);
}
private void ReportProblem(string message, bool isError)
{
if (_lastReportedProblem == message) return;
_lastReportedProblem = message;
string formattedMessage = $"[{nameof(PlayerSkinOverride)}] {message}";
if (isError)
Debug.LogError(formattedMessage, this);
else
Debug.LogWarning(formattedMessage, this);
}
}
}