Generador Procedural de Torres y Seguimiento de Cámara Profesional
## Tutorial 5: Generador Procedural de Torres (`HelixManager`)
### ¿Para qué sirve?
El cerebro procedural del juego. Rota la torre basándose en el arrastre (*swipe*) del usuario, lee los ScriptableObjects de etapa, instancia los niveles.
### Código del Script (`HelixManager.cs`)
```csharp
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class HelixManager : MonoBehaviour
{
[Header("Referencias de Posicionamiento")]
[SerializeField] private Transform topTransform;
[SerializeField] private Transform goalTransform;
[SerializeField] private GameObject helixLevelPrefab;
[Header("Configuración de Etapas")]
[SerializeField] private List<Stage> allStages = new List<Stage>();
[SerializeField] private GameObject splashPrefab;
private Vector2 lastTapPosition;
private Vector3 startRotation;
private float helixDistance;
private List<GameObject> spawnedLevels = new List<GameObject>();
private void Awake()
{
startRotation = transform.localEulerAngles;
if (topTransform != null && goalTransform != null)
{
helixDistance = topTransform.localPosition.y - (goalTransform.localPosition.y + 0.1f);
}
LoadStage(0);
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
lastTapPosition = Input.mousePosition;
}
else if (Input.GetMouseButton(0))
{
Vector2 currentTapPosition = Input.mousePosition;
if (lastTapPosition == Vector2.zero) lastTapPosition = currentTapPosition;
float distance = (lastTapPosition.x - currentTapPosition.x) / 4f;
lastTapPosition = currentTapPosition;
transform.Rotate(Vector3.up * distance);
}
else if (Input.GetMouseButtonUp(0))
{
lastTapPosition = Vector2.zero;
}
}
public void LoadStage(int stageNumber)
{
if (stageNumber >= allStages.Count)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
return;
}
Stage stage = allStages[Mathf.Clamp(stageNumber, 0, allStages.Count - 1)];
if (stage == null) return;
Camera.main.backgroundColor = stage.stageBackgroundColor;
if (splashPrefab != null)
{
SpriteRenderer splashRend = splashPrefab.GetComponent<SpriteRenderer>();
if (splashRend != null) splashRend.color = stage.stageSplashColor;
}
BallManager ball = FindFirstObjectByType<BallManager>();
if (ball != null)
{
Renderer ballRend = ball.GetComponent<Renderer>();
if (ballRend != null) ballRend.material.color = stage.stageBallColor;
}
Renderer helixRend = GetComponent<Renderer>();
if (helixRend != null) helixRend.material.color = stage.stageHelixColor;
transform.localEulerAngles = startRotation;
foreach (GameObject go in spawnedLevels)
{
if (go != null) Destroy(go);
}
spawnedLevels.Clear();
if (stage.levels.Count == 0 || helixLevelPrefab == null) return;
float levelDistance = helixDistance / stage.levels.Count;
float spawnPosY = topTransform.localPosition.y;
for (int i = 0; i < stage.levels.Count; i++)
{
spawnPosY -= levelDistance;
GameObject levelInstance = Instantiate(helixLevelPrefab, transform);
levelInstance.transform.localPosition = new Vector3(0, spawnPosY, 0);
spawnedLevels.Add(levelInstance);
int childCount = levelInstance.transform.childCount;
int partsToDisable = Mathf.Clamp(12 - stage.levels[i].parCount, 0, childCount);
List<GameObject> disableParts = new List<GameObject>();
int safetyCounter = 0;
while (disableParts.Count < partsToDisable && safetyCounter < 100)
{
safetyCounter++;
int randomIndex = Random.Range(0, childCount);
GameObject randomPart = levelInstance.transform.GetChild(randomIndex).gameObject;
if (!disableParts.Contains(randomPart))
{
randomPart.SetActive(false);
disableParts.Add(randomPart);
}
}
List<GameObject> leftParts = new List<GameObject>();
foreach (Transform t in levelInstance.transform)
{
Renderer partRend = t.GetComponent<Renderer>();
if (partRend != null) partRend.material.color = stage.stageLevelPartColor;
if (t.gameObject.activeInHierarchy) leftParts.Add(t.gameObject);
}
int deathPartsCount = Mathf.Clamp(stage.levels[i].deathPartCount, 0, leftParts.Count);
List<GameObject> deathParts = new List<GameObject>();
safetyCounter = 0;
while (deathParts.Count < deathPartsCount && leftParts.Count > 0 && safetyCounter < 100)
{
safetyCounter++;
int randomIndex = Random.Range(0, leftParts.Count);
GameObject randomPart = leftParts[randomIndex];
if (!deathParts.Contains(randomPart))
{
if (randomPart.GetComponent<DeathZone>() == null) randomPart.AddComponent<DeathZone>();
deathParts.Add(randomPart);
}
}
}
}
}
```
### Implementación
Se asigna al objeto raíz de la torre o cilindro central en la jerarquía de la escena.
---
## Tutorial 6: Seguimiento de Cámara Profesional (`CameraAdministrador`)
### ¿Para qué sirve?
Sigue verticalmente a la bola a lo largo de su descenso por la torre. Se ejecuta en `LateUpdate` para evitar tirones (*jitter*) derivados de las físicas y cuenta con búsqueda automática de referencias. verticalmente y distribuye de forma aleatoria.
### Código del Script (`CameraAdministrador.cs`)
```csharp
using UnityEngine;
public class CameraAdministrador : MonoBehaviour
{
[Header("Configuración de Seguimiento")]
[SerializeField] private BallManager ball;
[SerializeField] private bool smoothFollow = false;
[SerializeField] private float smoothSpeed = 10f;
private float offset;
void Start()
{
if (ball == null) ball = FindFirstObjectByType<BallManager>();
if (ball != null)
{
offset = transform.position.y - ball.transform.position.y;
}
}
void LateUpdate()
{
if (ball == null) return;
Vector3 targetPos = transform.position;
float targetY = ball.transform.position.y + offset;
if (smoothFollow)
{
targetPos.y = Mathf.Lerp(transform.position.y, targetY, smoothSpeed * Time.deltaTime);
}
else
{
targetPos.y = targetY;
}
transform.position = targetPos;
}
}
```
### Implementación
Agrégalo al componente Main Camera de tu escena de juego.
---

Deja un comentario