TIL

[멋쟁이사자처럼 부트캠프 TIL회고] 02.27 : 보상형, 전면 광고

Cadi 2025. 2. 28. 05:23

오늘의 목표

 

1. 자투리 지식 

2. 보상형 광고, 전면형 광고

3. 과제 : 동영상 따라 기능 구현

광고 초기화는 한 번이면 됨. 

 

전면광고에서 중요한 점은, 광고가 닫혔을 때 다음 동작이 나와야 한다는 것이다.

 

전면 광고 테스트 코드

 

Destroy를 안해주면 댕글링 포인트로 메모리 누수가 발생할수 있기때문에 디스트로이를 해야 한다. 

 

 

 

01.  자투리 지식

 

리뷰에 관하여

  • 리뷰는 사이트마다 그 길이와 경향이 다르다. 
    예를 들어, 스팀은 길고 자세하고 모바일 게임들 같은 경우 짧다. 
  • 리뷰를 선택해서 삭제할 수 있지만 정책에 어긋나 권장되지는 않는다. 
  • 리뷰를 쓰면 보상을 주겠다 ! 는 굉장히 힘들다. 리뷰를 달았는지 달지 않았는지 파악하기 어렵기 때문이다.
    그래서 창을 껐다 키기만 해도 보상을 주는 형태가 굉장히 많다.
  • 과거에는(혹은 현재에도) 리뷰를 세탁하고, 게임을 다운로드 받아주는 업체도 있었다.
    이는 게임에서의 첫 일주일이 얼마나 중요한지 보여주는 방증이기도 하다.
    불법적인 방법 빼고 최대한 홍보하자.
  • 시도 때도 없이 리뷰를 요청하면 게임 자체의 이미지가 나빠질 수 있다. 

테스트에 관하여

  • 구글 플레이 등에 게임을 출시할 때(업데이드 포함) 항상 테스트가 있다.
  • 구글에서 권장하지 않는 기능을 따로 우회해서 넣을 수도 있지만, 권장하지 않는다. 

 

공부에 관하여

  • 처음에 공부할 때는 블로그, 유튜브 등 정제된 정보를 갖고 공부하는 것이 좋다.
  • 하지만 나중에는 조금 더 자세하고 깊은, 원본의 정보를 봐야 한다. 
  • 현재 광고 가이드는 굉장히 친절한 편이지만, 다른 가이드들은 그렇지 않을 수 있다. 포기하지 말자.

 

 

 

using System;
using System.Collections;
using System.Collections.Generic;
using GoogleMobileAds.Api;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

public class AdmobAdsManager : Singleton<AdmobAdsManager>
{
    #if UNITY_ANDROID
    private string _bannerADUnitId = "ca-app-pub-3940256099942544/6300978111";
    private string _interstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712";
    
    #elif UNITY_IOS
    private string _bannerADUnitID = "ca-app-pub-9027792654397252/3458274561";
    #endif
    
    
    private BannerView _bannerView;
    private InterstitialAd _interstitialAd;
    private void Start()
    {
        MobileAds.Initialize(initStatus =>
        {
            //여기다가 광고 제거했으면 안뜨게 함.
            LoadBannerAd();
            // 전면 광고 로드
            LoadInterstitialAd();
        });
    }
    
    protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
    }
    #region Banner Ads

    public void CreateBannerView()
    {
        Debug.Log("CreateBannerView");

        if (_bannerView != null)
        {
            //TODO: 배너 뷰 소멸
            _bannerView.Destroy();
            _bannerView = null;
        }

        _bannerView = new BannerView(_bannerADUnitId, AdSize.Banner, AdPosition.Bottom);
    }

    public void LoadBannerAd()
    {
        if (_bannerView == null)
        {
            CreateBannerView();
        }

        var adRequest = new AdRequest();
        
        _bannerView.LoadAd(adRequest);
    }

    private void RegisterBannerAdsEventHandler()
    {
        // Raised when an ad is loaded into the banner view.
        _bannerView.OnBannerAdLoaded += () =>
        {
            Debug.Log("Banner view loaded an ad with response : "
                      + _bannerView.GetResponseInfo());
        };
        // Raised when an ad fails to load into the banner view.
        _bannerView.OnBannerAdLoadFailed += (LoadAdError error) =>
        {
            Debug.LogError("Banner view failed to load an ad with error : "
                           + error);
        };
        // Raised when the ad is estimated to have earned money.
        _bannerView.OnAdPaid += (AdValue adValue) =>
        {
            Debug.Log(String.Format("Banner view paid {0} {1}.",
                adValue.Value,
                adValue.CurrencyCode));
        };
        // Raised when an impression is recorded for an ad.
        _bannerView.OnAdImpressionRecorded += () =>
        {
            Debug.Log("Banner view recorded an impression.");
        };
        // Raised when a click is recorded for an ad.
        _bannerView.OnAdClicked += () =>
        {
            Debug.Log("Banner view was clicked.");
        };
        // Raised when an ad opened full screen content.
        _bannerView.OnAdFullScreenContentOpened += () =>
        {
            Debug.Log("Banner view full screen content opened.");
        };
        // Raised when the ad closed full screen content.
        _bannerView.OnAdFullScreenContentClosed += () =>
        {
            Debug.Log("Banner view full screen content closed.");
        };  
    }
    #endregion

    #region  Interstitial ADs

    /// <summary>
    /// 전면광고준비메서드
    /// </summary>
    public void LoadInterstitialAd()
    {
        if (_interstitialAd != null)
        {
            _interstitialAd.Destroy();
            _interstitialAd = null;
        }
        Debug.Log("LoadInterstitialAd");
        
        var adRequest = new AdRequest();
        InterstitialAd.Load(_interstitialAdUnitId, adRequest, (InterstitialAd ad, LoadAdError error) =>
        {
            if (error != null || ad == null)
            {
                Debug.LogError("interstitial ad failed to load an ad" + "with error : " + error);
                return;
               
            }
            Debug.Log("Interstitial ad loaded with response : " +ad.GetResponseInfo());
            _interstitialAd = ad;
        });
        
    }

    public void ShowInterstialAd()
    {
        if (_interstitialAd != null && _interstitialAd.CanShowAd())
        {
            Debug.Log("Showing the interstitial ad.");
            _interstitialAd.Show();
        }
        else
        {
            Debug.Log("Interstitial ad is not ready yet");
        }
    }
    
    private void RegisterInterstitialAdsEventHandlers(InterstitialAd interstitialAd)
    {
        // Raised when the ad is estimated to have earned money.
        interstitialAd.OnAdPaid += (AdValue adValue) =>
        {
            Debug.Log(String.Format("Interstitial ad paid {0} {1}.",
                adValue.Value,
                adValue.CurrencyCode));
        };
        // Raised when an impression is recorded for an ad.
        interstitialAd.OnAdImpressionRecorded += () =>
        {
            Debug.Log("Interstitial ad recorded an impression.");
        };
        // Raised when a click is recorded for an ad.
        interstitialAd.OnAdClicked += () =>
        {
            Debug.Log("Interstitial ad was clicked.");
        };
        // Raised when an ad opened full screen content.
        interstitialAd.OnAdFullScreenContentOpened += () =>
        {
            Debug.Log("Interstitial ad full screen content opened.");
        };
        // Raised when the ad closed full screen content.
        interstitialAd.OnAdFullScreenContentClosed += () =>
        {
            Debug.Log("Interstitial ad full screen content closed.");
            
            //전면 광고 닫히면 다시 로드
            {
                
            }
            LoadInterstitialAd();
        };
        // Raised when the ad failed to open full screen content.
        interstitialAd.OnAdFullScreenContentFailed += (AdError error) =>
        {
            Debug.LogError("Interstitial ad failed to open full screen content " +
                           "with error : " + error);
            LoadInterstitialAd();
        };
    }
    #endregion
}

 

아직 한 번 끝나고 나면 안됨, 왜 ? RegisterEventHandler를 등록하지 않았기 때문이다.

배너 광고는 항상 화면에 떠 있는 형태이지만, 이런 전면 광고와 같은 일회용 광고는 닫았을 때

Destroy해 주지 않으면 댕글링 포인터 문제 등이 발생해 메모리 누수가 있을 수 있다. 

using System;
using System.Collections;
using System.Collections.Generic;
using GoogleMobileAds.Api;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

public class AdmobAdsManager : Singleton<AdmobAdsManager>
{
    #if UNITY_ANDROID
    private string _bannerADUnitId = "ca-app-pub-3940256099942544/6300978111";
    private string _interstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712";
    
    #elif UNITY_IOS
    private string _bannerADUnitID = "ca-app-pub-9027792654397252/3458274561";
    #endif
    
    
    private BannerView _bannerView;
    private InterstitialAd _interstitialAd;
    private void Start()
    {
        MobileAds.Initialize(initStatus =>
        {
            //여기다가 광고 제거했으면 안뜨게 함.
            LoadBannerAd();
            // 전면 광고 로드
            LoadInterstitialAd();
        });
    }
    
    protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
    }
    #region Banner Ads

    public void CreateBannerView()
    {
        Debug.Log("CreateBannerView");

        if (_bannerView != null)
        {
            //TODO: 배너 뷰 소멸
            _bannerView.Destroy();
            _bannerView = null;
        }

        _bannerView = new BannerView(_bannerADUnitId, AdSize.Banner, AdPosition.Bottom);
    }

    public void LoadBannerAd()
    {
        if (_bannerView == null)
        {
            CreateBannerView();
        }

        var adRequest = new AdRequest();
        
        _bannerView.LoadAd(adRequest);
        RegisterBannerAdsEventHandler();
    }

    private void RegisterBannerAdsEventHandler()
    {
        // Raised when an ad is loaded into the banner view.
        _bannerView.OnBannerAdLoaded += () =>
        {
            Debug.Log("Banner view loaded an ad with response : "
                      + _bannerView.GetResponseInfo());
        };
        // Raised when an ad fails to load into the banner view.
        _bannerView.OnBannerAdLoadFailed += (LoadAdError error) =>
        {
            Debug.LogError("Banner view failed to load an ad with error : "
                           + error);
        };
        // Raised when the ad is estimated to have earned money.
        _bannerView.OnAdPaid += (AdValue adValue) =>
        {
            Debug.Log(String.Format("Banner view paid {0} {1}.",
                adValue.Value,
                adValue.CurrencyCode));
        };
        // Raised when an impression is recorded for an ad.
        _bannerView.OnAdImpressionRecorded += () =>
        {
            Debug.Log("Banner view recorded an impression.");
        };
        // Raised when a click is recorded for an ad.
        _bannerView.OnAdClicked += () =>
        {
            Debug.Log("Banner view was clicked.");
        };
        // Raised when an ad opened full screen content.
        _bannerView.OnAdFullScreenContentOpened += () =>
        {
            Debug.Log("Banner view full screen content opened.");
        };
        // Raised when the ad closed full screen content.
        _bannerView.OnAdFullScreenContentClosed += () =>
        {
            Debug.Log("Banner view full screen content closed.");
        };  
    }
    #endregion

    #region  Interstitial ADs

    /// <summary>
    /// 전면광고준비메서드
    /// </summary>
    public void LoadInterstitialAd()
    {
        if (_interstitialAd != null)
        {
            _interstitialAd.Destroy();
            _interstitialAd = null;
        }
        Debug.Log("LoadInterstitialAd");
        
        var adRequest = new AdRequest();
        InterstitialAd.Load(_interstitialAdUnitId, adRequest, (InterstitialAd ad, LoadAdError error) =>
        {
            if (error != null || ad == null)
            {
                Debug.LogError("interstitial ad failed to load an ad" + "with error : " + error);
                return;
               
            }
            Debug.Log("Interstitial ad loaded with response : " +ad.GetResponseInfo());
            _interstitialAd = ad;
            RegisterInterstitialAdsEventHandlers(_interstitialAd);
        });
        
    }

    public void ShowInterstialAd()
    {
        if (_interstitialAd != null && _interstitialAd.CanShowAd())
        {
            Debug.Log("Showing the interstitial ad.");
            _interstitialAd.Show();
        }
        else
        {
            Debug.Log("Interstitial ad is not ready yet");
        }
    }
    
    private void RegisterInterstitialAdsEventHandlers(InterstitialAd interstitialAd)
    {
        // Raised when the ad is estimated to have earned money.
        interstitialAd.OnAdPaid += (AdValue adValue) =>
        {
            Debug.Log(String.Format("Interstitial ad paid {0} {1}.",
                adValue.Value,
                adValue.CurrencyCode));
        };
        // Raised when an impression is recorded for an ad.
        interstitialAd.OnAdImpressionRecorded += () =>
        {
            Debug.Log("Interstitial ad recorded an impression.");
        };
        // Raised when a click is recorded for an ad.
        interstitialAd.OnAdClicked += () =>
        {
            Debug.Log("Interstitial ad was clicked.");
        };
        // Raised when an ad opened full screen content.
        interstitialAd.OnAdFullScreenContentOpened += () =>
        {
            Debug.Log("Interstitial ad full screen content opened.");
        };
        // Raised when the ad closed full screen content.
        interstitialAd.OnAdFullScreenContentClosed += () =>
        {
            Debug.Log("Interstitial ad full screen content closed.");
            
            //전면 광고 닫히면 다시 로드
            {
                
            }
            LoadInterstitialAd();
        };
        // Raised when the ad failed to open full screen content.
        interstitialAd.OnAdFullScreenContentFailed += (AdError error) =>
        {
            Debug.LogError("Interstitial ad failed to open full screen content " +
                           "with error : " + error);
            LoadInterstitialAd();
        };
    }
    #endregion
}

 

 

잘 등록해주면 된다. 

 

 

다음은 보상형 광고이다, 다른 부분은 모두 비슷하지만 다른 점이 조금 있다. 

광고에 관한 'Reward'를 설정해 주어야 한다는 점이다. 

 

#region Rewarded Ads

    private void LoadRewardedAd()
    {
        if (_rewardedAd != null)
        {
            _rewardedAd.Destroy();
            _rewardedAd = null;
        }

        Debug.Log("Loading the rewarded ad.");
        var adRequest = new AdRequest();

        RewardedAd.Load(_rewardedAdUnitId, adRequest, (RewardedAd ad, LoadAdError error) =>
        {
            if (error != null || ad == null)
            {
                Debug.LogError("Rewarded ad failed to load an ad" + "with error : " + error);
                return;
            }

            Debug.Log("Reward ad loaded with response : " + ad.GetResponseInfo());

            _rewardedAd = ad;
            RegisterRewardedAdEventHandlers(_rewardedAd);
        });
    }
    public void ShowRewardedAd()
    {
        const string rewardMsg = "Rewarded ad rewarded the user. Type : {0}, Amount : {1}";
        if (_rewardedAd != null || _rewardedAd.CanShowAd())
        {
            _rewardedAd.Show((Reward reward) =>
            {
                Debug.Log(String.Format(rewardMsg, reward.Type, reward.Amount));
            });
        }
    }
    
    private void RegisterRewardedAdEventHandlers(RewardedAd ad)
    {
        // Raised when the ad is estimated to have earned money.
        ad.OnAdPaid += (AdValue adValue) =>
        {
            Debug.Log(String.Format("Rewarded ad paid {0} {1}.",
                adValue.Value,
                adValue.CurrencyCode));
        };
        // Raised when an impression is recorded for an ad.
        ad.OnAdImpressionRecorded += () =>
        {
            Debug.Log("Rewarded ad recorded an impression.");
        };
        // Raised when a click is recorded for an ad.
        ad.OnAdClicked += () =>
        {
            Debug.Log("Rewarded ad was clicked.");
        };
        // Raised when an ad opened full screen content.
        ad.OnAdFullScreenContentOpened += () =>
        {
            Debug.Log("Rewarded ad full screen content opened.");
        };
        // Raised when the ad closed full screen content.
        ad.OnAdFullScreenContentClosed += () =>
        {
            Debug.Log("Rewarded ad full screen content closed.");
            LoadRewardedAd();
        };
        // Raised when the ad failed to open full screen content.
        ad.OnAdFullScreenContentFailed += (AdError error) =>
        {
            Debug.LogError("Rewarded ad failed to open full screen content " +
                           "with error : " + error);
            LoadRewardedAd();
        };
    }

    #endregion
}

 

 

* 잘 안될때, adb logcat -s Unity 라고 하면 해당으로 동작된 로그 확인 가능

 

 

 

 

 

 

 

Reward를 등록해 줄 때 , 주의할 점은 위 함수는 그냥 '닫혔을 때' 동작한다는 점이다.

사용자가 광고를 끝까지 봤는지는 보장해주지 않는다. 따라서 새로운 변수를 가져와서 하트 충전을 할 지 판단해야 한다. 

(OnAdFullScreenClose)

 


03. 과제 : 동영상 따라 구현하기

 

우선 내가 부족한 기능을 나열해봤다.

 

1. 하트 전역적인 동기화 

2. 스테이지 클리어, 스테이지 시작 시 애니메이션

3. 광고 후 보상

 

 

우선 첫 번째 문제는 하트의 카운트가 게임 전체에서 동일하게 동작하지 않는다는 점이다. 

현재 하트 수를 조정하는 코드는 많지 않다. 

 

게임을 킬 때와 끌 때, UserInformation에서 하트 카운트를 가져오고 또 세팅한다.

private void Awake()
{
    heartCount = UserInformations.HeartCount;
    lastStageIndex = UserInformations.LastStageIndex;
}
private void OnApplicationQuit()
{
    Debug.Log("OnApplicationQuit!!");
    UserInformations.HeartCount = heartCount;
    UserInformations.LastStageIndex = lastStageIndex;
}

 

그리고 게임 시작 버튼과 재시작 버튼에서 하트를 줄여준다.

public void OnClickPlayButton()
{
    if (GameManager.Instance.heartCount > 0)
    {
      
        GameManager.Instance.heartCount--;

        heartPanel.RemoveHeart(()=>
        {
            var stagePanelanimation =
                Instantiate(stagePanelAni, this.gameObject.transform).GetComponent<StagePanelAni>();
            stagePanelanimation.AinPlay(()=>
            {
                GameManager.Instance.StartGame();
            }, GameManager.Instance.lastStageIndex);
        });
    
        GameObject clickedButton = EventSystem.current.currentSelectedGameObject;
        clickedButton.GetComponent<Animator>().SetTrigger("Click");
    }
    else
    {
        heartPanel.EmptyHeart();
    }
  
  
}
public void OnClickRetryQuizButton()
{
    if (GameManager.Instance.heartCount > 0)
    {
        GameManager.Instance.heartCount--;
        heartCountText.text = GameManager.Instance.heartCount.ToString();
        
        heartPanel.RemoveHeart(() =>
        {
            SetQuizCardPanelActive(QuizCardPanelType.Front, true);
        
            // 타이머 초기화 및 시작
            timer.InitTimer();
            timer.StartTimer();
        });
     
    }
    else
    {
        // 하트가 부족해서 다시도전 불가
        // TODO: 하트 부족 알림
    }
}

 

 

여기서... GameManager에 있는 하트의 수가 , 게임을 시작할 때 분명히 줄어들었는데

이후 재시작 버튼이 있는 창이 뜨면 동기화가 되어 있지 않은 현상이 있었다 .

 

문제 1 : 싱글톤 문제

 

위의 코드 자체는 간단하고, 문제가 발생했을 때 항상 UserInforamation에 있는 값으로 할당되길래

싱글톤으로 작성된 게임 매니저 문제라고 생각했다. 그래서 게임 매니저의 Awake문에 로그를 찍어 봤더니

놀랍게도 시작 버튼을 누를 때마다 새롭게 생성되었다. 

 

 

문제가 생긴 이유는 다음과 같다. 바로 게임 씬( 두 번째 씬 ) 에도 게임 매니저 객체가 하이얼아키 창에 존재했기 때문이다.

그럼 두 번째 씬으로 넘어갔을 때 두 번째 씬에 있는 게임매니저 객체의 Awake가 실행되면서 본인을 _instance로 설정하게 되고 이로 인해 새롭게 UserInformation에서 데이터를 가져오게 된다. 

인줄 알았으나 여러번의 실험 끝에 찾았다.

 

 

 base.Awake(); 로 순서를 명시해둔 것이다 .

 

지금 게임 매니저 객체의 Awake문은 위에서 보았듯이

private void Awake()
{
    heartCount = UserInformations.HeartCount;
    lastStageIndex = UserInformations.LastStageIndex;
}

 

이렇게 한 것이다. 

문제가 있다, 바로 부모 객체인 (Singleton class)의 Awake가 먼저 실행되어서, 중복되었을 때는 삭제되어야 하는데

명시적으로 순서를 정해주지 않았던 것이다. 

그래서, 새로운 씬에서 게임 매니저가 새로 생성되었을 때 부모 객체의Awake가 먼저 실행되긴 하지만, 삭제 전에 자식의 Awake가 실행되어서 불필요한 값 덮어씌우기가 발생한 것이다. 

 

 

 

해결책을 찾는데 약 세 시간 정도 걸렸다.  해결책은 다음과 같다. 

  1. Base.Awake()로 실행 순서 명시적으로 정리
  2. 중복 객체가 있는 경우 Awake() 실행을 막기
  3. 씬이 전환 될 때, 중복된 GameManager 를 갖고 있는 객체 삭제
  4. 게임 매니저 스크립트를 애초에 할당하지 않음(새로 생성 X)

 

 

 

 

다른건 시간이 없어서 못했다. 

 

* 참고