[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 24회차 미션 시작합니다.

03. Firebase 네트워크 - 02, 03 챕터를 진행합니다.




Firebase 설정하기

1. Firebase에 가입하기

기본 Spark 무료 요금제이며, 실시간 데이터베이스의 사용량이 늘어나면 사용 제한이 걸리는 방식. Blaze 유료 종량제도 10G 까지는 과금되지 않음.




2. 프로젝트 추가하기.

콘솔로 이동 - 사용중인 프로젝트 목록이 나옵니다.

프로젝트를 "FastCampusDev"라는 이름으로 생성합니다. Google 애널리틱스 구성도 사용으로 하고, 다른 것들은 기본으로 하여 생성합니다.





3. 앱 추가하기: Unity -> Android

관리 메인 화면에서 "Unity" 앱 생성을 클릭하여 사용할 앱을 등록합니다.

"[v] Register as Android app"을 선택하고 Android 패키지 이름을 "comfastcampusdev.firebase"라고 작성합니다.
앱 닉네임은 "FastCampus Android Firebase"라고 작성합니다.





4. 구성 파일 다운로드

Firebase 콘솔에서도 다시 다운로드 가능합니다. 다운로드 파일을 Unity의 "Assets"에 적용하라는 안내도 보여주고 있네요.

 


google-services.json 파일을 다운로드 하고 편집기에서 열어서 살펴봅니다.


firebase_url이 중요합니다. firebase_url을 이용하여 database나 cloud storage등을 접근하여 사용하게 됩니다.
storage_bucket은 cloud storage에 파일 접근을 할 수 있게 해주는 주소입니다. 밑에 내용들은 Mobile에서 사용되는 Client 관련 정보라고 보면 됩니다.



해당 파일을 Unity Project의 "Assets"에 추가하여 줍니다.




5. Firebase SDK 다운로드

마찬가지로 Firebase Console에서 언제든 다운로드 가능합니다.




위의 그림과 같이 사용자 인증 설정하기를 진행해 주어야 합니다.

이메일/비밀번호, 전화, Google, Play게임, 게임 센터, Facebook, Twitter, GitHub, Yahoo, Microsoft, Apple, 익명 등 다양한 방법의 로그인을 제공합니다. 대단합니다. 항상 반복적으로 개발해와야 했던 내용이었 것만... 어느 반복이 너무 싫은 천재 개발자의 아이디어에서 시작된 것 같네요..

여기서는 테스트를 위해 "이메일/비밀번호" 방식을 "사용 설정"으로 변경하고 진행합니다.



Unity SDK 설치하기입니다.

Unity 상단 메뉴의 "Assets - Import Package - Custom Package..."를 클릭하여 Import 탐색기 다이알로그가 나타납니다.

여기서 아까 다운받은 firebase_unity_sdk_x.xx.x 폴더로 이동하면 dotnet3, dotnet4 폴더를 볼 수 있고, dotnet4로 진입합니다.

꼭 설치해야 하는 것은 FirebaseAnalytics입니다. 해당 기본 패키지를 설치합니다.

그리고 사용자 인증을 진행할 것이기 때문에 FirebaseAuth 패키지도 설치해줍니다.




사용자 인증 관련 주요 함수들입니다.

Singleton Pattern으로 구현되어 있기 때문에 DefaultInstance를 가져다가 사용하면 됩니다.

StreamingAssets 폴더에 google-services-desktop 파일이 생성되어 있다면 잘 된 것입니다.





Unity에서 사용할 Login UI입니다. 시간 관계상 구성된 상태로 진행합니다.

Email & Password 방식으로 로그인하고, 신규 가입이나 로그아웃 버튼 등이 추가된 상태입니다.


이제 인증을 위한 스크립트를 생성합니다.

public class FirebaseAuthController
{
    // Singletone
    private static FirebaseAuthController instance = null;
    public static FirebaseAuthController Instance {
        get {
            if (instance == null) instance = new FirebaseAuthController();
            return instance;
        }
    }
    
    private FirebaseAuth auth;
    private FirebaseUser user;
    
    private string displayName;
    private string emailAddress;
    private Uri photoUrl;
    
    public string UserId => user?.UserId ?? string.Empty;
    public string DisplayName => displayName;
    public string EmailAddress => emailAddress;
    public Uri PhotoUrl => photoUrl;
    
    public Action<bool> OnChangedLoginState;
    
    public void InitializeFirebase() {
        auth = FirebaseAuth.DefaultInstance;
        auth.StateChanged += OnAuthStateChanged;
        OnAuthStateChanged(this, null);
    }
    
    public void CreateUser(string email, string password) {
        auth.CreateUserWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
            if (task.IsCanceled) { Debug.LogError("CreateUserWithEmailAndPasswordAsync was canceled."); return; }
            if (task.IsFaulted) { // 에러 발생된 경우 처리
                Debug.LogError("CreateUserWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                int errorCode = GetFirebaseErrorCode(task.Exception);
                switch (errorCode) {
                case (int)AuthError.EmailAlreadyInUse:
                    Debug.LogError("Email Already In Use");
                    break;
                case (int)AuthError.InvalidEmail:
                    Debug.LogError("Invalid Email");
                    break;
                case (int)AuthError.WeakPassword:
                    Debug.LogError("Weak Password");
                    break;
                }
                return;
            }
            
            FirebaseUser newUser = task.Result; // 성공 결과
            Debug.LogFormat("Frebase user created successfully: {0} ({1})", newUser.DisplayName, newUser.UserId);
        });
    }
    
    public void SignIn(string email, string password) {
        auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
            if (task.IsCanceled) { Debug.LogError("SignInWithEmailAndPasswordAsync was canceled."); return; }
            if (task.IsFaulted) { // 에러 발생된 경우 처리
                Debug.LogError("SignInWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                int errorCode = GetFirebaseErrorCode(task.Exception);
                switch (errorCode) {
                case (int)AuthError.WrongPassword:
                    Debug.LogError("Wrong Password");
                    break;
                case (int)AuthError.UnverifiedEmail:
                    Debug.LogError("Unverified Email");
                    break;
                case (int)AuthError.InvalidEmail:
                    Debug.LogError("Invalid Email");
                    break;
                }
                return;
            }
            
            FirebaseUser newUser = task.Result; // 성공 결과
            Debug.LogFormat("Frebase user signed in successfully: {0} ({1})", newUser.DisplayName, newUser.UserId);
        });
    }
    
    public void SignOut() {
        auth.SignOut();
    }
    
    // Firebase의 exception id 가져오기
    private int GetFirebaseErrorCode(AggregateException exception) {
        FirebaseException firebaseException = null;
        foreach (Exception e in exception.Flatten().InnerExceptions) {
            firebaseException = e as FirebaseException; // Firebase Exception인 경우만 처리
            if (firebaseException != null) break;
        }
        return firebaseException?.ErrorCode ?? 0;
    }
   
    private void OnAuthStateChanged(object sender, EventArgs eventArgs) {
        if (auth.CurrentUser != user) {
            bool signedIn = (user != auth.CurrentUser && auth.CurrentUser != null);
            if (!signedIn && user != null) {
                Debug.Log("Signed out: " + user.UserId);
                OnChangedLoginState?.Invoke(false); // 로그아웃 이벤트 전달
            }
            
            user = auth.CurrentUser;
            if (signedIn) {
                Debug.Log("Signed in: " + user.UserId);
                displayName = user.DisplayName ?? string.Empty;
                emailAddress = user.Email ?? string.Empty;
                photoUrl = user.PhotoUrl ?? null;
                OnChangedLoginState?.Invoke(true); // 로그인 이벤트 전달
            }
        }
    }
}



public class FirebaseAuthUIController : MonoBehaviour
{
    public TMP_InputField emailInputField;
    public TMP_InputField passwordInputField;
    public TMP_Text outputText;
    
    void Start() {
        FirebaseAuthController.Instance.OnChangedLoginState += OnChangedLoginState;
        FirebaseAuthController.Instance.InitializeFirebase();
    }
    
    public void CreateUser() {
        string email = emailInputField.text;
        string password = passwordInputField.text;
        FirebaseAuthController.Instance.CreateUser(email, password);
    }
    
    public void SignIn() {
        string email = emailInputField.text;
        string password = passwordInputField.text;
        FirebaseAuthController.Instance.SignIn(email, password);
    }
    
    public void SignOut() {
        FirebaseAuthController.Instance.SignOut();
    }
    
    private void OnChangedLoginState(bool signedIn) {
        outputText.text = signedIn ? "Signed in: " : "Signed out: ";
        outputText.text += FirebaseAuthController.Instance.UserId;
    }
}





Unity Canvas에서 위와 같이 설정해 줍니다.




Unity 플레이를 진행하여 시작합니다.

eMail과 Password를 입력하고 "Create User"를 클릭하면 "Signed in: qFNEYxxxxxxxx"라는 사용자 아이디를 볼 수가 있는데, Hash 암호화가 되어 있기 때문에 특수 문자열처럼 표시되었습니다.



Firebase Console로 접근하여 보면 생성된 계정 정보를 확인할 수 있습니다.

그리고 동일 Email 생성 에러 및 암호 에러 등의 테스트를 진행해보면 Firebase를 통한 로그인 시스템이 정상적으로 잘 동작함을 확인할 수 있습니다.

 

회원 가입 및 로그인 등의 시스템이 간단하던 시절에는 개발시 문제가 되지 않았지만 지금처럼 연동 로그인이나 보안 등 복잡한 시스템이 되어가면서는 간단하지 않은 시스템이 되어 고민하게되는 부분이 많게 되는데, 이처럼 자동화되니 참 좋은 세상이네요. 물론 사용량에 따라 돈을 내야할 수 있겠지만 말이지요. ㅎㅎ





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 23회차 미션 시작합니다.

02. 디아블로 게임 - 41 챕터를 마무리하고,
03. Firebase 네트워크 - 01 챕터를 새롭게 시작합니다.





디아블로 게임의 마지막입니다. 문과 함정을 구현합니다.
스크립터블을 사용한 이벤트 오브젝트를 설명하기 위해 조금 복잡하더라도 이 구조를 선택하였습니다.

DoorTriggerArea는 문 근처 영역에 있을 때 트리거를 발생시킵니다.
DoorController는 문 애니메이션을 보여주는 GameObject입니다.



public class DoorController_New : MonoBehaviour
{
    public DoorEventObject_New doorEventObject;
    public int id = 0;
    public float openOffset = 4f; // 문이 열렸을 때 위로 올라가는 위치
    public float closeOffset = 1f;
    
    private void OnEnable() {
        doorEventObject.OnOpenDoor += OnOpenDoor;
        doorEventObject.OnCloseDoor += OnCloseDoor;
    }
    
    private void OnDisable() {
        doorEventObject.OnOpenDoor -= OnOpenDoor;
        doorEventObject.OnCloseDoor -= OnCloseDoor;
    }
    
    public void OnOpenDoor(int id) {
        if (id != this.id) return;
        StopAllCoroutines();
        StartCoroutine(OpenDoor());
    }
    
    public void OnOpenDoor(int id) {
        if (id != this.id) return;
        StopAllCoroutines();
        StartCoroutine(CloseDoor());
    }
    
    public IEnumerator OpenDoor() { // 애니메이션
        while (transform.position.y < openOffset) {
            Vector3 calcPosition = transform.position;
            calcPosition.y += 0.01;
            transform.position = calcPosition;
            yield return null;
        }
    }
    
    public IEnumerator CloseDoor() {
        while (transform.position.y < closeOffset) {
            Vector3 calcPosition = transform.position;
            calcPosition.y -= 0.01;
            transform.position = calcPosition;
            yield return null;
        }
    }
}




[CreateAssetMenu(fileName = "Event System", menuName = "Event System/Door Event Object_New")]
public class DoorEventObject_New : ScriptableObject
{
    [NonSerialized]
    public Action<int> OnOpenDoor;
    [NonSerialized]
    public Action<int> OnCloseDoor;
    
    public void OpenDoor(int id) {
        OnOpenDoor?.Invoke();
    }
    
    public void CloseDoor(int id) {
        OnCloseDoor?.Invoke();
    }
}




public class DoorTriggerArea_New : MonoBehaviour
{
    public DoorEventObject_New doorEventObject;
    public DoorController_New doorController;
    public bool autoClose = true;
    
    private void OnTriggerEnter(Collider other) {
        doorEventObject.OpenDoor(doorController.id);
    }
    
    private void OnTriggerExit(Collider other) {
        doorEventObject.CloseDoor(doorController.id);
    }
}





주황색 박스를 문이라고 설정한 상태입니다. 내부 Mesh Obstacle로 지정. DoorController_New를 추가.




유니티를 플레이하여 2개의 박스가 문처럼 잘 동작하는 것을 확인할 수 있으며,
AutoClose를 false로 하여 테스트하면 문이 열리고 다시 닫히지 않는 오브젝트로 잘 동작하는 것을 확인할 수 있습니다.



함정은 해당 GameObject가 Collider가 발생하였을 때 Character에 Damage를 발생시키는 간단한 로직입니다.


public class TrapController_New : MonoBehaviour
{
    public float damageInterval = 0.5f; // 몇초간격 반복 데미지 interval 시간
    public float damageDuration = 5f; // 몇초간 대기
    public int damage = 5;
    private float calcDuration = 0.0f;
    
    [SerializeField]
    private ParticleSystem effect;
    private IDamagable damagable;
    
    private void Update() {
        if (damagable != null) calcDuration -= Time.deltaTime;
    }
    
    private void OnTriggerEnter(Collider other) {
        damagable = other.GetComponent<IDamagable>();
        if (damagable != null) {
            calcDuration = damageDuration;
            effect.Play();
            StartCoroutine(ProcessDamage();
        }
    }
    
    private void OnTriggerExit(Collider other) {
        damagable = null;
        StopAllCoroutines();
        effect.Stop();
    }
    
    IEnumerator ProcessDamage() {
        while (calcDuration > 0 && damagable != null) {
            damagable.TakeDamage(damage, null);
            yield return new WaitForSeconds(damageInterval);
        }
        
        damagable = null;
        effect.Stop();
    }
}




함정에 대한 Unity 설정입니다.





플레이를 해봅니다. 초록색 박스 Collider에 진입하면 1초마다 데미지를 입게 됩니다. 5초후 꺼지고 다시 데미지를 발생합니다.


여기까지 RPG 액션 게임을 구현하는 방법에 대한 중요한 시스템에 대한 설명이 완료되었습니다.
강의를 위한 자료 특히 설명해야할 것이 많은 자료는 정리하여 준비하기가 정말 힘든데.. 잘 정리해 주셨네요.
자료 준비하느라 힘드셨을 듯합니다. 감사합니다.










이제 Firebase에 대한 강의를 시작합니다.
Firebase backend system과 NoSQL 등 실제 게임 제작시에 어떻게 구성하고 활용하는지 교육합니다.
김영민 강사님이 지속 알려주시네요 ^^~





+ Firebase 소개: Firebase에 대해 설명을 하고, 특히 게임 내에서 사용되는 부분을 집중하여 안내할 예정.
+ Firebase 설정
+ 사용자 인증 구현
+ 사용자 순위(leaderboard) 구현 - Real-time 게임 순위
+ 사용자 데이터 저장/불러오기



 

 


Firebase는 BaaS를 제공하는 스타트업 회사로 2014년도에 구글에 인수되었다고 합니다. 와우.. 대단합니다.
게다가 Twitter의 Febric이라는 BaaS 시스템도 구글에 인수되어 Firebase에 통합되고 있다고 합니다..

서버 개발이 필요없다니(???) 와우.. 이..럴..수..가..있..나..?
사용하는 방법을 진짜 봐야겠네요.. Firebase 설정만 하면 된다는 것 같은데 말이죠.. 획기적이네요.
C++ API도 제공된다고 합니다.

단점도 알아봐야겠지요? 유니티에서 게임을 개발하는 단계에서의 단점은..
Desktop 기반의 플랫폼을 지원하지 않습니다. 음.. 저에게는 아쉽네요. 현재 Desktop 기반에서 Unity를 활용하는 것에 초점을 맞추어 보려 하고 있는데 말입니다. Unity 게임 개발 중간에 SDK를 활용해서 테스트를 할 수는 있지만 Desktop이나 MacOS용 게임을 배포하려고 하면 지원이 되지 않는 것입니다.
이를 해결하기 위한 방법으로 중간 웹서비스에 대한 API를 거쳐서 활용하는 방법이 있다고 합니다. 추후 알아봐야겠습니다.

두번째 주의점으로는 유저가 기하급수적으로 늘어나는 경우 자체 구축 서버 시스템보다 유지보수 비용이 많이 증가할 수 있는 단점이 있습니다.

하지만 Firebase를 통해서 BaaS에 대한 기본 개념을 알아보기 쉽고, 중소기업이하 소규모 개발사에서는 기간, 비용 등이 단축될 수 있기 때문에 적극 추천하는 바입니다.




+ Authentication: 기본적으로 구글 연동 로그인 시스템 등을 지원합니다.
+ Database: Realtime과 Cloud 모두 NoSQL기반. Realtime Database는 JSON 구조 저장. Cloud는 데이터를 문서 컬렉션 단위로 저장.
  Cloud의 장점은 복잡한 계층적인 데이터나 추출 쿼리의 복잡성도 해결가능함.

  대신 단순 구조 시스템은 Realtime이 빠름.
+ Cloud Functions: Javascript, Typescript 등의 처리.
+ Hosting: 웹호스팅 서비스. 인앱결제 등. 턴-방식 게임에서도 사용 가능.

+ Google Analytics: 접속 정보 통계 분석. 인앱결제 통계. 특정 스테이지 분석 등.
+ Crashlytics: 앱의 Crash 지점 분석.
+ Cloud Messaging: Push 알림
+ Remote Config:

여기까지 Firebase의 많은 기능들중 게임 개발시 도움이 될만한 내용에 대한 정리입니다.






<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 22회차 미션 시작합니다.

02 디아블로 게임 - 39. 40 챕터를 진행합니다.

 


NPC와의 대화를 위한 Dialog System 구현입니다.


* Dialogue -> DialogManager에게 내용 전달하면 -> DialogUI에 표시
+ Name - 대화 이름
+ Sentences - 대화 내용


[Serializable]
public clas Dialogue_New
{
    public string name; // NPC 이름
    
    [TextArea(3, 10)]
    public string[] sentences; // 대화문장 배열
}



public class DialogueManager_New : MonoBehaviour
{
    private static DialogueManager_New instance; // Singleton Pattern
    public static DialogManager_New Instance = >instance;
    public Text nameText;
    public Text dialogueText;
    public Animator animator = null;
    private Queue<string> sentences;
    public event Action OnStartDialogue; // 다이알로그 시작 이벤트
    public event Action OnEndDialogue; // 다이알로그 종료 이벤트
    
    private void Awake() {
        instance = this;
    }
    
    void Start() {
        sentences = new Queue<string>();
    }
    
    public void StartDialogue(Dialogue_New dialogue) {
        OnStartDialogue?.Invoke();
        animator?.SetBool("IsOpen", true);
        nameText.text = dialogue.name;
        sentences.Clear();
        foreach (string sentence in dialogue.sentences) {
            sentences.Enqueue(sentence);
        }
        DisplayNextSentence();
    }
    
    public void DisplayNextSentence() {
        if (sentences.Count == 0) { EndDialogue(); return; }
        string sentence = sentences.Dequeue();
        StopAllCoroutines();
        StartCoroutine(TypeSentence(sentence));
    }
    
    public IEnumerator TypeSentence(string sentence) {
        dialogueText.text = string.Empty;
        yield return new WaitForSeconds(0.25f); // 애니메이션 완료 대기
        foreach (char letter in sentence.ToCharArray()) {
            dialogueText.text += letter; // 하나하나 문자로 찍히는 효과
            yield return null;
        }
    }
    
    pubic void EndDialogue() {
        animator?.SetBool("IsOpen", false);
        OnEndDialogue?.Invoke();
    }
}


 



자료 구조인 Queue와 Stack에 대한 설명도 간단히 해주십니다.
Queue : First Input First Out -> 먼저 넣은 것부터 뽑아서 사용.
Stack : First Input Last Out -> 먼저 넣은 것을 제일 마지막에 사용.

 

 

 


해당 대화 구현을 위한 DialogBox GameObject Unity 구성 화면입니다.

Continue 버튼으로 다음 문장을 진행. DisplayNextSentence()를 호출합니다.


이를 사용하는 DialogueNPC를 구현합니다.

public class DialogueNPC_New : MonoBehaviour, IInteractable
{
    [Serializable]
    Dialogue dialogue;
    
    bool isStartDialogue = false;
    GameObject interactGO;
    
    [SerializeField]
    float distance = 2.0f;
    public float Distance => distance;
    
    public void Interact(GameObject other) {
        float calcDistance = Vector3.Distance(other.transform.position, transform.position);
        if (calcDistance > distance) return;
        if (isStartDialogue) return;
        interactGO = other;
        DialogueManager_New.Instance.OnEndDialogue += OnEndDialogue;
        isStartDialogue = true;
        
        DialogueManager_New.Instance.StartDialogue(dialogue);
    }
    
    public void StopInteract(GameObject other) {
        isStartDialogue = false;
    }
    
    private void OnEndDialogue() {
        StopInteract(interactGO);
    }
}


 



Unity에서의 NPC 구성 화면입니다.
Sentences 3개를 넣어서 동작 테스트를 해보도록 합니다.





대화상대를 우클릭하면 대화 상대에게 이동을 하고 distance 거리내로 다가가게 되면 StartDialogue()를 시작으로 첫번째 문장이 표시됩니다.
[Continue] 버튼을 클릭하면 다음 문장을 보여주고 마지막 문장이 완료되면 EndDialogue()로 사라지는 애니메이션이 구동되어 없어집니다.




이제 Quest System을 구현해 보도록 하겠습니다.

QuestObject는 ScriptableObject를 상속받아 구현합니다.

QuestStatus
+ None: X
+ Accepted: 퀘스트 수락
+ Completed: 퀘스트 완료
+ Rewarded: 퀘스트 보상 완료



public enum QuestType_New {
    DestroyEnemy,
    AcquireItem,
}
[Serializable]
public class Quest_New
{
    public int id;
    public QuestType_New type;
    public int targetID; // 적의 정보
    public int count; // 적의 수
    public int completedCount;
    pubic int rewardExp;
    public int rewardGold;
    public int rewardItemId;
    public string title;
    public string description;
}



pubic enum QuestStatus {
    None,
    Accepted,
    Completed,
    Rewarded,
}
[CreateAssetMenu(filename = "New Quest", menuName = "Quest System/Quests/New Quest_New")]
public class QuestObject_New : ScriptableObject
{
    public Quest_New data = new Quest_New();
    public QuestStatus_New status;
}



[CreateAssetMenu(fileName = "Quest Database", menuName = "Quest System/Quests/New Quest Database")]
public class QuestDatabaseObject_New : ScriptableObject
{
    public QuestObject_New[] questObjects;
    
    public void OnValidate() {
        for (int index = 0; index < questObjects.Length; index++) {
            questObjects[index].data.id = index;
        }
    }
}






Quest를 2가지 추가하였고, 2가지 퀘스트를 구성하였습니다.
첫번째는 적을 잡아와라, 두번째는 특정 아이템을 구해오라는 퀘스트로 구성하였습니다.



public class QuestManager_New : MonoBehaviour
{
    private static QuestManager_New instance;
    public static QuestManager_New Instance => instance;
    public QuestDatabaseObject_New questDatabase;
    public event Action<QuestObject_New> OnCompletedQuest;
    
    private void Awake() {
        instance = this;
    }
   
    public void ProcessQuest(QuestType_New type, int targetId) {
        foreach (QuestObject_New questObject in questDatabase.questObjects) {
            if (questObject.status == QuestStatus.Accepted && questObject.data.type == type &&

                questObject.data.targetId == targetId) {
                questObject.data.completedCount++;
                if (questObject.data.completedCount >= questObject.data.count) {
                    questObject.status = QuestStatus.Completed;
                    OnCompletedQuest?.Invoke(questObject);
                }
            }
        }
    }
}



public class QuestNPC_New : MonoBehaviour, IInteractable
{
    public QuestObject questObject;
    public Dialogue readyDialogue; // 퀘스트 준비
    public Dialogue acceptedDialogue; // 이미 수락된 경우
    public Dialogue completedDialogue; // 완료된 퀘스트
    bool isStartQuestDialogue = false;
    GameObject interactGO = null;
    
    void Start() {
        QuestManager_New.Instance.OnCompletedQuest += OnCompletedQuest;
    }
    
    
    // IInteractable 관련 구현은 DialogueNPC_New와 거의 유사하므로 복사하여 수정.
}


그리고 EnemyController_Range에서 TakeDamage()가 발생할 때 적이 죽는 타이밍 즉, IsAlive가 아닌 경우가 발생할 때
QuestManager.Instance.ProcessQuest(QuestType.DestroyEnemy, 0);
를 호출하여 Quest를 확인하도록 처리합니다.

마찬가지로 InventoryObject에서 AddItem()이 발생할 때 관련 퀘스트를 확인하기 위하여
QuestManager.Instance.ProcessQuest(QuestType.AcquireItem, 1);
를 호출하여 Quest를 확인하도록 처리합니다.


 



Unity에서 QuestNPC를 2개 구성하였습니다.
하나는 적을 죽일 때의 Quest이고, 하나는 아이템을 얻는 Quest입니다.




실행을 하면 적을 죽이는 퀘스트와, 아이템을 얻어야 완료되는 2가지 퀘스트가 잘 구동되는 것을 확인할 수 있습니다.

실제로는 게임을 제작할 때 기획 의도에 맞게 Quest를 구성하면 되겠지요.







<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0




 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

+ Recent posts