게임 디자인 접근법

순간(moment)을 디자인 하고 레벨로 확장시킨다 -> moment들을 조합하기

ex) 주변 환경 활용 moment - Fly under, Fly over, Fly through a gap(틈 사이로 날아가기), 타이밍 맞춰서 날아가기, 움직이는 플랫폼에 착륙하기, 좁은 터널을 지나가기

ex2) 이미 게임에 있는 요소 활용 - 특정 레벨에서 로켓을 느리게하기(손상된 상태 - 왼쪽 회전만 된다든지), 부스터 아이템, 더 어두운 레벨, 밝은 레벨, 카메라 가까운 레벨, 더 큰 로켓, 조작키를 반대로 하기 등등

 

어플리케이션 종료

Application.Quit();는 빌드된 상태일 때 어플리케이션을 종료하는 함수이다.

Application.Quit();

 

어플리케이션 빌드

File - BuildSettings - Build 선택

Build
build된 모습
빌드 후 플레이

 

코드로 장애물 움직이기

양 옆으로 왔다 갔다 하는 장애물을 만들 예정이다.

[SerializeField]하고 [Range( , )]를 flaot 값 앞에 붙이면 인스펙터에 슬라이더처럼 생긴다.

[Range(,)]
슬라이더

 

Sine

Sin은 펜으로 나무디스크를 돌 때 균일한 속도로 종이가 움직이며 생긴 파형처럼 생성된다.

- 주기 : x축으로 다시 같은 지점까지 올 때까지의 크기 (걸리는 시간)

- 진폭 : y축으로 중앙부터 꼭짓점까지의 크기

SIn Cos

Tau

반지름이 1인 원에서 반지름을 길이 1의 호로 나타냈을 때의 각도가 radian이다. 여기서 3.14~~radian이 ㅠ(파이)고 6.28~~radian T(타우)다.  

타우

 

Nan 오류

어떠한 값을 0으로 나누려 할 때 뜬다.

NaN

float 값을 0과 비교해야할 일이 있을 때 Mathf.Epsilon을 사용한다.

Mathf.Epsilon

 

Oscillator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Oscillator : MonoBehaviour
{
    private Vector3 startingPosition;
    
    [SerializeField] private float period = 2f;
    
    [SerializeField] private Vector3 movementVector;
    private float movementFactor;
    void Start()
    {
        startingPosition = transform.position;
        Debug.Log(startingPosition);
    }

    void Update()
    {
        //if (period <= 0) return; 
        if (period == Mathf.Epsilon) return; // float 같은 소수에 0과 비교할 때 사용 
        float cycles = Time.time / period; // 시간에 따라 계속 증가
        
        const float tau = Mathf.PI * 2; // 6.283 일정한 값
        float rawSinWave = Mathf.Sin(cycles * tau); // -1~1

        movementFactor = (rawSinWave + 1f) / 2; // 0~2 -> 0~1
        
        Vector3 offset = movementVector * movementFactor;
        transform.position = startingPosition + offset;
    }
}

 

 

플레이 영상

잘 움직이는 모습

플레이 영상

 

치트키

L을 누르면 다음 레벨로 가게, C를 누르면 충돌 효과를 안 받게 하도록 하였다.

bool 타입은 아래처럼 사용하여 꺼져있으면 키고 켜져있으면 끄게 사용 가능하다.

toggle

 

CollisionHandler.cs

using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    private bool isTransitioning = false;
    private bool collisionDisabled = false;
    
    private AudioSource _audioSource;
    private Movement _movement;
    private Collider _collider;
    
    [SerializeField] float levelLoadDelay = 1f;

    [SerializeField] private AudioClip crashSound;
    [SerializeField] private AudioClip landingSound;
    [SerializeField] private ParticleSystem crashParticle;
    [SerializeField] private ParticleSystem landingParticle;

    void Start()
    {
        _audioSource = GetComponent<AudioSource>();
        _movement = GetComponent<Movement>();
        _collider = GetComponent<Collider>();
    }

    void Update()
    {
        RespondToDebugKeys();
    }
    
    void RespondToDebugKeys()
    {
        if (Input.GetKeyDown(KeyCode.L))
        {
            LoadNextLevel();
        }
        else if (Input.GetKeyDown(KeyCode.C))
        {
            collisionDisabled = !collisionDisabled; // toggle collision (true면 false, false면 true)
        }
    }

    void OnCollisionEnter(Collision collision)
    {
        if (isTransitioning || collisionDisabled)
            return;

        switch (collision.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is firendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence();
                break;
        }
    }

    void StartCrashSequence()
    {
        isTransitioning = true;
        _audioSource.Stop();
        PlaySound(crashSound);
        crashParticle.Play();
        _movement.enabled = false;
        Invoke(nameof(ReloadLevel), levelLoadDelay);
    }

    void LoadNextLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        int nextSceneIndex = currentSceneIndex + 1;
        if (nextSceneIndex == SceneManager.sceneCountInBuildSettings)
        {
            nextSceneIndex = 0;
        }
        SceneManager.LoadScene(nextSceneIndex);
    }

    void StartSuccessSequence()
    {        
        isTransitioning = true;
        _audioSource.Stop();
        PlaySound(landingSound);
        landingParticle.Play();
        _movement.enabled = false;
        Invoke(nameof(LoadNextLevel), levelLoadDelay);
    }

    void ReloadLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentSceneIndex);
    }

    void PlaySound(AudioClip clip)
    {
        GetComponent<AudioSource>().PlayOneShot(clip);
    }


}

 

 

외부 환경 만들기

라이팅 

Main Directional Light(sun) - 위치는 상관 없고 방향이 중요하다.(그림자에 영향을 줌)

Environment Lighting : 반사광

Scene Lights : Point Light, Spot Light

- PointLight : 전구와 비슷함, 범위가 있고 가운데에서 멀어질 수록 밝기가 낮음

ex) 가로등

Point Light

- Spot Light : 극장에서의 스포트라이트와 비슷함

Spot Light

* 라이팅을 어둡게 설정하고 싶은데 씬 창에서 조작하기 불편하다 하면 조명키를 눌러주면 된다. (대신 조명 설정은 못 봄)

씬창 조명 키기

- 장애물과 출발지, 도착지에는 Point Light 캐릭터에는 스포트라이트를 두어 어디로 가는지 어디가 위험한지 직관적으로 표현한다.

캐릭터 Spot Light

Material의 Emssion을 활용하면 발광체를 만들 수도 있다.

Emission

Window - Rendering - Lighting에서 Enviroment의 Skybox Material부터 바꿔줬다.

Lighting

이후 프로젝트 창에서 Material을 하나 생성해서 Shader 탭에서 Skybox 중 Procedual으로 바꿔줬다.

Shader - Skybox - Procedual

 이 쉐이더에서 skybox 부분 요소를 정할 수 있다.

지평선이 덜 어둡게 보인다.

이후 카메라에서 Clear Flags를 Solid Color로 해주고 Background를 검정색으로 하면 뒷 배경도 어두워진다.

뒷 배경도 어두워졌다.

 

피봇 사용하기

씬에서 오브젝트를 다룰 때 Global 대신 Local을 사용하면 그 오브젝트의 피봇을 기준으로 조작할 수 있다.

피봇

Extract Method

Extract Method를 통해 기능마다 함수로 나누어 가독성을 높인다.

- 드래그 후 단축키 Ctrl + R + M *라이더 기준

Extract Method

- 이후 이름 짓기 * 나중에 F2 버튼을 통해 다시 지어도 됨 *라이더 기준

이름 짓기(동사 + 명사)

 

Movement.cs

순서를 맞추면 가독성이 높아진다. Update 안에 ProcessThrust, ProcessRotation -> 이후 순서도 ProcessThrust, ProcessRotation

- 이후에 left right stop 등

using UnityEngine;
using UnityEngine.Serialization;

public class Movement : MonoBehaviour
{
    private Rigidbody _rigid;
    private AudioSource _audioSource;
    
    [SerializeField] private float mod = 100f;
    [SerializeField] private float rotationSpeed = 1f;
    
    [SerializeField] private AudioClip mainEngine;
    [SerializeField] private ParticleSystem thrustParticle;
    [SerializeField] private ParticleSystem rightThrustParticle;
    [SerializeField] private ParticleSystem leftThrustParticle;
    
    void Start()
    {
        _rigid = GetComponent<Rigidbody>();
        _audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        ProcessThrust();
        ProcessRotation();
    }

    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            StartThrusting();
        }
        else
        {
            StopThrusting();
        }
    }
    
    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A))
        {
            RotateLeft();
        }
        else if (Input.GetKey(KeyCode.D))
        {
            RotateRight();
        }
        else
        {
            StopRotating();
        }
    }

    void StartThrusting()
    {
        _rigid.AddRelativeForce(Vector3.up * (mod * Time.deltaTime));
        if (!_audioSource.isPlaying)
        {
            _audioSource.PlayOneShot(mainEngine);
        }
        if (!thrustParticle.isPlaying)
        {
            thrustParticle.Play();
        }
    }
    
    void StopThrusting()
    {
        thrustParticle.Stop();
        _audioSource.Stop();
    }

    private void RotateLeft()
    {
        ApplyRotation(rotationSpeed);
        if (!rightThrustParticle.isPlaying)
        {
            rightThrustParticle.Play();
        }
    }
    
    private void RotateRight()
    {
        ApplyRotation(-rotationSpeed);
        if (!leftThrustParticle.isPlaying)
        {
            leftThrustParticle.Play();
        }
    }
    
    private void StopRotating()
    {
        rightThrustParticle.Stop();
        leftThrustParticle.Stop();
    }

    void ApplyRotation(float rotationThisFrame)
    {
        // rigid.constraints = (RigidbodyConstraints)((int)RigidbodyConstraints.FreezeRotationX + (int)RigidbodyConstraints.FreezeRotationY);
        // rigid.constraints = RigidbodyConstraints.FreezeRotation;
        
        _rigid.freezeRotation = true; 
        transform.Rotate(Vector3.forward * (rotationThisFrame * Time.deltaTime));
        _rigid.freezeRotation = false;
    }
    
}

파티클 시스템

Particles : Emitter에서 생성되는 것들 * 각 파티클들은 게임 개체가 아님

Particles System : 이미터와 여러 파티클들로 이루어져있고 게임 객체에 추가되는 컴포넌트 - 게임 오브젝트는 부착된 컴포넌트에 따라 유형이 정해짐 (충돌을 해결할 때 게임 개체에 추가되기도 함)

Emitter : 파티클들을 방출(Emitting)하는 물체, 공간, 혹은 지점

Module : 파티클에 추가 효과 부여

파티클 시스템
모듈

 

 

프리펩에 파티클 추가

파티클을 프리펩 인스펙터 창에서 불러올 때 프로젝트 창에서가 아닌 자식 오브젝트로부터 불러오는 게 좋다 -> 위치나 설정이 잘 안 되어 있을 수 있음

파티클 불러오기

 

좌우 부스터 로직

- 첫 if에서는 오른쪽 부스터가 켜질 때(왼쪽으로 회전할 때), 오른쪽 부스터 파티클이 플레이중이지 않을 때

- else if에서는 왼쪽 부스터가 켜질 때(오른쪽으로 회전할 때), 왼쪽 부스터 파티클이 플레이중이지 않을 때

- else에서는 아니면 다 멈추기

사이드 부스터

 

bool 변수

bool 값을 사용하여 Transition중(이미 충돌)이면 return을 반환하게 하였다.(void 함수 나가기)

bool 변수

- 충돌하는 부분과 클리어하는 부분에 isTransitioning에 true 값을 줘서 충돌을 제어한다.

- AudioSource.Stop()은 해당 오디오 소스의 재생되던 사운드가 꺼지게 하는 것이다.

bool ~ = true

 

 

프리펩

프리펩 이동

프로젝트 창에서 더블클릭 할 때 -> 프리펩만 있는 공간으로 이동

프리펩1

하이러키 창에서 >를 클릭하여 프리펩 모드로 들어갈 때 -> 뒷배경이 보이는 프리펩을 볼 수 있다.

프리펩 2

 

프리펩 특징

1. 큐브, 캡슐 등등 프리펩 하위에 그 오브젝트 타입을 생성하면 크기나 위치가 같아진다.

* 부모와는 다르게 자식의 스케일은 1로 되어있음 => 여기서 여러 문제가 발생하여 보통은 부모의 스케일은 기본 값으로 조정하고 자식에서 스케일이나 등을 조절하는 식으로 많이 만든다.

부모 프리펩
자식 프리펩

2. 자식의 위치는 부모의 상대적인 위치이다. => 부모의 위치는 자식의 피봇이 된다

부모가 자식의 피봇이 된다.

3. 부모의 피봇과 자식오브젝트들의 중심 위치가 비슷해야 어색하지 않게 회전할 수 있다.

피봇 위치 맞추기

 4. 콜라이더는 부모에서 한 번에 처리했다. -> 깔끔

자식 콜라이더 삭제
부모 콜라이더 생성

'유데미 강의 > C#과 Unity로 3D 게임 개발하기 : 부스트 프로젝트' 카테고리의 다른 글

간단한 리팩토링  (0) 2022.08.23
파티클 시스템  (0) 2022.08.23
오디오 클립  (0) 2022.08.23
Invoke 함수 사용  (0) 2022.08.12
SceneManager  (0) 2022.08.12

다중 오디오 클립

다중으로 오디오 클립을 캐싱하여 조건마다 소리를 낼 수 있다.

오디오 캐싱

CollisionHandler.cs

using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    private AudioSource _audioSource;
    private Movement _movement;
    
    [SerializeField] float levelLoadDelay = 1f;

    [SerializeField] private AudioClip crashSound;
    [SerializeField] private AudioClip landingSound;

    private void Start()
    {
        _audioSource = GetComponent<AudioSource>();
        _movement = GetComponent<Movement>();
    }

    private void OnCollisionEnter(Collision collision)
    {
        switch (collision.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is firendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence();
                break;
        }
    }

    void StartCrashSequence()
    {
        PlaySound(crashSound);
        _movement.enabled = false;
        Invoke(nameof(ReloadLevel), levelLoadDelay);
    }

    void LoadNextLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        int nextSceneIndex = currentSceneIndex + 1;
        if (nextSceneIndex == SceneManager.sceneCountInBuildSettings)
        {
            nextSceneIndex = 0;
        }
        SceneManager.LoadScene(nextSceneIndex);
    }

    void StartSuccessSequence()
    {
        PlaySound(landingSound);
        _movement.enabled = false;
        Invoke(nameof(LoadNextLevel), levelLoadDelay);
    }

    void ReloadLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentSceneIndex);
    }

    void PlaySound(AudioClip clip)
    {
        GetComponent<AudioSource>().PlayOneShot(clip);
    }
}

Movement.cs

using UnityEngine;

public class Movement : MonoBehaviour
{
    private bool isAlive;
    
    private Rigidbody _rigid;
    private AudioSource _audioSource;
    
    [SerializeField] private float mod = 100f;
    [SerializeField] private float rotationSpeed = 1f;
    
    [SerializeField] private AudioClip _mainEngine;
    void Start()
    {
        _rigid = GetComponent<Rigidbody>();
        _audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        ProcessThrust();
        ProcessRotation();
    }

    void ProcessThrust()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            _rigid.AddRelativeForce(Vector3.up * (mod * Time.deltaTime));
            if (!_audioSource.isPlaying)
                _audioSource.PlayOneShot(_mainEngine);
        }
        else
            _audioSource.Stop();
    }

    void ProcessRotation()
    {
        if (Input.GetKey(KeyCode.A))
        {
            ApplyRotation(rotationSpeed);
        }
        else if (Input.GetKey(KeyCode.D))
        {
            ApplyRotation(-rotationSpeed);
        }
    }

    private void ApplyRotation(float rotationThisFrame)
    {
        // rigid.constraints = (RigidbodyConstraints)((int)RigidbodyConstraints.FreezeRotationX + (int)RigidbodyConstraints.FreezeRotationY);
        // rigid.constraints = RigidbodyConstraints.FreezeRotation;
        
        _rigid.freezeRotation = true; 
        transform.Rotate(Vector3.forward * (rotationThisFrame * Time.deltaTime));
        _rigid.freezeRotation = false;
    }
}

 

플레이 영상

다중 오디오 클립

착지 이후에 충돌 소리가 나는 버그는 나중에 수정하도록 하자 (collision 스크립트를 끈다던지)

'유데미 강의 > C#과 Unity로 3D 게임 개발하기 : 부스트 프로젝트' 카테고리의 다른 글

파티클 시스템  (0) 2022.08.23
bool 변수로 제어하기, 로켓 꾸미기  (0) 2022.08.23
Invoke 함수 사용  (0) 2022.08.12
SceneManager  (0) 2022.08.12
Switch  (0) 2022.08.12

Invoke

Invoke 사용하기

- 메서드가 x초 동안 지연된 이후 실행되게 할 수 있음

- 문법 : Invoke("함수 이름", 지연시간);

- 장점 : 사용하고 이해하기 쉬움

- 단점 : 문자열 참조로 함수 이름을 변경하거나 동적으로 할당할 때 불편함, 코루틴에 비해 성능이 떨어짐

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    [SerializeField]
    float levelLoadDelay = 1f;
    private void OnCollisionEnter(Collision collision)
    {
        switch (collision.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is firendly");
                break;
            case "Finish":
                StartSuccessSequence();
                break;
            default:
                StartCrashSequence();
                break;
        }
    }

    // 추락 시퀀스 후 씬 전환
    void StartCrashSequence()
    {
        // 추락할 때 효과음 넣기
        // 추락할 때 파티클 넣기
        GetComponent<Movement>().enabled = false;
        Invoke("ReLoadLevel", levelLoadDelay);
    }

    void LoadNextLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        int nextSceneIndex = currentSceneIndex + 1;
        // SceneManager.sceneCountInBuildSettings 인덱스 총 갯수를 계산
        if (nextSceneIndex == SceneManager.sceneCountInBuildSettings)
        {
            nextSceneIndex = 0;
        }
        SceneManager.LoadScene(nextSceneIndex);
    }

    // 성공 시퀀스 후 씬 전환
    void StartSuccessSequence()
    {
        // 추락할 때 효과음 넣기
        // 추락할 때 파티클 넣기
        GetComponent<Movement>().enabled = false;
        Invoke("LoadNextLevel", levelLoadDelay);
    }

    void ReLoadLevel()
    {
        // 변수에 저장하는 이유는 나중에 봤을 때 코드를 해석할 시간을 줄이기 위해서이다.
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        // 현재 실행되고 있는 씬의 인덱스를 불러옴
        SceneManager.LoadScene(currentSceneIndex);
    }
}

도착 했을 때
부딪쳤을 때

 

함수명이 하는 기능을 나타내주도록 구분하는 게 중요한 것 같다

ex) StartCrashSequence는 충돌시 시퀀스에 관한 부분

ReloadLevel은 씬 재시작에 관한 부분

'유데미 강의 > C#과 Unity로 3D 게임 개발하기 : 부스트 프로젝트' 카테고리의 다른 글

bool 변수로 제어하기, 로켓 꾸미기  (0) 2022.08.23
오디오 클립  (0) 2022.08.23
SceneManager  (0) 2022.08.12
Switch  (0) 2022.08.12
유니티 오디오  (0) 2022.08.12

SceneManager

Scene들의 이동은 Build Settings의 Scenes In Build 내의 씬들을 기반으로 이동한다.

* File - Build Settings로 접근 가능하다.

- 씬은 Add Open Scenes를 눌러 현재 켜져있는 씬을 추가하거나 드래그 앤 드롭으로 에셋폴더에 있는 씬을 Scenes In Build에 가져다 넣어서 추가하면 된다.

- 우측에 숫자는 인덱스인데 0부터 시작한다. (배열의 인덱스와 비슷)

Build Settings

 

CollisionHandler.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class CollisionHandler : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        switch (collision.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is firendly");
                break;
            case "Finish":
                Debug.Log("Congrats, yo, you finished!");
                LoadNextLevel();
                break;
            case "Fuel":
                Debug.Log("You picked up fuel");
                break;
            default:
                Debug.Log("Sorry, you blew up!");
                ReLoadLevel();
                break;
        }
    }

    void LoadNextLevel()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        int nextSceneIndex = currentSceneIndex + 1;
        // SceneManager.sceneCountInBuildSettings 인덱스 총 갯수를 계산
        if (nextSceneIndex == SceneManager.sceneCountInBuildSettings)
        {
            nextSceneIndex = 0;
        }
        SceneManager.LoadScene(nextSceneIndex);
    }

    void ReLoadLevel()
    {
        // 변수에 저장하는 이유는 나중에 봤을 때 코드를 해석할 시간을 줄이기 위해서이다.
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        // 현재 실행되고 있는 씬의 인덱스를 불러옴
        SceneManager.LoadScene(currentSceneIndex);
    }
}

 

정상적으로 다음 씬으로 넘어가고

마지막 레벨에 다다랐을 때 첫 씬으로 돌아오는 것을 확인할 수 있다.

'유데미 강의 > C#과 Unity로 3D 게임 개발하기 : 부스트 프로젝트' 카테고리의 다른 글

오디오 클립  (0) 2022.08.23
Invoke 함수 사용  (0) 2022.08.12
Switch  (0) 2022.08.12
유니티 오디오  (0) 2022.08.12
소스 컨트롤  (0) 2022.08.12

Switch문

If나 Else문 같은 조건문으로 하나의 변수와 비교하여 참인 경우만 해당라인 코드를 실행한다.

Switch 문법

switch (비교할 변수)
        {
        	// 비교할 변수와 case 뒤의 변수가 같을 때 아래 코드를 실행
            // break;를 만나면 switch문 탈출
            // default는 다른 case들이 변수와 일치하지 않을 때 실행
            case valueA:
                메서드A();
                break;
            case valueB:
                메서드B();
            default:
                메서드C();
                break;
        }

 

CollisionHandler.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CollisionHandler : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        switch (collision.gameObject.tag)
        {
            case "Friendly":
                Debug.Log("This thing is firendly");
                break;
            case "Finish":
                Debug.Log("Congrats, yo, you finished!");
                break;
            case "Fuel":
                Debug.Log("You picked up fuel");
                break;
            default:
                Debug.Log("Sorry, you blew up!");
                break;
        }
    }
}

플레이

Collision Switch문

'유데미 강의 > C#과 Unity로 3D 게임 개발하기 : 부스트 프로젝트' 카테고리의 다른 글

Invoke 함수 사용  (0) 2022.08.12
SceneManager  (0) 2022.08.12
유니티 오디오  (0) 2022.08.12
소스 컨트롤  (0) 2022.08.12
오브젝트 회전과 매개변수와 Rigidbody  (0) 2022.08.04

+ Recent posts