노마드 코더 Nomad Coders (https://www.youtube.com/@nomadcoders) 님의
Unity 6 로 횡스크롤 게임 만들기 강의가 있어서 따라해본다....
Kimchi Run 만들기
https://www.youtube.com/watch?v=A58_FWqiekI
6.1 로 따라서 만든 최종 버전: https://github.com/sgchoi5/Unity_Study_Projects/blob/main/Kimchi-Run.zip
Unzip 후에 Unity Hub 에서 Add Project 에서 Add from repository 를 이용해서 import
Installation 부터 완전 초보자 대상 강의라 자잘하게 배울 게 많으네..
Kimchi-Run Project 생성
Project Tab 에서 Assets - Scenes - SampleScene 을 main 으로 변경
Assets 폴더에 Create Folder - Sprites 생성
* Sprites 는 게임에 사용할 이미지
assets.zip 내의 파일들을 Sprites 폴더에 드래그 & 드랍
폴더 내의 저해상도 이미지들은 유니티에서 보기가 좋지 않은데,
픽셀이 보이지 않도록 최적화해주는 Unity 때문
Filter Mode 에는 세 가지가 있는데, Point 로 변경
- Point (No filter) : 픽셀 하나만 참고. 뚜렷하지만 계단현상 심함
- Bilinear : 4개 픽셀 참고. 부드럽고 자연스럽지만 약간 흐림
- Trilinear : Bilinear + Mipmap 까지 부드럽게 보간. (특히 축소할 때 더 부드러움)
--- Background
3D Object > Quad 추가 (Width / Height 만 존재)
Quad 에는 Sky 이미지를 지정해주지만, 이미지가 반복되도록 하기 위해서 Repeat 으로 변경
Repeat : 텍스처 반복
Clamp : 늘려서
Mirror : 반복될 때 뒤집힘
Mirror Once : 뒤집힘, 이후는 늘림
Per-axis :
Repeat 으로 설정한 이후에는 Materials 폴더가 생기고, 어떻게 반복할지에 대한 설정을 해줄 수가 있다
1 -> 2 로 변경 (가로를 2장으로 반복)
Offset 의 X 값을 변경해서 횡스크롤을 표현할 수 있다 (나중에 코드로 처리)
Building png 이미지의 투명 영역을 이용할 수 있도록
Building materials 의 속성을 Transparent 로 바꿔준다
Surface Type 은
- Opaque : 불투명
- Transparent : 투명
Sky, Building, Platform 순으로 보이게 하기 위해서
Transform 의 Z 값을 100, 90, 80 으로 설정한다
화면에서 카메라 background 색을 변경해서 (Sky 이미지에서 제일 위쪽 색을 스포이드로 지정)
빈 공간을 채우는 효과
--- Scrolling Script
Assets 폴더에 Scripts 폴더 만들고, Create - New - MonoBehaviour script 로 BackgroundScroll script 를 만들자
Header tag 를 이용하면 inspector 에 추가되는 public 변수에 대한 제목이 달린다
Tooltip tag 를 이용하면 Scroll Speed 에 마우스 오버하면 도움말이 출력됨
[Header("Settings")]
[Tooltip("How fast should the texture scroll?")]
public float scrollSpeed;
Mesh Renderer 에 대한 설명
Mesh Renderer 를 script 에서 조정할 수 있도록 한다
[Header("Reference")]
public MeshRenderer meshRenderer;
Inspector 에서 추가된 meshRenderer 에 드래그 앤 드랍으로 지정해준다
Sky, Buildings, Platform 에도 동일하게 적용후에
void Update()
{
meshRenderer.material.mainTextureOffset += new Vector2(scrollSpeed * Time.deltaTime, 0);
}
offset 값을 수정해서 횡스크롤이 발생하도록 처리
Update() 는 frame 단위로 호출이 되기 때문에 더해지는 값에 Time.deltaTime 을 곱해주어야 한다
Time.deltaTime : The interval in seconds from the last frame to the current one
이전 프레임에서 현재 프레임까지의 초 단위 간격으로, Time.deltaTime 을 곱해서 더해주게 되면 1초에 mainTextureOffset 값 단위로 이동하게 된다
--- Player Jump
player_run 이미지를 선택해서 유니티가 제대로 처리했는지 확인해보고, 필요시 Inspector 에서 Open Sprite Editor 를 통해서 조정이 가능
player_run 의 이미지를 드래그 & 드랍으로 Scene 에 두면, animation 으로 저장을 할 수가 있게 된다. 이후에 화면에 아주 작게 보이게 되는데, 이 때에는 Pixels Per Unit 값을 수정해서 크게 보이도록 한다
✅ 하나의 스케일 단위에 몇 개의 픽셀을 넣어야 하는 의미
player 에게 점프 효과를 주기 위해서 Rigidbody 2D component 를 추가하고,
영원히 추락하지 않도록 Box Collier 2D (물체가 서로 부딪히게 하고, 충돌을 감지)를 추가한다
platform 의 경우에는 이미 Mesh Collier componet 가 있는데 삭제하고, Box Collier 2D component 를 추가
Edit Collier 를 통해서 크기 조정 가능
유니티 매뉴얼에서의 Collider 2D 설명
When you attach a collider
2D component to a GameObject
, the collider 2D defines which area of the GameObject has collision
and can interact with other colliders in the scene
. The collider 2D is invisible, and it’s shape is about the same shape as the GameObject for more accurate collisions. You can adjust a collider’s shape and other properties in its Inspector
window properties.
바닥에 달리고 있을 때에만 점프하기 위해서 isGrounded 변수를 추가하고
아래 메소드를 통해서 바닥에 착지했을때를 처리
MonoBehaviour.OnCollisionEnter2D(Collision2D)
입력된 Collider 가 플레이어의 Collider 와 부딪히면 호출
Player.cs
public class Player : MonoBehaviour
{
[Header("Settings")]
public float jumpForce;
[Header("Reference")]
public Rigidbody2D playerRigidbody;
private bool isGrounded = true;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && isGrounded) {
playerRigidbody.AddForceY(jumpForce, ForceMode2D.Impulse);
isGrounded = false;
}
}
void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.name == "Platform") {
isGrounded = true;
}
}
}
AddForceY() 로 점프 효과
OnCollisionEnter2D 로 바닥에 착지 처리
--- Animations
Player GameObject 를 선택하고, 메뉴에서 Window - Animation - Animation 선택
(단축키 Ctrl - 6)
Play 버튼을 눌러야 설정된 image 들이 보인다.. 처음에는 안 보여..
Player Run 오른쪽에 드롭다운 버튼을 누르면 Create New Clip... 을 선택해서 새로운 Animation 을 생성할 수 있다
Player Run
Player Jump
Player Land
점프 동작을 위해서 생성한 animation 을 Window - Animation - Animator 에서 처리
Entry 는 Player 의 시작점
Game 을 실행하게 되면, Animator window 에서 실행되는 상황을 볼 수도 있다
Make Transition 을 통해서 순서를 지정
Has Exit Time 을 통해서 자동 진행 여부 결정
Parameters 에서 Trigger 나 Condition 추가를 통해서 상태 진행 조정
Assets - Animations 에서 Player Jump/Land animation 은 Loop Time 을 Uncheck 해서 반복없이 1 회만 수행하게 하고, Player Run 만 반복 수행
❗애니메이션 동작을 확인하기 위해서 게임 자체의 속도를 조정하는 방법도 있다
Edit - Project Settings - Time
Time Scale 값을 0.5 로 하게 되면 게임 플레이 속도가 절반
--- Obstacles part 1
빌딩들의 경우에는 아래 쪽 기준으로 위치할 수 있도록 Center 에서 Bottom 으로 바꿔준다
building 이미지를 추가하고, 실행시 Player 와 이미지가 겹치는 문제 해결을 위해서
Main Camera 의 Transform - Position - Z 값을 -50
Player 의 Transform - Position - Z 값을 -10 으로 설정함
❗다른 강의에서는 겹치는 경우에 Order in Layer 값을 조정했음 (크면 앞)
< 배경 빌딩 처리 방법 >
Building 하나에 Mover.cs 와 Destroyer.cs 를 component 로 추가하고
생성되면 이동하다가 화면을 나가면 파괴도록 처리
Prefabs 에 저장한 후에 복사해서 5개를 만든 후에 이미지를 다르게 지정
Empty GameObject 를 추가해서 Building Spawner 로 하고 Spawner.cs 를 component 로 추가하고, 일정 시간마다 GameObject 를 랜덤하게 생성 처리
Mover.cs
public class Mover : MonoBehaviour
{
[Header("Setting")]
public float moveSpeed = 1f;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.position += Vector3.left * moveSpeed * Time.deltaTime;
}
}
Destroyer.cs
public class Destroyer : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x < -15)
{
Destroy(gameObject);
}
}
}
Spawner.cs (Inspector 에서 gameObjects 에 빌딩 5개를 prefabs 에서 끌어와서 지정)
public class Spawner : MonoBehaviour
{
[Header("Settings")]
public float minSpawnDelay;
public float maxSpawnDelay;
[Header("Referece")]
public GameObject[] gameObjects;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
}
void Spawn() {
GameObject randomObject = gameObjects[Random.Range(0, gameObjects.Length)];
Instantiate(randomObject, transform.position, Quaternion.identity);
Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
}
}
--- Obstacles part 2
바이크 장애물도 기존에 만들어둔 Mover.cs 와 Destroyer.cs 를 재활용하고,
충돌을 위한 Collider 를 추가할때에 Polygon Collider 2D component 를 추가해서 좀 더 정확한 충돌 영역을 설정한다
❗ Physical Interaction 을 피하기 위해서 Is Trigger 를 check
할머니 장애물의 경우에도 동일하게 하고 Capsule Collider 2D componet 를 사용
배추, 마늘, 고추, 황금배추도 동일하게 처리
Enermy, Food, Golden Tag 를 각각 지정해서 Player 의 충돌처리를 준비한다
Player.cs 에 추가
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.tag == "Enermy") {
} else if (collider.gameObject.tag == "Food") {
} else if (collider.gameObject.tag == "Golden") {
}
}
--- Player Hit and Heal
Player.cs 에 생명과 충돌 처리시 해야할 코드를 넣는다
public BoxCollider2D playerCollider;
public int lives = 3;
public bool isInvincible = false;
void KillPlayer() {
playerCollider.enabled = false;
playerAnimator.enabled = false;
playerRigidbody.AddForceY(jumpForce, ForceMode2D.Impulse);
}
void Hit() {
lives -= 1;
if (lives == 0) {
KillPlayer();
}
}
void Heal() {
lives = Mathf.Min(3, lives + 1);
}
void StartInvincible() {
isInvincible = true;
Invoke("StopInvincible", 5f);
}
void StopInvincible() {
isInvincible = false;
}
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.tag == "Enermy") {
if (!isInvincible) {
Destroy(collider.gameObject);
}
Hit();
} else if (collider.gameObject.tag == "Food") {
Destroy(collider.gameObject);
Heal();
} else if (collider.gameObject.tag == "Golden") {
Destroy(collider.gameObject);
StartInvincible();
}
}
playerCollider reference 변수에 Box Collider 2D 를 할당하지 않았는데, (자주 하는 실수라던데..)
Run 해 보면 crash 가 난다거나 하지는 않고, Console 창에 Warning 이 떠 있어서 빨리 고침
--- Game Manager
GameManager 인스턴스를 게임 어느 곳에도 이용할 수 있도록 하기 위해서
아래와 같이 처리한다
Awake() 는 Start() 보다 먼저 실행
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
void Awake()
{
if (Instance == null) {
Instance = this;
}
}
Game Intro 와 Game 실행시에 동작하는 것을 GameObject 의 SetActive(true/false) 로 분리해서 처리
GameManager.cs
[Header("Reference")]
public GameObject IntroUI;
public GameObject EnemySpawner;
public GameObject FoodSpawner;
public GameObject GoldenSpawner;
// Update is called once per frame
void Update()
{
if (State == GameState.Intro && Input.GetKeyDown(KeyCode.Space)) {
State = GameState.Playing;
IntroUI.SetActive(false);
EnemySpawner.SetActive(true);
FoodSpawner.SetActive(true);
GoldenSpawner.SetActive(true);
}
}
Spawner.cs 에서는 OnStart() 대신에 OnEnable() 로 시작하도록 변
void OnEnable()
{
Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
}
Enable / Disable 로 Spawner 를 처리하기 때문에 Invoke 를 취소하는 처리도 추가 필요
void OnEnable() {
Invoke("Spawn", Random.Range(minSpawnDelay, maxSpawnDelay));
}
void OnDisable() {
CancelInvoke();
}
GameManager 생성해서 게임 상태 관리하는 것이 기본인 듯... 다른 강의에서도 GameManager 로 처리...
--- High Scores
생명 표시를 위한 Heart GameObject 생성
Heart.cs
public class Heart : MonoBehaviour
{
public Sprite OnHeart;
public Sprite OffHeart;
public int LiveNumber;
public SpriteRenderer spriteRenderer;
// Update is called once per frame
void Update()
{
if (GameManager.Instance.Lives >= LiveNumber)
{
spriteRenderer.sprite = OnHeart;
} else {
spriteRenderer.sprite = OffHeart;
}
}
}
점수를 표시하기 위해서
Canvas component 를 추가하고 아래와 같이 설정
그리고, Render Camera 에 Main Camera 를 드래그 앤 드롭 해준다.
Text Componet 추가해서 점수 표
GameManager.cs 에서 score 관련 코드 추가
Score 를
Time.time 값을 기준해서 시작과 차이로 처리
void Update()
{
if (State == GameState.Intro && Input.GetKeyDown(KeyCode.Space)) {
State = GameState.Playing;
...
PlayStartTime = Time.time;
}
float CalculateScore() {
return Time.time - PlayStartTime;
}
void SaveHighScore() {
int score = Mathf.FloorToInt(CalculateScore());
int currentHighScore = PlayerPrefs.GetInt("highScore");
if(score > currentHighScore) {
PlayerPrefs.SetInt("highScore", score);
PlayerPrefs.Save();
}
}
게임의 난이도를 위해서 속도 처리는 정적인 moveSpeed 값 대신에
GameManager 가 제공하는 값으로 처리
GameManager.cs
public float CalculateGameSpeed() {
if (State != GameState.Playing) {
return 3f;
}
float speed = 3f + (0.5f * Mathf.Floor(CalculateScore() / 10f));
return Mathf.Min(speed, 20f);
}
Mover.cs
{
transform.position += Vector3.left * GameManager.Instance.CalculateGameSpeed() * Time.deltaTime;
}
BackgroundScroll.cs 에서도 동일하게 속도를 변경
void Update()
{
meshRenderer.material.mainTextureOffset += new Vector2(scrollSpeed *
GameManager.Instance.CalculateGameSpeed() / 20 * Time.deltaTime, 0);
}
마지막으로 Web 빌드로 Publish 하는 과정으로 마침...
2 시간 넘는 강의 과정인데, 내용이 아주 알차게 되어 있어서 따라하다 보면 배우는 게 엄청 많다
추천 👍
댓글
댓글 쓰기