4. 첫 번째 스크립트
프레임워크 전체를 여는 단 하나의 패턴과, 가장 먼저 쓰게 될 다섯 가지 서비스의 복사-붙여넣기 스니펫.
4.1 빠른 시작
새 C# 스크립트를 만들어 아래 내용을 붙여 넣고, 아무 씬의 아무 GameObject에나 올린 뒤 Play를 누르세요:
using CommonGameSystem.Core;
using UnityEngine;
public class HelloCgs : MonoBehaviour
{
private void Start()
{
var scheduler = ServiceLocator.Resolve<IScheduler>();
var tween = ServiceLocator.Resolve<ITweenService>();
scheduler.After(1f, () => Debug.Log("One second after Play."));
tween.To(1f, 2f, 0.5f,
scale => transform.localScale = Vector3.one * scale,
EaseType.OutBack);
}
}
오브젝트가 탄력 있는 오버슈트와 함께 두 배 크기로 튀어 오르고, 1초 뒤 메시지가 나타납니다. 없는 것에 주목하세요: 매니저 프리팹도, 초기화 씬도, 셋업 컴포넌트도 없습니다. 부트스트랩이 여러분의 Start가 실행되기 전에 23개 서비스를 모두 등록했습니다.
4.2 서비스 해석(resolve)의 동작 방식
모든 CGS 서비스는 같은 방식으로 접근합니다: 서비스 로케이터에 필요한 인터페이스를 요청하세요.
var save = ServiceLocator.Resolve<ISaveService>();
세 가지 규칙이 이것을 빠르고 안전하게 유지합니다:
- 일찍 해석하고, 결과를 캐시하세요. 서비스는 첫 씬이 로드되기 전에 등록되므로
Awake와Start모두 해석하기 안전한 위치입니다. 참조를 필드에 저장해 재사용하세요 — 절대Update안에서Resolve를 호출하지 마세요. - 없는 서비스는 예외를 던집니다. 해당 인터페이스에 등록된 것이 없으면
Resolve<T>()는 명확한 예외를 던집니다. 부드러운 확인이 필요하면ServiceLocator.TryResolve(out T service)또는ServiceLocator.IsRegistered<T>()를 사용하세요. - 무엇이든 교체할 수 있습니다.
ServiceLocator.Replace<T>(instance)는 언제든 자체 구현이나 아무것도 하지 않는 널 버전으로 바꿔 넣습니다(1장 1.4절).
전형적인 소비자는 이런 모습입니다:
public class GameHud : MonoBehaviour
{
private ISaveService _save; // cached once, used many times
private void Awake()
{
_save = ServiceLocator.Resolve<ISaveService>();
}
}
4.3 가장 먼저 쓰게 될 다섯 가지 서비스
아래 각 스니펫은 4.1절의 using 선언 아래, MonoBehaviour 안에서 실행됩니다.
저장과 로드
직렬화 가능한 클래스라면 무엇이든 세이브 파일이 될 수 있습니다. 쓰기는 원자적입니다 — 저장 도중 크래시가 나도 이전 파일은 결코 손상되지 않습니다. 전체 레퍼런스: Save/Load.
[System.Serializable]
public class PlayerData { public int Level; public float Health; }
var save = ServiceLocator.Resolve<ISaveService>();
save.Save("slot0", new PlayerData { Level = 3, Health = 75f });
var result = save.Load<PlayerData>("slot0");
if (result.Status == SaveStatus.Ok)
Debug.Log("Loaded level " + result.Value.Level);
이벤트
어떤 클래스든 이벤트가 될 수 있습니다. 발행자와 구독자는 서로를 참조하지 않습니다 — 오직 이벤트 타입만 참조합니다. 전체 레퍼런스: Event Bus.
public sealed class CoinCollected { public int Amount; }
var events = ServiceLocator.Resolve<IEventBus>();
System.IDisposable ticket = events.Subscribe<CoinCollected>(
e => Debug.Log("Coins gained: " + e.Amount));
events.Publish(new CoinCollected { Amount = 5 });
ticket.Dispose(); // stop listening (do this in OnDestroy)
타이머
타이머는 예약된 순서대로 발화합니다. 일회성 타이머는 스스로 정리되고, 반복 타이머는 dispose할 때까지 계속 돕니다. 전체 레퍼런스: Scheduler.
var scheduler = ServiceLocator.Resolve<IScheduler>();
scheduler.After(2f, () => Debug.Log("Two seconds later, exactly once."));
System.IDisposable heartbeat =
scheduler.Every(0.5f, () => Debug.Log("Every half second."));
heartbeat.Dispose(); // stop the repeating timer when done
트위닝
트윈은 값을 한 숫자에서 다른 숫자로 애니메이션하며 각 단계를 여러분에게 건네줍니다. 기본적으로 트윈은 게임플레이 클럭에서 돌아가므로 게임이 일시정지되면 함께 일시정지됩니다. 전체 레퍼런스: Tween.
var tween = ServiceLocator.Resolve<ITweenService>();
CanvasGroup group = GetComponent<CanvasGroup>();
tween.To(1f, 0f, 0.75f,
alpha => group.alpha = alpha,
EaseType.OutQuad,
onComplete: () => Debug.Log("Fade finished."));
입력
입력은 여러분의 input actions 애셋에 있는 이름 있는 액션 맵을 통해 읽습니다. "Gameplay"와 "Jump"를 여러분의 애셋에 있는 이름으로 바꾸세요. InputAction 타입을 위해 using UnityEngine.InputSystem;을 추가하세요. 전체 레퍼런스: Input.
private IInputService _input;
private InputAction _jump;
private void Start()
{
_input = ServiceLocator.Resolve<IInputService>();
_jump = _input.GetAction("Gameplay", "Jump");
}
private void Update()
{
if (_jump != null && _input.WasPressedThisFrame(_jump))
Debug.Log("Jump!");
}
4.4 다음 단계
이제 프레임워크 전체를 여는 단 하나의 패턴을 알게 되었습니다: 인터페이스를 해석하고, 캐시하고, 호출한다. 여기서부터는:
- 서비스 레퍼런스. 모든 서비스에 자체 페이지가 있습니다 — 문서 색인에서 시작하거나, 프로젝트의
Assets/CommonGameSystem.Core/Documentation/Modules/(예:SaveLoad.md,Tween.md)에서 보세요. 각 페이지는 전체 API, 흔한 패턴, 중요한 주의사항을 다룹니다. - 샘플. RPG Starter Sample(3장)은 위 서비스 여러 개가 하나의 작은 게임 조각 안에서 협력하는 모습을 보여줍니다.
- 이 매뉴얼의 나머지. 5장은 다섯 가지 핵심 개념을 설명하고, 6장은 프레임워크를 여러분의 애셋에 연결합니다.
- 체인지로그. Welcome 창의 퀵 링크(3장)에서 설치된 버전의 문서와 체인지로그를 열 수 있습니다.
다음: 5. 핵심 개념