Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлен компонент подмены спрайтов игрока перед началом игры. - Обновлены сцена уровня, префабы, шейдеры, настройки импорта и данные призов. - Зафиксирован embedded-пакет Unity MCP 9.6.8 и обновлены связанные файлы проекта.
227 lines
8.0 KiB
C#
227 lines
8.0 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|