앱인토스 개발자센터 로고
Skip to content

시작 스토리 디자인 가이드

AppsInToss Unity 게임에서 매력적이고 몰입도 높은 게임 시작 경험을 설계하는 방법을 다뤄요.


1. 런치 오페라 개념

런치 오페라란?

🎭 런치 오페라 구성요소
├── 브랜드 스토리텔링
│   ├── 토스 × 게임 브랜딩
│   ├── 게임 세계관 소개
│   └── 캐릭터/스토리 티저
├── 사용자 온보딩
│   ├── 직관적인 조작법 안내
│   ├── 핵심 게임 메커니즘 소개
│   └── 진행 목표 제시
├── 감정적 연결
│   ├── 음악과 사운드 디자인
│   ├── 시각적 임팩트
│   └── 개인화된 경험
└── 즉시 참여 유도
    ├── 빠른 액션 시퀀스
    ├── 성취감 제공
    └── 다음 단계로의 자연스러운 전환

런치 오페라 매니저

c#
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using TMPro;

public class LaunchOperaManager : MonoBehaviour
{
    public static LaunchOperaManager Instance { get; private set; }
    
    [System.Serializable]
    public class OperaSequence
    {
        [Header("시퀀스 설정")]
        public string sequenceName;
        public float duration = 3f;
        public bool skippable = true;
        public bool autoAdvance = true;
        
        [Header("시각적 요소")]
        public GameObject[] visualElements;
        public AnimationClip[] animations;
        public Sprite backgroundImage;
        public Color backgroundColor = Color.black;
        
        [Header("오디오")]
        public AudioClip backgroundMusic;
        public AudioClip[] soundEffects;
        public float musicVolume = 0.7f;
        
        [Header("텍스트")]
        public string title;
        public string[] dialogues;
        public float textSpeed = 50f; // 글자/초
        public bool showCharacterByCharacter = true;
        
        [Header("인터랙션")]
        public bool requiresUserInput = false;
        public string inputPrompt = "터치하여 계속";
        public KeyCode[] skipKeys = { KeyCode.Space, KeyCode.Return };
    }
    
    [Header("런치 오페라 설정")]
    public OperaSequence[] operaSequences;
    public bool enableSkipAll = true;
    public string skipAllText = "모두 건너뛰기";
    
    [Header("UI 컴포넌트")]
    public Canvas operaCanvas;
    public Image backgroundImage;
    public TextMeshProUGUI titleText;
    public TextMeshProUGUI dialogueText;
    public Button continueButton;
    public Button skipButton;
    public Button skipAllButton;
    public Slider progressSlider;
    
    [Header("애니메이션 컴포넌트")]
    public Animator sceneAnimator;
    public ParticleSystem[] atmosphereParticles;
    
    [Header("오디오 컴포넌트")]
    public AudioSource musicSource;
    public AudioSource sfxSource;
    
    // 내부 상태
    private int currentSequenceIndex = 0;
    private bool isPlaying = false;
    private bool isSequenceComplete = false;
    private bool userWantsToSkip = false;
    private Coroutine currentSequenceCoroutine;
    private Coroutine textAnimationCoroutine;
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            InitializeLaunchOpera();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeLaunchOpera()
    {
        // UI 초기 설정
        SetupInitialUI();
        
        // 이벤트 리스너 등록
        RegisterEventListeners();
        
        // 오디오 초기화
        InitializeAudio();
        
        Debug.Log("런치 오페라 시스템 초기화 완료");
    }
    
    void SetupInitialUI()
    {
        // 초기 UI 상태
        operaCanvas.gameObject.SetActive(true);
        titleText.text = "";
        dialogueText.text = "";
        
        // 버튼 설정
        continueButton.onClick.AddListener(OnContinueClicked);
        skipButton.onClick.AddListener(OnSkipClicked);
        skipAllButton.onClick.AddListener(OnSkipAllClicked);
        
        // 스킵 버튼 표시/숨김
        skipAllButton.gameObject.SetActive(enableSkipAll);
        
        // 진행률 바 초기화
        progressSlider.value = 0f;
        progressSlider.maxValue = operaSequences.Length;
    }
    
    void RegisterEventListeners()
    {
        AppsInToss.OnEvent += HandleAppsInTossEvent;
    }
    
    void InitializeAudio()
    {
        // 오디오 소스 초기 설정
        if (musicSource == null)
            musicSource = gameObject.AddComponent<AudioSource>();
        if (sfxSource == null)
            sfxSource = gameObject.AddComponent<AudioSource>();
        
        musicSource.loop = true;
        sfxSource.loop = false;
    }
    
    void HandleAppsInTossEvent(string eventName, Dictionary<string, object> data)
    {
        switch (eventName)
        {
            case "opera_start":
                StartLaunchOpera();
                break;
            case "opera_sequence_complete":
                OnSequenceComplete();
                break;
            case "opera_user_skip":
                userWantsToSkip = true;
                break;
        }
    }
    
    public void StartLaunchOpera()
    {
        if (isPlaying) return;
        
        isPlaying = true;
        currentSequenceIndex = 0;
        
        Debug.Log("런치 오페라 시작");
        
        // 첫 번째 시퀀스 시작
        StartCurrentSequence();
        
        // 분석 데이터 전송
        SendOperaAnalytics("opera_started", currentSequenceIndex);
    }
    
    void StartCurrentSequence()
    {
        if (currentSequenceIndex >= operaSequences.Length)
        {
            CompleteLaunchOpera();
            return;
        }
        
        var sequence = operaSequences[currentSequenceIndex];
        isSequenceComplete = false;
        userWantsToSkip = false;
        
        Debug.Log($"시퀀스 시작: {sequence.sequenceName}");
        
        // 현재 시퀀스 실행
        if (currentSequenceCoroutine != null)
        {
            StopCoroutine(currentSequenceCoroutine);
        }
        
        currentSequenceCoroutine = StartCoroutine(PlaySequence(sequence));
        
        // 진행률 업데이트
        UpdateProgress();
    }
    
    IEnumerator PlaySequence(OperaSequence sequence)
    {
        // 시퀀스 시작 준비
        yield return StartCoroutine(PrepareSequence(sequence));
        
        // 백그라운드 뮤직 시작
        if (sequence.backgroundMusic != null)
        {
            PlayBackgroundMusic(sequence.backgroundMusic, sequence.musicVolume);
        }
        
        // 시각적 요소 애니메이션
        StartCoroutine(AnimateVisualElements(sequence));
        
        // 대화/텍스트 표시
        yield return StartCoroutine(ShowSequenceText(sequence));
        
        // 사용자 입력 대기 또는 자동 진행
        if (sequence.requiresUserInput)
        {
            yield return StartCoroutine(WaitForUserInput(sequence));
        }
        else if (!sequence.autoAdvance)
        {
            yield return new WaitForSeconds(sequence.duration);
        }
        
        // 시퀀스 완료
        yield return StartCoroutine(CompleteSequence(sequence));
        
        isSequenceComplete = true;
        
        // 다음 시퀀스로 진행
        if (!userWantsToSkip)
        {
            yield return new WaitForSeconds(0.5f);
            NextSequence();
        }
    }
    
    IEnumerator PrepareSequence(OperaSequence sequence)
    {
        // 배경 설정
        if (sequence.backgroundImage != null)
        {
            backgroundImage.sprite = sequence.backgroundImage;
            backgroundImage.color = Color.white;
        }
        else
        {
            backgroundImage.color = sequence.backgroundColor;
        }
        
        // 시각적 요소 초기화
        foreach (var element in sequence.visualElements)
        {
            if (element != null)
            {
                element.SetActive(true);
            }
        }
        
        // 페이드 인 효과
        yield return StartCoroutine(FadeInSequence());
        
        // 분위기 파티클 시작
        StartAtmosphereParticles();
    }
    
    IEnumerator FadeInSequence()
    {
        CanvasGroup canvasGroup = operaCanvas.GetComponent<CanvasGroup>();
        if (canvasGroup == null)
        {
            canvasGroup = operaCanvas.gameObject.AddComponent<CanvasGroup>();
        }
        
        float fadeTime = 0.5f;
        float elapsedTime = 0f;
        
        canvasGroup.alpha = 0f;
        
        while (elapsedTime < fadeTime)
        {
            elapsedTime += Time.deltaTime;
            canvasGroup.alpha = elapsedTime / fadeTime;
            yield return null;
        }
        
        canvasGroup.alpha = 1f;
    }
    
    void StartAtmosphereParticles()
    {
        foreach (var particles in atmosphereParticles)
        {
            if (particles != null && !particles.isPlaying)
            {
                particles.Play();
            }
        }
    }
    
    IEnumerator AnimateVisualElements(OperaSequence sequence)
    {
        // 씬 애니메이터 트리거
        if (sceneAnimator != null && !string.IsNullOrEmpty(sequence.sequenceName))
        {
            sceneAnimator.SetTrigger(sequence.sequenceName);
        }
        
        // 개별 요소 애니메이션
        for (int i = 0; i < sequence.animations.Length && i < sequence.visualElements.Length; i++)
        {
            if (sequence.animations[i] != null && sequence.visualElements[i] != null)
            {
                var elementAnimator = sequence.visualElements[i].GetComponent<Animator>();
                if (elementAnimator != null)
                {
                    elementAnimator.Play(sequence.animations[i].name);
                }
            }
        }
        
        yield return null;
    }
    
    IEnumerator ShowSequenceText(OperaSequence sequence)
    {
        // 제목 표시
        if (!string.IsNullOrEmpty(sequence.title))
        {
            titleText.text = sequence.title;
            yield return StartCoroutine(AnimateTextAppearance(titleText));
        }
        
        // 대화 텍스트 순차 표시
        foreach (var dialogue in sequence.dialogues)
        {
            if (userWantsToSkip) break;
            
            if (sequence.showCharacterByCharacter)
            {
                yield return StartCoroutine(ShowTextCharacterByCharacter(dialogue, sequence.textSpeed));
            }
            else
            {
                dialogueText.text = dialogue;
                yield return StartCoroutine(AnimateTextAppearance(dialogueText));
            }
            
            // 대화 간 대기 시간
            if (sequence.dialogues.Length > 1)
            {
                yield return new WaitForSeconds(1f);
            }
        }
    }
    
    IEnumerator ShowTextCharacterByCharacter(string text, float charactersPerSecond)
    {
        dialogueText.text = "";
        float timePerCharacter = 1f / charactersPerSecond;
        
        for (int i = 0; i <= text.Length; i++)
        {
            if (userWantsToSkip) 
            {
                dialogueText.text = text;
                break;
            }
            
            dialogueText.text = text.Substring(0, i);
            
            // 문장부호에서 약간 더 대기
            if (i < text.Length && (text[i] == '.' || text[i] == '!' || text[i] == '?'))
            {
                yield return new WaitForSeconds(timePerCharacter * 3f);
            }
            else
            {
                yield return new WaitForSeconds(timePerCharacter);
            }
        }
    }
    
    IEnumerator AnimateTextAppearance(TextMeshProUGUI textComponent)
    {
        textComponent.color = new Color(textComponent.color.r, textComponent.color.g, textComponent.color.b, 0f);
        
        float fadeTime = 0.5f;
        float elapsedTime = 0f;
        
        while (elapsedTime < fadeTime)
        {
            if (userWantsToSkip) break;
            
            elapsedTime += Time.deltaTime;
            float alpha = elapsedTime / fadeTime;
            textComponent.color = new Color(textComponent.color.r, textComponent.color.g, textComponent.color.b, alpha);
            
            yield return null;
        }
        
        textComponent.color = new Color(textComponent.color.r, textComponent.color.g, textComponent.color.b, 1f);
    }
    
    IEnumerator WaitForUserInput(OperaSequence sequence)
    {
        // 입력 안내 표시
        continueButton.gameObject.SetActive(true);
        continueButton.GetComponentInChildren<TextMeshProUGUI>().text = sequence.inputPrompt;
        
        bool inputReceived = false;
        
        while (!inputReceived && !userWantsToSkip)
        {
            // 터치/클릭 입력 체크
            if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
            {
                inputReceived = true;
            }
            
            if (Input.GetMouseButtonDown(0))
            {
                inputReceived = true;
            }
            
            // 키보드 입력 체크
            foreach (var key in sequence.skipKeys)
            {
                if (Input.GetKeyDown(key))
                {
                    inputReceived = true;
                    break;
                }
            }
            
            yield return null;
        }
        
        continueButton.gameObject.SetActive(false);
    }
    
    IEnumerator CompleteSequence(OperaSequence sequence)
    {
        // 시퀀스 완료 애니메이션
        yield return StartCoroutine(FadeOutSequence());
        
        // 시각적 요소 정리
        foreach (var element in sequence.visualElements)
        {
            if (element != null)
            {
                element.SetActive(false);
            }
        }
        
        // 분석 데이터 전송
        SendOperaAnalytics("sequence_completed", currentSequenceIndex);
    }
    
    IEnumerator FadeOutSequence()
    {
        CanvasGroup canvasGroup = operaCanvas.GetComponent<CanvasGroup>();
        
        float fadeTime = 0.3f;
        float elapsedTime = 0f;
        
        while (elapsedTime < fadeTime)
        {
            elapsedTime += Time.deltaTime;
            canvasGroup.alpha = 1f - (elapsedTime / fadeTime);
            yield return null;
        }
        
        canvasGroup.alpha = 0f;
        
        // 텍스트 정리
        titleText.text = "";
        dialogueText.text = "";
    }
    
    void PlayBackgroundMusic(AudioClip music, float volume)
    {
        if (musicSource != null && music != null)
        {
            musicSource.clip = music;
            musicSource.volume = volume;
            musicSource.Play();
        }
    }
    
    void PlaySoundEffect(AudioClip sfx)
    {
        if (sfxSource != null && sfx != null)
        {
            sfxSource.PlayOneShot(sfx);
        }
    }
    
    void NextSequence()
    {
        currentSequenceIndex++;
        StartCurrentSequence();
    }
    
    void CompleteLaunchOpera()
    {
        isPlaying = false;
        
        Debug.Log("런치 오페라 완료");
        
        // 완료 애니메이션
        StartCoroutine(PlayCompletionSequence());
    }
    
    IEnumerator PlayCompletionSequence()
    {
        // 모든 UI 페이드 아웃
        CanvasGroup canvasGroup = operaCanvas.GetComponent<CanvasGroup>();
        
        float fadeTime = 1f;
        float elapsedTime = 0f;
        
        while (elapsedTime < fadeTime)
        {
            elapsedTime += Time.deltaTime;
            canvasGroup.alpha = 1f - (elapsedTime / fadeTime);
            yield return null;
        }
        
        operaCanvas.gameObject.SetActive(false);
        
        // 파티클 정리
        foreach (var particles in atmosphereParticles)
        {
            if (particles != null)
            {
                particles.Stop();
            }
        }
        
        // 완료 분석 데이터 전송
        SendOperaAnalytics("opera_completed", operaSequences.Length);
        
        // 게임 시작 신호 전송
        AppsInToss.SendEvent("opera_finished", new Dictionary<string, object>
        {
            {"total_sequences", operaSequences.Length},
            {"completion_time", Time.time}
        });
        
        // 메인 게임 씬으로 전환
        StartMainGame();
    }
    
    void StartMainGame()
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameScene");
    }
    
    void UpdateProgress()
    {
        if (progressSlider != null)
        {
            progressSlider.value = currentSequenceIndex;
        }
    }
    
    void OnContinueClicked()
    {
        // 현재 텍스트 애니메이션이 진행 중이면 즉시 완료
        if (textAnimationCoroutine != null)
        {
            StopCoroutine(textAnimationCoroutine);
            userWantsToSkip = true;
        }
        else if (isSequenceComplete)
        {
            NextSequence();
        }
    }
    
    void OnSkipClicked()
    {
        if (operaSequences[currentSequenceIndex].skippable)
        {
            userWantsToSkip = true;
            
            SendOperaAnalytics("sequence_skipped", currentSequenceIndex);
        }
    }
    
    void OnSkipAllClicked()
    {
        if (enableSkipAll)
        {
            // 모든 시퀀스 건너뛰기
            SendOperaAnalytics("opera_skipped", currentSequenceIndex);
            
            CompleteLaunchOpera();
        }
    }
    
    void SendOperaAnalytics(string eventType, int sequenceIndex)
    {
        var analyticsData = new Dictionary<string, object>
        {
            {"event_type", eventType},
            {"sequence_index", sequenceIndex},
            {"sequence_name", sequenceIndex < operaSequences.Length ? operaSequences[sequenceIndex].sequenceName : "complete"},
            {"play_time", Time.time},
            {"device_model", SystemInfo.deviceModel},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("launch_opera", analyticsData);
    }
    
    // 입력 처리
    void Update()
    {
        // ESC 키로 전체 스킵
        if (Input.GetKeyDown(KeyCode.Escape) && enableSkipAll)
        {
            OnSkipAllClicked();
        }
    }
    
    // 공개 API
    public bool IsPlaying()
    {
        return isPlaying;
    }
    
    public int GetCurrentSequenceIndex()
    {
        return currentSequenceIndex;
    }
    
    public float GetProgress()
    {
        if (operaSequences.Length == 0) return 1f;
        return (float)currentSequenceIndex / operaSequences.Length;
    }
    
    public void PauseOpera()
    {
        if (currentSequenceCoroutine != null)
        {
            StopCoroutine(currentSequenceCoroutine);
        }
        
        if (musicSource != null)
        {
            musicSource.Pause();
        }
    }
    
    public void ResumeOpera()
    {
        if (musicSource != null)
        {
            musicSource.UnPause();
        }
        
        if (!isSequenceComplete)
        {
            StartCurrentSequence();
        }
    }
    
    void OnDestroy()
    {
        AppsInToss.OnEvent -= HandleAppsInTossEvent;
    }
}

2. 스토리 템플릿

게임 온보딩 템플릿

c#
public class GameOnboardingTemplate : MonoBehaviour
{
    [Header("온보딩 단계")]
    public OnboardingStep[] onboardingSteps;
    
    [System.Serializable]
    public class OnboardingStep
    {
        public string stepName;
        public Sprite illustrationImage;
        public string title;
        public string description;
        public bool hasInteraction;
        public UnityEngine.Events.UnityEvent onStepComplete;
    }
    
    public void SetupOnboardingOpera(LaunchOperaManager operaManager)
    {
        var sequences = new List<LaunchOperaManager.OperaSequence>();
        
        foreach (var step in onboardingSteps)
        {
            var sequence = new LaunchOperaManager.OperaSequence
            {
                sequenceName = step.stepName,
                backgroundImage = step.illustrationImage,
                title = step.title,
                dialogues = new string[] { step.description },
                requiresUserInput = step.hasInteraction,
                duration = 3f
            };
            
            sequences.Add(sequence);
        }
        
        operaManager.operaSequences = sequences.ToArray();
    }
}

브랜드 스토리 템플릿

c#
public class BrandStoryTemplate : MonoBehaviour
{
    [Header("브랜드 스토리")]
    public BrandStoryElement[] brandElements;
    
    [System.Serializable]
    public class BrandStoryElement
    {
        public string brandMessage;
        public Sprite brandVisual;
        public Color brandColor;
        public AudioClip brandMusic;
        public float displayDuration = 4f;
    }
    
    public void CreateBrandOpera(LaunchOperaManager operaManager)
    {
        var sequences = new List<LaunchOperaManager.OperaSequence>();
        
        // 토스 브랜딩 시퀀스
        var tossSequence = new LaunchOperaManager.OperaSequence
        {
            sequenceName = "TossBranding",
            title = "토스와 함께하는 게임",
            dialogues = new string[] { "토스에서 즐기는 새로운 게임 경험" },
            backgroundColor = new Color(49/255f, 130/255f, 247/255f), // 토스 블루
            duration = 2f,
            autoAdvance = true
        };
        sequences.Add(tossSequence);
        
        // 게임별 브랜드 요소들
        foreach (var element in brandElements)
        {
            var sequence = new LaunchOperaManager.OperaSequence
            {
                sequenceName = "Brand_" + element.brandMessage.Replace(" ", "_"),
                backgroundImage = element.brandVisual,
                backgroundColor = element.brandColor,
                dialogues = new string[] { element.brandMessage },
                backgroundMusic = element.brandMusic,
                duration = element.displayDuration,
                autoAdvance = true
            };
            
            sequences.Add(sequence);
        }
        
        operaManager.operaSequences = sequences.ToArray();
    }
}

3. 인터랙티브 요소

터치 인터랙션 시스템

c#
public class OperaInteractionSystem : MonoBehaviour
{
    [Header("인터랙션 설정")]
    public InteractionType[] interactions;
    
    [System.Serializable]
    public class InteractionType
    {
        public string name;
        public Vector2 screenPosition; // 0-1 범위
        public float radius = 0.1f;
        public Sprite hintIcon;
        public string hintText;
        public UnityEngine.Events.UnityEvent onInteraction;
        public ParticleSystem interactionEffect;
    }
    
    private Camera uiCamera;
    private Canvas interactionCanvas;
    
    void Start()
    {
        uiCamera = Camera.main;
        interactionCanvas = GetComponent<Canvas>();
    }
    
    public void ShowInteractionHint(InteractionType interaction)
    {
        StartCoroutine(AnimateInteractionHint(interaction));
    }
    
    IEnumerator AnimateInteractionHint(InteractionType interaction)
    {
        // 힌트 UI 생성
        var hintObject = CreateInteractionHint(interaction);
        
        // 펄스 애니메이션
        while (hintObject != null)
        {
            yield return StartCoroutine(PulseHint(hintObject.transform));
        }
    }
    
    GameObject CreateInteractionHint(InteractionType interaction)
    {
        var hintObject = new GameObject("InteractionHint");
        hintObject.transform.SetParent(interactionCanvas.transform);
        
        var rectTransform = hintObject.AddComponent<RectTransform>();
        var image = hintObject.AddComponent<Image>();
        
        // 화면 위치 설정
        Vector2 screenPos = new Vector2(
            interaction.screenPosition.x * Screen.width,
            interaction.screenPosition.y * Screen.height
        );
        
        rectTransform.anchoredPosition = screenPos;
        rectTransform.sizeDelta = Vector2.one * 100f;
        
        // 힌트 이미지 설정
        image.sprite = interaction.hintIcon;
        image.color = Color.white;
        
        return hintObject;
    }
    
    IEnumerator PulseHint(Transform hintTransform)
    {
        Vector3 originalScale = Vector3.one;
        Vector3 targetScale = Vector3.one * 1.2f;
        
        float duration = 0.8f;
        float elapsedTime = 0f;
        
        // 확대
        while (elapsedTime < duration / 2f)
        {
            elapsedTime += Time.deltaTime;
            float progress = elapsedTime / (duration / 2f);
            hintTransform.localScale = Vector3.Lerp(originalScale, targetScale, progress);
            yield return null;
        }
        
        elapsedTime = 0f;
        
        // 축소
        while (elapsedTime < duration / 2f)
        {
            elapsedTime += Time.deltaTime;
            float progress = elapsedTime / (duration / 2f);
            hintTransform.localScale = Vector3.Lerp(targetScale, originalScale, progress);
            yield return null;
        }
        
        hintTransform.localScale = originalScale;
    }
    
    void Update()
    {
        HandleTouchInput();
    }
    
    void HandleTouchInput()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);
            if (touch.phase == TouchPhase.Began)
            {
                CheckInteractionHit(touch.position);
            }
        }
        
        // 마우스 클릭도 지원 (에디터 테스트용)
        if (Input.GetMouseButtonDown(0))
        {
            CheckInteractionHit(Input.mousePosition);
        }
    }
    
    void CheckInteractionHit(Vector2 inputPosition)
    {
        foreach (var interaction in interactions)
        {
            Vector2 screenPos = new Vector2(
                interaction.screenPosition.x * Screen.width,
                interaction.screenPosition.y * Screen.height
            );
            
            float distance = Vector2.Distance(inputPosition, screenPos);
            float hitRadius = interaction.radius * Screen.width;
            
            if (distance <= hitRadius)
            {
                TriggerInteraction(interaction);
                break;
            }
        }
    }
    
    void TriggerInteraction(InteractionType interaction)
    {
        // 이벤트 실행
        interaction.onInteraction.Invoke();
        
        // 파티클 효과
        if (interaction.interactionEffect != null)
        {
            interaction.interactionEffect.Play();
        }
        
        // 성공 피드백
        StartCoroutine(ShowInteractionSuccess(interaction));
        
        Debug.Log($"인터랙션 트리거: {interaction.name}");
    }
    
    IEnumerator ShowInteractionSuccess(InteractionType interaction)
    {
        // 성공 애니메이션이나 피드백 표시
        yield return new WaitForSeconds(0.5f);
    }
}

4. 성능 최적화

오페라 성능 모니터

c#
public class OperaPerformanceMonitor : MonoBehaviour
{
    private Dictionary<string, float> sequencePerformance = new Dictionary<string, float>();
    private float operaStartTime;
    
    void Start()
    {
        AppsInToss.OnEvent += TrackOperaPerformance;
    }
    
    void TrackOperaPerformance(string eventName, Dictionary<string, object> data)
    {
        if (eventName == "opera_started")
        {
            operaStartTime = Time.realtimeSinceStartup;
        }
        else if (eventName == "sequence_completed")
        {
            int sequenceIndex = (int)data["sequence_index"];
            float duration = Time.realtimeSinceStartup - operaStartTime;
            
            string sequenceName = $"sequence_{sequenceIndex}";
            sequencePerformance[sequenceName] = duration;
            
            // 메모리 사용량 체크
            CheckMemoryUsage(sequenceName);
        }
        else if (eventName == "opera_completed")
        {
            GeneratePerformanceReport();
        }
    }
    
    void CheckMemoryUsage(string sequenceName)
    {
        long memoryUsage = System.GC.GetTotalMemory(false);
        
        var memoryData = new Dictionary<string, object>
        {
            {"sequence_name", sequenceName},
            {"memory_usage_mb", memoryUsage / (1024f * 1024f)},
            {"timestamp", Time.realtimeSinceStartup}
        };
        
        AppsInToss.SendAnalytics("opera_memory_usage", memoryData);
    }
    
    void GeneratePerformanceReport()
    {
        float totalDuration = Time.realtimeSinceStartup - operaStartTime;
        
        var report = new Dictionary<string, object>
        {
            {"total_duration", totalDuration},
            {"sequence_performance", sequencePerformance},
            {"device_model", SystemInfo.deviceModel},
            {"graphics_memory", SystemInfo.graphicsMemorySize},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("opera_performance_report", report);
        
        Debug.Log($"런치 오페라 성능 보고서: 총 {totalDuration:F2}초 소요");
    }
    
    void OnDestroy()
    {
        AppsInToss.OnEvent -= TrackOperaPerformance;
    }
}

런치 오페라는 첫인상이 결정되는 중요한 순간이에요.
사용자의 감정적 몰입을 이끌어내면서도 건너뛸 수 있는 선택권을 제공하여 다양한 사용자 취향을 배려하세요.