Unity - Google AdMob 광고 삽입하는 법(플러그인 다운로드, 광고 코드)
이 게시물을 참고했습니다 감사합니다(__)
https://github.com/googleads/googleads-mobile-unity/releases/tag/v10.5.0
이 링크에서

패키지 다운 후 드래그
Asset - Google ~ - Settings

Enable 선택 후

여기서 Android 에 ID 입력

뭔진 모르겠지만 있어야하는듯함
using GoogleMobileAds;
using GoogleMobileAds.Api;
using UnityEngine;
public class GoogleMobileAdsDemoScript : MonoBehaviour
{
public void Awake()
{
// Initialize Google Mobile Ads SDK.
MobileAds.Initialize((InitializationStatus initStatus) =>
{
// This callback is called once the MobileAds SDK is initialized.
});
}
}
겜매에 두개를 넣어줌
인스펙터의 저 아이디는 광고 단위에서 추가한 아이디임


AdsManager.cs
using System;
using GoogleMobileAds;
using GoogleMobileAds.Api;
using UnityEngine;
public class AdsManager : MonoBehaviour
{
public static AdsManager I { get; private set; }
#if UNITY_ANDROID
[SerializeField] private string rewardedAdRevivalId = "ca-app-pub-1881501262849586/3221896109"; // 테스트용
#elif UNITY_IOS
[SerializeField] private string rewardedAdUnitId = "ca-app-pub-3940256099942544/1712485313"; // 테스트용
#else
[SerializeField] private string rewardedAdUnitId = "unused";
#endif
private RewardedAd _rewardedAd;
private bool _initializing;
private void Awake()
{
if (I != null) { Destroy(gameObject); return; }
I = this;
DontDestroyOnLoad(gameObject);
}
private void Start()
{
InitializeSdkIfNeeded(() => PreloadRewarded());
}
public void InitializeSdkIfNeeded(Action onDone = null)
{
if (_initializing) return;
_initializing = true;
MobileAds.Initialize((InitializationStatus status) =>
{
_initializing = false;
onDone?.Invoke();
});
}
// 미리 로드
public void PreloadRewarded()
{
if (string.IsNullOrEmpty(rewardedAdRevivalId)) return;
if (_rewardedAd != null)
{
_rewardedAd.Destroy();
_rewardedAd = null;
}
var request = new AdRequest();
RewardedAd.Load(rewardedAdRevivalId, request, (RewardedAd ad, LoadAdError error) =>
{
if (error != null)
{
Debug.LogWarning($"[Ads] Rewarded load failed: {error.GetMessage()}");
return;
}
_rewardedAd = ad;
HookRewardedEvents(_rewardedAd);
Debug.Log("[Ads] Rewarded loaded.");
});
}
private void HookRewardedEvents(RewardedAd ad)
{
ad.OnAdFullScreenContentOpened += () =>
{
Debug.Log("[Ads] Rewarded opened.");
};
ad.OnAdFullScreenContentClosed += () =>
{
Debug.Log("[Ads] Rewarded closed.");
// 닫히면 다음 광고 미리 로드
PreloadRewarded();
};
ad.OnAdFullScreenContentFailed += (AdError err) =>
{
Debug.LogWarning($"[Ads] Rewarded open failed: {err.GetMessage()}");
PreloadRewarded();
};
}
/// <summary>
/// 공용: 리워드 광고 표시 (보상 콜백/미준비 콜백)
/// </summary>
public void ShowRewarded(Action onRewardEarned, Action onUnavailable = null)
{
if (_rewardedAd != null && _rewardedAd.CanShowAd())
{
_rewardedAd.Show((Reward reward) =>
{
onRewardEarned?.Invoke();
});
}
else
{
Debug.LogWarning("[Ads] Rewarded not ready.");
onUnavailable?.Invoke();
PreloadRewarded();
}
}
/// <summary>
/// 부활 전용 진입점
/// </summary>
public void ShowReviveAd(Action onRevive, Action onFail = null)
{
ShowRewarded(
onRewardEarned: onRevive,
onUnavailable: onFail
);
}
}
GameReviveSystem.cs
using System;
using UnityEngine;
using WordEater.Core;
[Serializable]
public class WordEaterCheckpoint
{
public Vector3 Position;
public int BatteryPercent;
public int TurnsLeft;
public int MistakesLeft;
public GrowthStage Stage;
public string CurrentAnswer;
}
public class GameReviveSystem : MonoBehaviour
{
public static GameReviveSystem I { get; private set; }
[SerializeField] private RevivePopup revivePopup;
private WordEaterCheckpoint _cp;
private bool _reviveOffered;
void Awake() => I = this;
public void SaveCheckpoint(WordEater.Core.WordEater we, int batteryPercent)
{
_cp = new WordEaterCheckpoint
{
Position = we.transform.position,
BatteryPercent = Mathf.Clamp(batteryPercent, 0, 100),
TurnsLeft = we.GetTurnsLeft(), //
MistakesLeft = we.GetMistakesLeft(),
Stage = we.ReturnStage(),
CurrentAnswer = we.CurrentAnswer
};
}
public void OnPlayerDied(Action onGiveUp)
{
if (_reviveOffered) return;
_reviveOffered = true;
if (revivePopup == null)
{
Debug.LogWarning("[Revive] revivePopup 미할당");
_reviveOffered = false;
onGiveUp?.Invoke();
return;
}
// ▶ 게임 정지: UI는 Unscaled로 뜨게
Time.timeScale = 0f;
revivePopup.Show(
onAccept: () =>
{
ReviveFromCheckpoint();
Debug.Log("[WordEater] 턴 고갈 → 사망 처리");
_reviveOffered = false;
Time.timeScale = 1f; // ▶ 재개
},
onDecline: () =>
{
_reviveOffered = false;
Time.timeScale = 1f; // ▶ 재개
onGiveUp?.Invoke(); // ▶ EndingController(1) 호출됨
}
);
}
public void ReviveFromCheckpoint()
{
var player = FindFirstObjectByType<WordEater.Core.WordEater>();
if (player == null) { Debug.LogWarning("[Revive] WordEater 없음"); return; }
if (_cp == null) { Debug.LogWarning("[Revive] 체크포인트 없음"); return; }
// 위치/배터리 복원
player.transform.position = _cp.Position;
var battery = player.GetComponent<WordEater.Systems.BatterySystem>()
?? FindFirstObjectByType<WordEater.Systems.BatterySystem>();
if (battery != null) battery.SetBatteryPercent(Mathf.Max(_cp.BatteryPercent, 50));
// 턴/오답/정답/단계 복원
player.RestoreTurns(_cp.TurnsLeft, _cp.MistakesLeft);
player.RestoreAnswer(_cp.CurrentAnswer, _cp.Stage);
// 최종 활성화
player.Reactivate();
Debug.Log("[Revive] 체크포인트로 완전 복원 완료");
}
}
얘는 초기 알파값을 0으로 일단 해두고

RevivePopup.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class RevivePopup : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Button watchAdButton;
[SerializeField] private Button noThanksButton;
[SerializeField] private GameObject spinner; // 로딩 표시용
private Action _onAccept;
private Action _onDecline;
private bool _visible;
private void Awake()
{
if (watchAdButton != null) watchAdButton.onClick.AddListener(OnClickWatchAd);
if (noThanksButton != null) noThanksButton.onClick.AddListener(OnClickNoThanks);
HideImmediate();
}
public void Show(Action onAccept, Action onDecline)
{
_onAccept = onAccept;
_onDecline = onDecline;
_visible = true;
if (spinner != null) spinner.SetActive(false);
// 팝업은 UnscaledTime 기반으로 동작 (애니메이션/타이머 쓴다면)
var cg = canvasGroup;
if (cg != null)
{
cg.ignoreParentGroups = true;
cg.blocksRaycasts = true;
cg.interactable = true;
}
// 최상단 보장(없다면 추가)
var canvas = GetComponentInParent<Canvas>();
if (canvas != null)
{
canvas.overrideSorting = true;
canvas.sortingOrder = 10000;
}
SetCanvas(true);
}
public void Hide()
{
_visible = false;
SetCanvas(false);
}
private void HideImmediate()
{
_visible = false;
SetCanvas(false, immediate: true);
}
private void SetCanvas(bool show, bool immediate = false)
{
if (canvasGroup == null) return;
canvasGroup.alpha = show ? 1f : 0f;
canvasGroup.blocksRaycasts = show;
canvasGroup.interactable = show;
gameObject.SetActive(show);
}
private void OnClickWatchAd()
{
if (!_visible) return;
if (spinner != null) spinner.SetActive(true);
watchAdButton.interactable = false;
noThanksButton.interactable = false;
// 광고 표시
AdsManager.I.ShowReviveAd(
onRevive: () =>
{
// 광고 보상 수령 → 부활 승인
_onAccept?.Invoke();
Hide();
ResetButtons();
},
onFail: () =>
{
// 광고가 준비 안됨/실패 → 안내 후 버튼 복구
if (spinner != null) spinner.SetActive(false);
watchAdButton.interactable = true;
noThanksButton.interactable = true;
Debug.LogWarning("[Revive] 광고가 준비되지 않았습니다. 잠시 후 다시 시도해주세요.");
}
);
}
private void OnClickNoThanks()
{
_onDecline?.Invoke();
Hide();
ResetButtons();
}
private void ResetButtons()
{
if (spinner != null) spinner.SetActive(false);
watchAdButton.interactable = true;
noThanksButton.interactable = true;
}
}
워드이터가 죽었을때 띄우게 하려고 일단 전체 코드
using System;
using System.IO;
using UnityEngine;
using WordEater.Data;
using WordEater.Services;
using WordEater.Systems;
using static UnityEngine.EventSystems.EventTrigger;
namespace WordEater.Core
{
/// <summary>
/// 한 마리 워드 이터의 생애 주기를 관리하는 상태 머신
/// </summary>
public class WordEater : MonoBehaviour
{
[Header("할당")]
[SerializeField] private GrowthConfig growthConfig; // 단계 규칙 SO
[SerializeField] private WordAssignmentService wordService; // 단어 배정
[SerializeField] private BatterySystem battery;
[SerializeField] private SubmitManager submitmanager;
[SerializeField] private GameManager gamemanager;
[SerializeField] private GalleryUIManager galleryUIManager;
[SerializeField] private Sprite BitImg;
[SerializeField] private Sprite ByteImg;
[SerializeField] private Sprite WordImg;
[SerializeField] private bool isDead = false;
[Header("Runtime (read-only)")]
[SerializeField] private GrowthStage stage = GrowthStage.Bit; // 현재 단계
[SerializeField] private string currentAnswer; // 현재 정답(프로토타입용 노출)
private TurnController turn; // 턴/오답 관리자
private WordEntry currentEntry; // 현재 단어 데이터(주제/연관어 포함)
private string pendingEvoId; // Bit/Byte 동안 쓸 임시 키=
private void Awake()
{
turn = new TurnController(growthConfig);
}
/// <summary>
/// 단계 시작(턴/오답 초기화 + 단어 배정)
/// </summary>
public void BeginStage(GrowthStage s, bool initial = false)
{
turn.StartStage(s);
//처음 (다시시작이나 게임 클리어 포함)
if (initial)
{
//BIT상태로 변경
stage = GrowthStage.Bit;
var sr = GetComponent<SpriteRenderer>();
if (sr != null)
{
sr.sprite = BitImg;
}
// 현재 단어 선택
currentEntry = wordService.PickInitialWord();
// ✅ 임시 키 생성 (한 생애 내내 고정)
pendingEvoId = $"evo_{System.DateTime.UtcNow.Ticks}";
// ✅ Bit 썸네일을 임시 키로 저장
if (sr != null)
GalleryCapture.SaveSpriteThumb(sr, $"thumb_{pendingEvoId}_s0", 256);
submitmanager.OnRelevantButton();
}
else
{
var sr = GetComponent<SpriteRenderer>();
if (s == GrowthStage.Byte)
{
if (sr != null) sr.sprite = ByteImg;
// ✅ Byte 썸네일을 임시 키로 저장
if (sr != null)
GalleryCapture.SaveSpriteThumb(sr, $"thumb_{pendingEvoId}_s1", 256);
}
else if (s == GrowthStage.Word)
{
if (sr != null) sr.sprite = WordImg;
}
}
// BeginStage 끝부분(초기 진입 포함)
if (GameReviveSystem.I != null && battery != null)
{
GameReviveSystem.I.SaveCheckpoint(this, battery.CurrentPercent);
}
// EvolveOrFinish에서 다음 단계 단어 배정 직후
if (GameReviveSystem.I != null && battery != null)
{
GameReviveSystem.I.SaveCheckpoint(this, battery.CurrentPercent);
}
currentAnswer = currentEntry.word;
GameEvents.OnNewWordAssigned?.Invoke(currentAnswer); // UI: "새 단어 등장" (정답 직접 노출 대신 디버그/프로토타입용)
}
/// <summary>
/// 데이터 주입(정답/오답 판정, 다음 문제 배정, 진화 체크)
/// </summary>
public void DoFeedData(string userInput)
{
if (!battery.TryConsume(ActionType.FeedData))
return; // OnActionBlockedLowBattery 이벤트로 HUD/토스트 띄우기
// 턴 소모(FeedData는 1턴)
if (!turn.ConsumeTurn(ActionType.FeedData))
{
WordEaterDie();
return;
}
if (turn.TurnsLeft <= 0) { WordEaterDie(); return; }
// 정답 판정(v1 : 완전 일치, v2 : 오타/의미 유사도 확정 예정)
bool ok = IsCorrect(userInput, currentAnswer);
GameEvents.OnFeedResult?.Invoke(userInput, ok);
if (ok)
{
EvolveOrFinish();
}
else
{
if (!turn.RegisterMistake()) { WordEaterDie(); return; }
}
// 턴 바닥나면 사망
if (turn.TurnsLeft <= 0) { WordEaterDie(); return; }
}
/// <summary>
/// 미니게임/힌트(턴 1 소모)
/// </summary>
public void DoOptimizeAlgo() // 미니게임 자리(힌트/버프 지급)
{
if (isDead) return;
if (!turn.ConsumeTurn(ActionType.OptimizeAlgo))
{
WordEaterDie(); return;
}
// TODO: 힌트 토큰 +1, 상성 버프 스택 등
}
/// <summary>
/// 노이즈 제거(턴 2 소모, 배율/버프 예정)
/// </summary>
public void DoCleanNoise() // 2턴 소모, 배율/보상 증가 버프
{
if (isDead) return;
if (!turn.ConsumeTurn(ActionType.CleanNoise))
{
WordEaterDie(); return;
}
// TODO: 배율 스택 += 1
}
// === 내부 로직 =======================================================
private bool IsCorrect(string input, string answer)
{
// v1: 완전 일치. v2: 유사도(레벤슈타인/임베딩) 도입.
return string.Equals(input.Trim(), answer.Trim(), System.StringComparison.Ordinal);
}
/// <summary>
/// 단계 종료 처리(다음 단계 or 성체)
/// - 현재 단계가 최종 단계(Word)이면: 도감에 등록하고 엔딩 처리
/// - 아니면: 다음 단계로 진화
/// </summary>
private void EvolveOrFinish()
{
if (stage == GrowthStage.Word)
{
// 성체 달성 → UI/연출 등 외부 구독자에게 알림
GameEvents.OnEvolved?.Invoke(stage);
// 성체가 되었으므로 "도감"에 현재 개체를 등록
RegisterToGallery();
// 게임 클리어 처리(엔딩 등)
gamemanager.EndingController(2);
galleryUIManager.Refresh();
return;
}
// (성체가 아니라면) 다음 단계 단어 배정 및 진화
currentEntry = wordService.PickNextLinkedWord(currentEntry, stage);
currentAnswer = currentEntry.word;
GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);
stage = (GrowthStage)((int)stage + 1);
GameEvents.OnEvolved?.Invoke(stage);
BeginStage(stage);
}
/// <summary>
/// 사망 처리(휴지통/광고 보상 등 훅)
/// </summary>
private void WordEaterDie()
{
if (isDead) return;
isDead = true;
GameEvents.OnDied?.Invoke();
enabled = false;
// 광고 팝업 띄우고, 거절하면 그때 엔딩
if (GameReviveSystem.I != null)
{
Debug.Log("광고 팝업 띄울겨");
GameReviveSystem.I.OnPlayerDied(onGiveUp: () =>
{
// 정말 포기한 경우에만 게임오버 연출로 이동
gamemanager.EndingController(1);
});
}
else
{
// 시스템이 없으면 안전하게 기존 흐름 유지
gamemanager.EndingController(1);
}
}
// 부활시
public void Reactivate()
{
isDead = false; // 죽은 상태 해제
enabled = true; // 다시 동작
// 필요하면 무적 타이머/상태 초기화/입력언락 등을 여기서 처리
}
public int GetTurnsLeft() => turn.TurnsLeft;
public int GetMistakesLeft() => turn.MistakesLeft; // TurnController에 프로퍼티 노출 필요
public void RestoreTurns(int turnsLeft, int mistakesLeft)
{
turn.ForceRestore(turnsLeft, mistakesLeft); // TurnController에 강제 복원 API 추가
}
public void RestoreAnswer(string answer, GrowthStage s)
{
stage = s;
currentAnswer = answer;
GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);
// 스프라이트 동기화
var sr = GetComponent<SpriteRenderer>();
if (sr != null)
{
if (stage == GrowthStage.Bit) sr.sprite = BitImg;
if (stage == GrowthStage.Byte) sr.sprite = ByteImg;
if (stage == GrowthStage.Word) sr.sprite = WordImg;
}
}
public WordEntry returnCurrentEnrty()
{
return currentEntry;
}
public string CurrentAnswer => currentAnswer;
public GrowthStage ReturnStage()
{
return stage;
}
/// <summary>
/// [도감 등록] 성체 달성 시 현재 워드이터를 도감 JSON에 등록한다.
/// 동작 순서:
/// 1) 현재 단어/단계를 이용해 "고유 ID" 생성 (충돌 방지용)
/// 2) UI에 표시할 제목/카테고리(설명) 텍스트 구성
/// 3) 현재 SpriteRenderer의 스프라이트를 썸네일 PNG로 저장
/// 4) GalleryStore(싱글톤)로 Upsert → gallery.json에 반영
/// </summary>
private void RegisterToGallery()
{
var entry = currentEntry;
// ✅ 최종 키: Word 단계의 MakeStableId (예: "2-수학")
string finalId = MakeStableId(entry);
// 임시 경로/최종 경로
string baseDir = Application.persistentDataPath;
string tmpS0 = Path.Combine(baseDir, $"thumb_{pendingEvoId}_s0.png");
string tmpS1 = Path.Combine(baseDir, $"thumb_{pendingEvoId}_s1.png");
string finS0 = Path.Combine(baseDir, $"thumb_{finalId}_s0.png");
string finS1 = Path.Combine(baseDir, $"thumb_{finalId}_s1.png");
// ✅ Bit/Byte 파일을 최종 키 이름으로 이동(있을 때만)
MoveIfExists(tmpS0, finS0);
MoveIfExists(tmpS1, finS1);
// ✅ Word 썸네일은 최종 키로 저장
var sr = GetComponent<SpriteRenderer>();
string finS2 = Path.Combine(baseDir, $"thumb_{finalId}_s2.png");
GalleryCapture.SaveSpriteThumb(sr, $"thumb_{finalId}_s2", 256);
// 도감 등록: 대표 썸네일은 Word
var item = new GalleryItem
{
id = finalId,
displayName = entry.word,
desc = GetTopicForDisplay(entry),
thumbPath = finS2,
dateCaught = System.DateTime.Now.ToString("yyyy-MM-dd")
};
GalleryStore.Instance.Upsert(item);
}
static void MoveIfExists(string src, string dst)
{
try
{
if (File.Exists(src))
{
// 덮어쓰기 방지: 기존 있으면 삭제
if (File.Exists(dst)) File.Delete(dst);
File.Move(src, dst);
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[Gallery] Move fail {src} -> {dst} : {ex.Message}");
}
}
/// <summary>
/// [도감 고유키 생성] 단어가 중복될 수 있으므로 "단계-단어" 형태로 고정 키를 만든다.
/// 예: stage=2, word="수학" → "2-수학"
/// </summary>
static string MakeStableId(WordEntry e)
{
string slug = e.word.Trim().Replace(" ", ""); // 공백 제거 등 최소 정규화
return $"{e.stage}-{slug}";
}
/// <summary>
/// [표시용 카테고리 텍스트] 데이터 구조상 topic 필드는 없으므로 다음 규칙 적용:
/// - Word(2단계)는 상위 개념이므로 "자기 자신"을 카테고리로 사용
/// - Bit/Byte는 related[] 첫 번째 항목을 대표 카테고리로 사용
/// - 없을 경우 "기타"
/// </summary>
static string GetTopicForDisplay(WordEntry e)
{
if (e.stage == 2) return e.word; // 최상위 카테고리
if (e.related != null && e.related.Length > 0)
return e.related[0]; // 대표 카테고리 하나만 표시
return "기타";
}
}
}
길다 길어
여기만 보면됨
/// <summary>
/// 사망 처리(휴지통/광고 보상 등 훅)
/// </summary>
private void WordEaterDie()
{
if (isDead) return;
isDead = true;
GameEvents.OnDied?.Invoke();
enabled = false;
// 광고 팝업 띄우고, 거절하면 그때 엔딩
if (GameReviveSystem.I != null)
{
Debug.Log("광고 팝업 띄울겨");
GameReviveSystem.I.OnPlayerDied(onGiveUp: () =>
{
// 정말 포기한 경우에만 게임오버 연출로 이동
gamemanager.EndingController(1);
});
}
else
{
// 시스템이 없으면 안전하게 기존 흐름 유지
gamemanager.EndingController(1);
}
}
// 부활시
public void Reactivate()
{
isDead = false; // 죽은 상태 해제
enabled = true; // 다시 동작
// 필요하면 무적 타이머/상태 초기화/입력언락 등을 여기서 처리
}
public int GetTurnsLeft() => turn.TurnsLeft;
public int GetMistakesLeft() => turn.MistakesLeft; // TurnController에 프로퍼티 노출 필요
public void RestoreTurns(int turnsLeft, int mistakesLeft)
{
turn.ForceRestore(turnsLeft, mistakesLeft); // TurnController에 강제 복원 API 추가
}
public void RestoreAnswer(string answer, GrowthStage s)
{
stage = s;
currentAnswer = answer;
GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);
// 스프라이트 동기화
var sr = GetComponent<SpriteRenderer>();
if (sr != null)
{
if (stage == GrowthStage.Bit) sr.sprite = BitImg;
if (stage == GrowthStage.Byte) sr.sprite = ByteImg;
if (stage == GrowthStage.Word) sr.sprite = WordImg;
}
}
TurnController.cs 변경점 (강제 복원 API 추가)
using UnityEngine;
using WordEater.Core;
using WordEater.Data;
namespace WordEater.Core
{
/// <summary>
/// 현재 단계(StageConfig)에 따라 '턴/오답' 수를 관리하는 컨트롤러
/// </summary>
public class TurnController
{
private readonly GrowthConfig _growth;
public int TurnsLeft { get; private set; }
public int MistakesLeft { get; private set; }
public TurnController(GrowthConfig growth) => _growth = growth;
/// <summary>
/// 단계 시작 시 규칙 초기화
/// </summary>
public void StartStage(GrowthStage stage)
{
var cfg = _growth.Get(stage);
TurnsLeft = cfg.turnsPerStage;
MistakesLeft = cfg.maxMistakes;
GameEvents.OnStageStarted?.Invoke(stage, TurnsLeft, MistakesLeft);
}
/// <summary>
/// 액션 수행 시 턴 차감 (Clean은 2턴)
/// </summary>
public bool ConsumeTurn(ActionType action)
{
int cost = action == ActionType.CleanNoise ? 2 : 1;
TurnsLeft -= cost;
// (OnTurnsChanged는 기존대로 두되, 필요시 RaiseTurnsChanged로 교체 가능)
GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
return TurnsLeft > 0;
}
/// <summary>
/// 오답 1회 등록
/// </summary>
public bool RegisterMistake()
{
MistakesLeft -= 1;
// 래퍼로 호출
GameEvents.RaiseMistakesChanged(MistakesLeft);
return MistakesLeft >= 0;
}
/// <summary>
/// ✅ 부활 복원을 위한 강제 복원 API (권장: stage 넘겨서 룰에 맞게 Clamp)
/// </summary>
public void ForceRestore(int turnsLeft, int mistakesLeft, GrowthStage stage)
{
var cfg = _growth.Get(stage);
TurnsLeft = Mathf.Clamp(turnsLeft, 0, cfg.turnsPerStage);
MistakesLeft = Mathf.Clamp(mistakesLeft, 0, cfg.maxMistakes);
GameEvents.OnStageStarted?.Invoke(stage, TurnsLeft, MistakesLeft);
GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
GameEvents.RaiseMistakesChanged(MistakesLeft);
}
/// <summary>
/// (옵션) stage 없이 그대로 복원 – 기존 호출부 호환용
/// </summary>
public void ForceRestore(int turnsLeft, int mistakesLeft)
{
TurnsLeft = turnsLeft;
MistakesLeft = mistakesLeft;
GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
GameEvents.RaiseMistakesChanged(MistakesLeft);
}
}
}