목차
퀴즈 게임 만들기
25.02.26
Quiz Game의 Stage 팝업을 재사용 Cell 방식으로 만들기
: 강사님의 구현방식은 3개의 Stage를 1개의 Cell로 생각 --> 실제론 Stage 1개가 Cell이고, Stage 3개를 묶어주는 하나의 타입을 새로 만듦 (배열을 활용)
└ 재사용 Cell 방식 구현
>> StagePopupPanelController.cs에 Scroll View 바인딩
※ Cell 영역 안에 있는 Cell의 개수에 따른 차이
: Cell의 위치를 지정할 때, Cell 개수가 홀수인지 짝수인지에 따라 centerIndex 값이 달라진다.
- 홀수 : Cell 개수 / 2 를 하면 centerIndex가 나온다
- 짝수 : Cell 개수 / 2 를 해도 한 쪽에 치우져 있으므로 0.5f 만큼 Index값을 빼준다.
--> centerIndex를 기준으로 그보다 작은 Index는 왼쪽에, 그보다 큰 Index는 오른쪽에 배치하면 된다.
>> Stage Cell Prefab의 Anchor과 Width, Height 조정
--> Anchor는 Alt + Shift
>> StagePopupPanelController.cs에 값 바인딩
>> Stage Popup Panel의 자식으로 빈 게임 오브젝트로 'Obejct Pool' 생성
: ObjectPool.cs 추가 및 바인딩
>> Content의 'Grid Layout Group'과 'Content Size Fitter' Component 제거
(나는 비활성화로 해두자)
>> StagePopupPanelController.cs에 값 바인딩
└ Scroll 동작 구현
>> OnValueChanged에 함수 바인딩
게임에 광고 넣기
: Admob 사용
>> 광고 플랫폼
Unity Ads
- 장점 : Unity를 위해 만들어졌기 때문에 적용이 쉽다.
- 단점 : 다른 광고 플랫폼에 비해 광고가 부족하다. --> 광고가 나오지 않는 문제 발생
Admob
- 장점 : 다른 플랫폼에 비해 광고가 많다.
- 단점 : Unity에 적용하기가 비교적 어렵다.
>> 광고 종류
- 배너 광고 : 화면 위나 아래에 계속 떠있기 때문에, 게임의 디자인을 해치고 화면이 좁게 느껴지게 만든다. 유저가 실수로 눌렀을 때 광고를 보게 되면 받는 스트레스는 덤.
- 전면 광고 : 게임 중간에 띄우는 광고로, 약간의 시간이 지나면 종료할 수 있다.
- 보상형 광고 : 일정 시간 동안 광고를 종료할 수 없는 광고, 광고를 전부 시청한 뒤에 보상을 주는 형태
>> Admob 시작하기
1. 앱 추가
2. 광고 단위 만들기
→ 배너
--> 이후 완료 (나오는 앱 ID와 광고 단위 ID는 나중에도 볼 수 있다.)
→ 전면
--> 이후 완료
→ 리워드
--> 이후 완료
└ 초기화 + 배너광고
>> SDK 설치
: Google에 'Admob Unity SDK' 검색
--> 업데이트가 될 때마다 방법이 달라지기 때문에 이 글의 방식보다 가이드를 따르자 (Github를 통해 설치)
https://developers.google.com/admob/unity/quick-start?hl=ko
시작하기 | Unity | Google for Developers
A mobile ads SDK for AdMob publishers who are building apps on Unity.
developers.google.com
1. 다운 받은 UnityPackage 설치
--> 설치할 때 팝업창이 사라졌다고 해도 끝까지 기다리기 (우측 하단에 기다리는 표시가 없어질 때까지 기다리기)
※ 에러 발생 --> 재부팅으로 해결
※ 설치 완료 시, 나오는 창
--> Yes는 2번 누름
2. 계속 뜨는 에러를 지우려면 Build Settings에서 IOS Module을 설치해야한다.
: 설치 후, 에러가 떠있는데 이는 재부팅으로 해결
3. Project Settings에서 설정하기
4. 추가 설정
--> 선택하면 나오는 창은 OK누르면 끝
5. 앱 ID 넣기
: Admob 홈페이지의 '앱 설정'에서 앱 ID를 복사하여 파일에다가 붙여넣기
>> AdmobAdsManager.cs 생성
: 가이드를 따라 작성하되, 세 가지 타입의 광고를 전부 사용할 것이기 때문에 어느 정도 조정
※ 광고가 나오는 순서
: SDK 초기화 -> (미리)광고 로드 -> (필요한 순간에)광고 표시 -> 광고 로드 -> ...
>> 빈 게임 오브젝트로 Admob Ads Manager를 생성 후, AdmobAdsManager.cs 추가
>> 추가 세팅
: Project Settings - Player - Android - Other Settings
--> 요새는 전부 64비트로 지원되기 때문에 IL2CPP로 설정 및 ARM64 체크
: Project Settings - Player - Android - Resolution and Presentation
--> 화면이 가로(왼쪽, 오른족)나 거꾸로 전환되지 않도록 체크 해제
: App Icon 설정
>> Build
빌드해서 폰으로 광고가 뜨는지 테스트 해보자
--> 광고를 클릭하면 정책 위반이니 주의하자
>> 폰에 배너 광고가 바로 안 뜨는 경우가 있다.
: 광고 단위 생성 후, 24시간 이내에는 안 뜨는 경우가 있다 (수강생님 정보)
--> 테스트 단위 ID를 사용하면 해결된다.
#if UNITY_ANDROID
private string _adUnitId = "ca-app-pub-3940256099942544/6300978111";
#elif UNITY_IPHONE
private string _adUnitId = "ca-app-pub-3940256099942544/2934735716";
#endif
└ 테스트 기기 추가
--> 수업에서 따로 진행하지 않으니, 따로 해보자
최종 코드
>> StagePopupPanelController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(PopupPanelController))]
public class StagePopupPanelController : MonoBehaviour
{
[SerializeField] private GameObject stageCellPrefab;
[SerializeField] private Transform contentTransform;
[SerializeField] private GameObject scrollView;
[SerializeField] private int cellColumnCount;
[SerializeField] private float cellWidth;
[SerializeField] private float cellHeight;
[SerializeField] private Vector2 spacing;
private ScrollRect _scrollViewScrollRect;
private RectTransform _scrollViewRectTransform;
// 화면에 나타나는 Cell 영역을 담고 있는 List
private List<(int index, StageCellButton[] stageCellButtons)> _visibleCells;
private float _previousScrollRectYValue = 1f;
private int _maxStageCount = 100; // MAX_STAGE_COUNT를 대신하여 테스트용으로 작성
private void Awake()
{
_scrollViewScrollRect = scrollView.GetComponent<ScrollRect>();
_scrollViewRectTransform = scrollView.GetComponent<RectTransform>();
}
private void Start()
{
// 타이틀 지정
GetComponent<PopupPanelController>().SetTitleText("STAGE");
// 데이터 로드
ReloadData();
#region 임시
// var lastStageIndex = 90; // UserInformations.LastStageIndex;
// var maxStageCount = 100; // Constants.MAX_STAGE_COUNT;
//
// // Stage Popup Panel을 켰을 때 바로 마지막에 클리어한 Stage로 점프
// var contentPos = 340 * (lastStageIndex / 3); // Cell Size : 300, Cell Spacing : 40, 한 줄에 Cell 3개
// contentTransform.gameObject.GetComponent<RectTransform>().DOAnchorPosY(contentPos, 1f);
//
// // Stage Cell 만들기
// for (int i = 0; i < maxStageCount; i++)
// {
// GameObject stageCellObject = Instantiate(stageCellPrefab, contentTransform);
// StageCellButton stageCellButton = stageCellObject.GetComponent<StageCellButton>();
//
// if (i < lastStageIndex)
// {
// stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Clear);
// }
// else if (i == lastStageIndex)
// {
// stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Normal);
// }
// else
// {
// stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Lock);
// }
// }
#endregion
}
private (int start, int count) GetVisibleIndexRange()
{
var visibleRect = new Rect(
_scrollViewScrollRect.content.anchoredPosition.x,
_scrollViewScrollRect.content.anchoredPosition.y,
_scrollViewRectTransform.rect.width,
_scrollViewRectTransform.rect.height);
var start = Mathf.FloorToInt(visibleRect.y / (cellHeight + spacing.y));
var visibleCount = Mathf.CeilToInt(visibleRect.height / (cellHeight + spacing.y));
start = Mathf.Max(0, start - 1); // Stage가 0 이하로 내려가지 않도록
// 버퍼 추가
// Count 값 설정
var count = Mathf.CeilToInt(_maxStageCount / cellColumnCount); // Stage가 10개라면 4개의 Cell이 나와야 하므로 올림
count = Mathf.Min(count, start + visibleCount);
return (start, count);
}
/// <summary>
/// 특정 Index가 화면에 나와야 할 Index인지 확인
/// </summary>
/// <param name="index">특정 Index</param>
/// <returns>true : 나와야 한다, false : 나오지 않아도 된다</returns>
private bool IsVisibleIndex(int index)
{
var (start, end) = GetVisibleIndexRange();
end = Mathf.Min(end, _maxStageCount - 1);
return start <= index && index <= end;
}
private StageCellButton CreateStageCellButton(int index, int row, int col)
{
var stageCellButton = ObjectPool.Instance.GetObject().GetComponent<StageCellButton>();
stageCellButton.SetStageCell(index, StageCellButton.StageCellType.Normal);
// StageCellButton 위치 지정
float centerIndex = 0;
if (cellColumnCount % 2 == 0)
{
centerIndex = cellColumnCount / 2 - 0.5f;
}
else
{
centerIndex = cellColumnCount / 2; // AI는 Mathf.Floor()를 이용하여 소수점을 버리기를 추천
}
var offset = col - centerIndex;
var x = cellWidth * offset + spacing.x * offset;
var y = -(cellHeight + spacing.y) * row;
// Cell에 위치 지정
stageCellButton.RectTransform.anchoredPosition = new Vector2(x, y);
return stageCellButton;
}
private void ReloadData()
{
// Scroll View의 Content 사이즈 조절
// _maxStageCount와 cellColumnCount가 둘다 int 이므로 _maxStageCount를 float으로 변환시켜 계산하여 올림처리
_scrollViewScrollRect.content.sizeDelta =
new Vector2(0, Mathf.CeilToInt((float)_maxStageCount / cellColumnCount) * (cellHeight + spacing.y));
// 화면에 보이는 Cell을 담고 있는 _visibleCell을 초기화
_visibleCells = new List<(int index, StageCellButton[] stageCellButtons)>();
// Cell 생성
// 1. 만들어야 하는 셀 Index 찾기
var (start, count) = GetVisibleIndexRange();
for (int i = start; i < count; i++) // Cell 영역을 하나씩 생성
{
List<StageCellButton> stageCellButtons = new List<StageCellButton>();
for (int j = 0; j < cellColumnCount; j++) // Cell 영역 안에 3개의 Cell 생성
{
var index = i * cellColumnCount + j;
if (index < _maxStageCount) // Cell 생성
{
var stageCellButton = CreateStageCellButton(index, i, j);
stageCellButtons.Add(stageCellButton);
}
}
_visibleCells.Add((i, stageCellButtons.ToArray()));
}
}
/// <summary>
/// Scroll View가 Scroll 될 때 호출되는 Method
/// </summary>
/// <param name="value">Scroll 정도</param>
public void OnValueChanged(Vector2 value)
{
if (_previousScrollRectYValue < value.y)
{
// 위로 스크롤
// 상단에 새로운 Cell을 만들 필요가 있으면 만들기
var firstRow = _visibleCells.First(); // _visibleCells[0]
var newFirstIndex = firstRow.index - 1;
if (IsVisibleIndex(newFirstIndex))
{
List<StageCellButton> stageCellButtons = new();
for (int i = 0; i < cellColumnCount; i++)
{
var index = newFirstIndex * cellColumnCount + i;
if (index < _maxStageCount)
{
var stageCellButton = CreateStageCellButton(index, newFirstIndex, i);
stageCellButtons.Add(stageCellButton);
}
}
_visibleCells.Insert(0, (newFirstIndex, stageCellButtons.ToArray()));
}
// 하단에 더 이상 보이지 않는 Cell이 있으면 제거하기
var lastRow = _visibleCells.Last(); // _visibleCells[]
if (!IsVisibleIndex(lastRow.index))
{
var stageCellButtons = lastRow.stageCellButtons;
foreach (var stageCellButton in stageCellButtons)
{
ObjectPool.Instance.ReturnObject(stageCellButton.gameObject);
}
_visibleCells.RemoveAt(_visibleCells.Count - 1);
}
}
else
{
// 아래로 스크롤
// 하단에 새로운 Cell을 만들 필요가 있으면 만들기
var lastRow = _visibleCells.Last();
var newLastIndex = lastRow.index + 1;
if (IsVisibleIndex(newLastIndex))
{
List<StageCellButton> stageCellButtons = new();
for (int i = 0; i < cellColumnCount; i++)
{
var index = newLastIndex * cellColumnCount + i;
if (index < _maxStageCount)
{
var stageCellButton = CreateStageCellButton(index, newLastIndex, i);
stageCellButtons.Add(stageCellButton);
}
}
_visibleCells.Add((newLastIndex, stageCellButtons.ToArray()));
}
// 상단에 더 이상 보이지 않는 Cell이 있으면 제거하기
var firstRow = _visibleCells.First();
if (!IsVisibleIndex(firstRow.index))
{
var stageCellButtons = firstRow.stageCellButtons;
foreach (var stageCellButton in stageCellButtons)
{
ObjectPool.Instance.ReturnObject(stageCellButton.gameObject);
}
_visibleCells.RemoveAt(0);
}
}
_previousScrollRectYValue = value.y;
}
}
>> StageCellButton.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class StageCellButton : MonoBehaviour
{
[SerializeField] private GameObject normalImageObject;
[SerializeField] private GameObject clearImageObject;
[SerializeField] private GameObject lockImageObject;
[SerializeField] private TMP_Text[] stageIndexText;
private RectTransform _rectTransform;
public RectTransform RectTransform => _rectTransform;
public enum StageCellType { Normal, Clear, Lock }
private StageCellType _stageCellType;
private int _stageIndex;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
}
public void SetStageCell(int stageIndex, StageCellType stageCellType)
{
_stageIndex = stageIndex;
_stageCellType = stageCellType;
// Stage Index 텍스트에 출력
foreach (var stageIndexText in stageIndexText)
{
var indexText = _stageIndex + 1;
stageIndexText.text = indexText.ToString();
}
// 클리어 상태에 따라 Cell 이미지 변경
switch (_stageCellType)
{
case StageCellType.Normal:
normalImageObject.SetActive(true);
clearImageObject.SetActive(false);
lockImageObject.SetActive(false);
break;
case StageCellType.Clear:
normalImageObject.SetActive(false);
clearImageObject.SetActive(true);
lockImageObject.SetActive(false);
break;
case StageCellType.Lock:
normalImageObject.SetActive(false);
clearImageObject.SetActive(false);
lockImageObject.SetActive(true);
break;
}
}
public void OnClickStageCellButton()
{
if (_stageCellType != StageCellType.Clear) return;
// TODO: _stageIndex에 해당하는 게임 시작
}
}
>> AdmobAdsManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using GoogleMobileAds.Api;
using UnityEngine;
using UnityEngine.SceneManagement;
public class AdmobAdsManager : Singleton<AdmobAdsManager>
{
#if UNITY_ANDROID
private string _bannerAdUnitId = "ca-app-pub-3940256099942544/6300978111"; // 테스트 광고 ID
#elif UNITY_IOS
private string _bannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"; // 테스트 광고 ID
#endif
private BannerView _bannerView;
private void Start()
{
// SDK 초기화
MobileAds.Initialize(initStatus =>
{
// 배너 광고 표시 --> 광고 제거 구매 여부를 확인 후 표시할지 말지 결정해야 한다.
LoadBannerAd();
});
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
#region Banner Ads
public void CreateBannerView()
{
Debug.Log("Creating banner view");
if (_bannerView != null)
{
// Banner View 소멸
_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
}
>> UserInformations.cs
: Build할 때, 다른 수강생님이 만든 레지스트리 초기화 함수 때문에 에러가 나서 주석 처리
using UnityEngine;
using UnityEditor;
public static class UserInformations
{
// 다른 수강생님이 만든 레지스트리 초기화 함수
// [MenuItem("Window/PlayerPrefs 초기화")]
// private static void ResetPrefs()
// {
// PlayerPrefs.DeleteAll();
// Debug.Log("PlayerPrefs has been reset.");
// }
private const string HEART_COUNT = "HeartCount"; // string key 값 저장
private const string LAST_STAGE_INDEX = "LastStageIndex";
// 하트 수
public static int HeartCount
{
get
{
// "HeartCount"라는 이름의 정보를 Int로 가져오는데, Default 값은 5 --> 최초로 게임이 시작되면 저장된 값이 없기 때문
return PlayerPrefs.GetInt(HEART_COUNT, 5);
}
set
{
// value 값으로 PlayerPrefs에 저장
PlayerPrefs.SetInt(HEART_COUNT, value);
}
}
// 스테이지 클리어 정보
public static int LastStageIndex
{
get { return PlayerPrefs.GetInt(LAST_STAGE_INDEX, 0); }
set { PlayerPrefs.GetInt(LAST_STAGE_INDEX, value); }
}
// 효과음 재생 여부
public static bool IsPlaySFX
{
get { return PlayerPrefs.GetInt("IsPlaySFX", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlaySFX", value ? 1 : 0); }
}
// 배경음악 재생 여부
public static bool IsPlayBGM
{
get { return PlayerPrefs.GetInt("IsPlayBGM", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlayBGM", value ? 1 : 0); }
}
}
C# 단기 교육 보강
15일차
장염 이슈로 추후 작성 예정....
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 65일차 (0) | 2025.03.05 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 62일차 (0) | 2025.02.25 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 61일차 (0) | 2025.02.25 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 60일차 (0) | 2025.02.21 |