Add Escape Feature

This commit is contained in:
copecone committed 2026-05-24 14:50:42 +09:00
1 parent 0b4a772933
commit fb97a85875
4 files changed
+224 -9

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
using System.Diagnostics;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace ADOFAIRenderer.Component
{
public class EscapeHandlerUI : MonoBehaviour
{
private GameObject _containerObj = null!;
private TextMeshProUGUI _textComponent = null!;
private Stopwatch? _escapeStopwatch;
private void Start()
{
// UI 레이어(5) 설정
gameObject.layer = 5;
// Canvas 설정
var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 10000; // 디버그 모니터보다 위에 오도록 설정
// 해상도 대응 캔버스 스케일러 설정
var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
gameObject.AddComponent<GraphicRaycaster>();
// 1. 컨테이너 및 배경 패널 오브젝트 생성
_containerObj = new GameObject("EscapeContainer");
_containerObj.transform.SetParent(transform, false);
_containerObj.layer = 5;
var bgImage = _containerObj.AddComponent<Image>();
// 고급스러운 다크톤 아크릴 감성
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f);
var containerRect = _containerObj.GetComponent<RectTransform>();
containerRect.anchorMin = new Vector2(0.5f, 0.5f);
containerRect.anchorMax = new Vector2(0.5f, 0.5f);
containerRect.pivot = new Vector2(0.5f, 0.5f);
containerRect.anchoredPosition = Vector2.zero; // 화면 중앙
containerRect.sizeDelta = new Vector2(460, 160);
// 2. 텍스트 오브젝트 생성
var textObj = new GameObject("EscapeText");
textObj.transform.SetParent(_containerObj.transform, false);
textObj.layer = 5;
_textComponent = textObj.AddComponent<TextMeshProUGUI>();
_textComponent.alignment = TextAlignmentOptions.Center;
_textComponent.fontSize = 60;
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
_textComponent.richText = true;
// 게임의 기본 폰트 적용 (RDString.fontData.fontTMP)
if (RDString.fontData.fontTMP != null)
_textComponent.font = RDString.fontData.fontTMP;
var textRect = textObj.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = new Vector2(10, 10);
textRect.offsetMax = new Vector2(-10, -10);
// 초기 상태는 비활성화
_containerObj.SetActive(false);
}
private void Update()
{
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
if (!RenderMod.IsRendering)
{
Destroy(gameObject);
return;
}
if (Input.GetKey(KeyCode.Escape))
{
if (_escapeStopwatch == null)
{
_escapeStopwatch = new Stopwatch();
_escapeStopwatch.Start();
_containerObj.SetActive(true);
}
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
var remaining = Mathf.Max(0f, 3f - elapsed);
// 남은 시간 텍스트 업데이트
_textComponent.text = $"<color=#FF5252>탈출까지 {remaining:F1}초...</color><\n<size=40><color=#DDDDDD>Esc 키를 계속 누르고 계세요</color></size>";
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐)
var scale = 1.0f + (elapsed / 3.0f) * 0.15f;
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
if (elapsed < 3f) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
_containerObj.SetActive(false);
// 렌더링 취소 및 씬 이탈
if (RenderManager.Instance)
RenderManager.Instance.CancelRender();
}
else
{
if (_escapeStopwatch == null) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
_containerObj.SetActive(false);
}
}
private void OnDestroy()
{
if (_escapeStopwatch == null) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
}
}
}
+85 -8
View File
@@ -19,6 +19,8 @@ namespace ADOFAIRenderer.Component
public static RenderManager Instance = null!; public static RenderManager Instance = null!;
private static int _originalInputOffset; private static int _originalInputOffset;
private static GameObject? _debugMonitorObj; private static GameObject? _debugMonitorObj;
private static GameObject? _escapeHandlerObj;
private static string? _currentOutPath;
public static RenderSettings Configuration; public static RenderSettings Configuration;
@@ -96,6 +98,13 @@ namespace ADOFAIRenderer.Component
RenderMod.IsRendering = true; RenderMod.IsRendering = true;
Countdown = 3; Countdown = 3;
if (!_escapeHandlerObj)
{
_escapeHandlerObj = new GameObject("EscapeHandlerCanvas");
_escapeHandlerObj.AddComponent<EscapeHandlerUI>();
DontDestroyOnLoad(_escapeHandlerObj);
}
scrController.instance!.StartCoroutine(SetupPlay()); scrController.instance!.StartCoroutine(SetupPlay());
} }
@@ -103,10 +112,8 @@ namespace ADOFAIRenderer.Component
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2) if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
{ {
RenderMod.IsRendering = RenderMod.IsCapturing = false; RenderMod.IsRendering = RenderMod.IsCapturing = false;
Time.timeScale = 1f;
scrConductor.currentPreset.inputOffset = _originalInputOffset;
StopFFmpeg(false); StopFFmpeg(false);
EndRender();
} }
else if (scrController.instance.state == States.Won) else if (scrController.instance.state == States.Won)
{ {
@@ -119,8 +126,6 @@ namespace ADOFAIRenderer.Component
if (FrameCount - WonFrame < extraFrames) return; if (FrameCount - WonFrame < extraFrames) return;
RenderMod.IsRendering = RenderMod.IsCapturing = false; RenderMod.IsRendering = RenderMod.IsCapturing = false;
Time.timeScale = 1f;
scrConductor.currentPreset.inputOffset = _originalInputOffset;
StopFFmpeg(true); StopFFmpeg(true);
} }
} }
@@ -236,6 +241,7 @@ namespace ADOFAIRenderer.Component
var startTime = DateTime.Now; var startTime = DateTime.Now;
var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + ext; var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + ext;
_currentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
RenderThread = new Thread(() => RenderThread = new Thread(() =>
{ {
@@ -244,10 +250,9 @@ namespace ADOFAIRenderer.Component
var workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps); var workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps);
var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec); var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec);
var outPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
SetupCaptureDirectory(); SetupCaptureDirectory();
videoFFmpeg.Start(outPath, bitrate); videoFFmpeg.Start(_currentOutPath, bitrate);
int num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps); int num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
@@ -304,32 +309,100 @@ namespace ADOFAIRenderer.Component
RenderThread.Start(); RenderThread.Start();
} }
public void CancelRender()
{
RenderMod.IsRendering = RenderMod.IsCapturing = false;
StopFFmpeg(false);
EndRender();
}
private void StopFFmpeg(bool success) private void StopFFmpeg(bool success)
{ {
EndRequested = true; EndRequested = true;
SkipPressToStart = false;
_renderSetup = false;
if (_escapeHandlerObj)
{
Destroy(_escapeHandlerObj);
_escapeHandlerObj = null;
}
if (success) if (success)
{ {
StartCoroutine(StopRenderCoroutine()); StartCoroutine(StopRenderCoroutine());
} }
else else
{ {
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다."); Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다.");
// 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
lock (ImageQueue)
{
foreach (var kvp in ImageQueue)
{
if (kvp.Value != null && kvp.Value.Length > 0)
{
System.Buffers.ArrayPool<byte>.Shared.Return(kvp.Value);
}
}
ImageQueue.Clear();
}
// 디버그 모니터 UI 정리
if (_debugMonitorObj) if (_debugMonitorObj)
{ {
Destroy(_debugMonitorObj); Destroy(_debugMonitorObj);
_debugMonitorObj = null; _debugMonitorObj = null;
} }
// 2. 렌더링 스레드가 큐 비어 있음을 감지하고 break 하도록 대기
if (RenderThread is { IsAlive: true }) if (RenderThread is { IsAlive: true })
{ {
RenderThread.Join(2000); RenderThread.Join(2000);
} }
// 3. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복
if (scrCamera.instance)
{
scrCamera.instance.forceRTCam = false;
scrCamera.instance.SetupRTCam(false);
var hook = scrCamera.instance.GetComponent<PostRenderHook>();
if (hook) Destroy(hook);
}
// 4. 인게임 실패 방지 플래그 원상 복구
if (scrController.instance)
{
scrController.instance.noFail = false;
scrController.instance.noFailInfiniteMargin = false;
}
// 5. 취소된 미완성 임시 동영상 파일 즉각 삭제
if (!string.IsNullOrEmpty(_currentOutPath) && File.Exists(_currentOutPath))
{
try
{
File.Delete(_currentOutPath);
Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {_currentOutPath}");
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}");
}
_currentOutPath = null;
}
} }
} }
private static IEnumerator StopRenderCoroutine() private static IEnumerator StopRenderCoroutine()
{ {
yield return new WaitUntil(() => EndSuccessful); yield return new WaitUntil(() => EndSuccessful);
EndRender();
}
private static void EndRender()
{
Time.timeScale = 1f;
if (_debugMonitorObj) if (_debugMonitorObj)
{ {
Destroy(_debugMonitorObj); Destroy(_debugMonitorObj);
@@ -337,6 +410,10 @@ namespace ADOFAIRenderer.Component
} }
RDC.auto = false; RDC.auto = false;
scrConductor.currentPreset.inputOffset = _originalInputOffset;
GCS.customLevelPaths = null;
GCS.customLevelId = null;
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ using UnityEngine.EventSystems;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.3-BETA", "ADOFAI Editor Forum")] [assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.4-BETA", "ADOFAI Editor Forum")]
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
namespace ADOFAIRenderer namespace ADOFAIRenderer
+8
View File
@@ -36,6 +36,14 @@ namespace ADOFAIRenderer.UI
codecDropdown.AddOptions(options); codecDropdown.AddOptions(options);
} }
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
}
}
public void OnClick() public void OnClick()
{ {
if (IsAnyFieldEmpty()) return; if (IsAnyFieldEmpty()) return;