젬스톤 서바이버 최종 리뷰

서문


졸업 작품으로써 만들기 시작한 ‘젬스톤 서바이버’가 드디어 끝이 났다. 학기 시작하기 한참 전에 졸업 프로젝트 주제를 제출해야 하는 것을 모르고있다가 부랴부랴 급하게 제출한 주제가 이 ‘젬스톤 서바이버’였다. 옛날부터 생각은 해오던 주제지만 이렇게 갑작스럽게 만들게 될 줄은 몰랐었다.
게임을 메인 프로그래머로써 아무 것도 없는 맨땅에서부터 만드는 것은 처음이고 코딩을 거의 모르는 팀원 한 명과 작업했기 때문에 사실상 1인개발로 제작해야 했는데 필요한 기능부터 하나하나 구현해가며 Bottom-up 방식으로 묶어나가니 어떻게든 게임이 구현이 되었다.
중간 정리 때부터 시간도 많이 지나고 했으니 게임이 완성된 기념으로 한번 코드 전체를 되돌아보는 시간을 가지고자 글을 작성하게 되었다. 기능 구현에 급급하게 짠 코드라 세련됨이라고는 1도 없고 상당히 지저분하지만 그런 부분을 반성하는 것 또한 글의 의의이니 스크립트를 너무 자잘한 것들은 제외한 핵심적인 파트들을 하나씩 살펴보도록 하겠다.

스크립트


Gamemanager(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class gamemanager : MonoBehaviour
{
    public static gamemanager instance=null;
    public poolmanager poolmng;
    public GameObject inventory;
    public invenmanager invenmanager;
    public playermanager player;
    public Spawner spawner;
    public Slider hpbar;
    public GameObject hp_red;
    public GameObject uimng;
    public uimanager ui;
    public int char_num;
    public float gameTime;
    public bool inv_active=false;
    public TMP_Text min_text;
    public TMP_Text sec_text; 
    public TMP_Text gold_text;
    public TMP_Text kill_text;
    public int gold=0;
    public int kill=0;
    public gemData char1;
    public gemData char2;
    public gemData char3;
    public GameObject game_over_screen;
    public GameObject clear_screen;
    public float maxGameTime = 2 * 10f; // 20�? / 5 * 60f >> 5�?
    public float health;
    public float maxhealth=1000;
    public bool isLive = true;

    void Awake() //게임 초기화 및 ui매니저 데이터 인계받음
    {
        if(instance==null) {
            instance=this;
        }
        else {
            Destroy(this);
        }
        min_text.text="00";
        sec_text.text="00";
        gold_text.text="000";
        uimng=GameObject.Find("UImanager");
        ui=uimng.GetComponent<uimanager>();
        char_num=ui.char_num;
        hp_refresh();
    }

    private void Start() {
        if(char_num==1) {
            GameObject mn=invenmanager.monoliths[0];
            weaponmanager wpmn=mn.GetComponent<weaponmanager>();
            wpmn.gems[0]=char1;
            wpmn.mono_slots[0].g=char1;
            wpmn.monolith_active();
        }
        else if(char_num==2) {
            GameObject mn=invenmanager.monoliths[0];
            weaponmanager wpmn=mn.GetComponent<weaponmanager>();
            wpmn.gems[0]=char2;
            wpmn.mono_slots[0].g=char2;
            wpmn.monolith_active();
            player.char_select2();
        }
        else if(char_num==3) {
            GameObject mn=invenmanager.monoliths[0];
            weaponmanager wpmn=mn.GetComponent<weaponmanager>();
            wpmn.gems[0]=char3;
            wpmn.mono_slots[0].g=char3;
            wpmn.monolith_active();
            player.char_select3();
        }
    }

    public void hp_refresh() {
        hpbar.value=health/maxhealth;
        if(hpbar.value==0) hp_red.SetActive(false);
    }
    

    private void Update()
    {
        gameTime += Time.deltaTime;

        //if(gameTime > maxGameTime){
           // gameTime = maxGameTime;
        //}

        int min=(int)gameTime / 60;
        int sec=((int)gameTime - min*60) % 60;

        if(sec>=60) {
            sec-=60;
        }
        if(min<10) min_text.text="0"+min.ToString();
        else min_text.text=min.ToString();

        if(sec<10) sec_text.text="0"+sec.ToString();
        else sec_text.text=sec.ToString();

        if(gold<10) gold_text.text="00"+gold.ToString();
        else if(gold<100) gold_text.text="0"+gold.ToString();
        else gold_text.text=gold.ToString();

        if(Input.GetKeyDown(KeyCode.I)) { //인벤토리 오픈 및 초기화
            inventory.SetActive(true);
            invenmanager.slot_refresh();
            hpbar.gameObject.SetActive(false);
            Time.timeScale=0;
            inv_active=true;
        }

        if(inv_active==true && Input.GetKeyDown(KeyCode.Escape)) {//인벤토리 켜져있을시 닫고 인벤이 꺼져있으면 설정창을 on
            GameObject[] monoliths=invenmanager.monoliths;
            foreach(GameObject mono in monoliths) {
                mono.GetComponent<weaponmanager>().monolith_active();
            }
            inventory.SetActive(false);
            hpbar.gameObject.SetActive(true);
            inv_active=false;
            Time.timeScale=1;
        }
    }

    public void merchant_phase() { //게임 시작 후 일정시간이 지나면 상점 페이즈를 오픈, 시간을 정지함
    //그와 동시에 현재 스테이지에 있던 모든 오브젝트를 비활성화함으로써 초기화
        foreach(List<GameObject> pool in poolmng.pools) {
            foreach(GameObject obj in pool) {
                obj.SetActive(false);
            }
        }
        spawner.stage++;
        spawner.boss_spawned=false;
        gameTime=0;
        Time.timeScale=0;
        ui.merchant_on();
    }

    public void game_over() {
        game_over_screen.SetActive(true);
        StartCoroutine(game_over_back());
    }

    public void game_clear() {
        clear_screen.SetActive(true);
        kill_text.text=kill.ToString();
        StartCoroutine(game_over_back());
    }

    IEnumerator game_over_back() {
        yield return new WaitForSeconds(4f);
        SceneManager.LoadScene("starting scene");
    }

}

게임 전체를 총괄하는 게임 매니저이다. 게임의 여러 요소를 게임을 시작할 때, 그리고 게임의 스테이지가 넘어갈 때에 초기화해주는 역할도 하기 때문에 변수가 엄청나게 많은 것을 확인할 수 있다.
게임매니저는 게임 내에서 시간과 캐릭터의 체력을 관리하고 스테이지가 넘어가는 페이즈, 게임오버, 게임 클리어 페이즈를 발동시키는 역할도 한다. 그러면서 자연스럽게 UI매니저의 역할도 겸하게 되는데 처음에는 억지로 UI와 관련된 파트는 전부 따로 제작한 UI매니저에 분담해주려고 했으나 지금와서 보면 오히려 인게임의 UI는 게임 매니저에서 관리하는 것이 훨씬 자연스럽고 코드도 지저분하지 않으며 더 편한 것 같다.


UImanager(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class uimanager : MonoBehaviour
{
    public static uimanager ui_instance=null;
    public GameObject startpannnel;
    public GameObject characterpannel;
    public GameObject charremindpannel;
    public GameObject quitremindpannel;
    public GameObject ingame_option;
    public GameObject merchant_pannel;
    public GameObject tuto_pannel;
    public GameObject main_option;
    bool in_game=false;
    public bool pause=false;
    Button fsbtn;
    Button wsbtn;
    Button backbtn;
    public int char_num;
   
   private void Awake() {
    if(ui_instance==null) {
            ui_instance=this;
            DontDestroyOnLoad(this.gameObject);
        }
    else {
        Destroy(ui_instance.gameObject);
        ui_instance=this;
        DontDestroyOnLoad(this.gameObject);
    }
   }
   public void startbutton() {
    startpannnel.SetActive(false);
    characterpannel.SetActive(true);
   }

   public void main_option_button() {
    main_option.SetActive(true);
   }
   public void main_option_back() {
    main_option.SetActive(false);
   }

   public void tuto_button() {
    tuto_pannel.SetActive(true);
   }
   public void tuto_back_button() {
    tuto_pannel.SetActive(false);
   }
   public void characterselect1() {
    char_num=1;
    charremindpannel.SetActive(true);
   }

   public void characterselect2() {
    char_num=2;
    charremindpannel.SetActive(true);
   }

   public void characterselect3() {
    char_num=3;
    charremindpannel.SetActive(true);
   }

   public void characteryes() {
    characterpannel.SetActive(false);
    startpannnel=null;
    characterpannel=null;
    charremindpannel=null;
    quitremindpannel=null;
    SceneManager.LoadScene("in-game");
    in_game=true;
   }

   public void back_to_main() {
    Time.timeScale=1;
    SceneManager.LoadScene("starting scene");
   }

   public void characterno() {
    char_num=0;
    charremindpannel.SetActive(false);
   }

   public void backtomain() {
    characterpannel.SetActive(false);
    startpannnel.SetActive(true);
   }

   public void exit_button() {
    quitremindpannel.SetActive(true);
   }

   public void exit_yes() {
    Debug.Log("exited");
    Application.Quit();
   }

   public void exit_no() {
    quitremindpannel.SetActive(false);
   }

   public void full_screen() {
    Screen.sleepTimeout = SleepTimeout.NeverSleep;
    Screen.SetResolution(1920, 1080, true);
    Debug.Log("full screen");
   }

   public void window_screen() {
    Screen.sleepTimeout = SleepTimeout.NeverSleep;
    Screen.SetResolution(1280, 720, false);
    Debug.Log("windowed");
   }

   public void merchant_on() {
    merchant_pannel=GameObject.Find("Canvas").transform.Find("merchant pannel").gameObject;
    merchant_pannel.SetActive(true);
    Time.timeScale=0;
   }

   private void Update() {
    if(in_game==true && gamemanager.instance!=null) {
        //인벤토리가 꺼져있을때 esc를 누르면 인게임 옵션 패널을 띄워줌
        if(pause==true && Input.GetKeyDown(KeyCode.Escape)) {
            ingame_option.SetActive(false);
            pause=false;
            Time.timeScale=1;
        }
        else if(gamemanager.instance.inv_active==false && Input.GetKeyDown(KeyCode.Escape)) {
            ingame_option=GameObject.Find("Canvas").transform.Find("ingame_option pannel").gameObject;
            ingame_option.SetActive(true);
            Time.timeScale=0;
            pause=true;
            fsbtn=GameObject.Find("full screen button").GetComponent<Button>();
            fsbtn.onClick.AddListener(full_screen);
            wsbtn=GameObject.Find("window screen button").GetComponent<Button>();
            wsbtn.onClick.AddListener(window_screen);
            backbtn=GameObject.Find("back button").GetComponent<Button>();
            backbtn.onClick.AddListener(back_to_main);
        }
        
    }
   }
}

메인 화면과 인게임의 여러 UI의 발동을 담당하는 UI매니저이다. 메인화면에서 인게임으로 넘어가는 동안의 캐릭터 선택, 튜토리얼 보기, 옵션창 등의 버튼에 할당되어야 하는 함수들이 있으며 DontDestroyOnLoad를 사용하여 게임이 메인화면 씬에서 인게임 씬으로 넘어가도 파괴되지 않고 인게임의 상점, 인벤토리 등의 UI를 담당하게 된다.
DontDestroyOnLoad를 사용한 이유는 게임의 기획 상 캐릭터 선택창에서 캐릭터를 선택하면 선택한 캐릭터에 맞는 시작 젬을 인게임에서 가지고 시작해야 하기 때문이다. 따라서 메인화면에서부터 씬을 넘어 인게임으로 어떤 캐릭터를 선택했는지 변수를 전달해주는 부분이 필요했고 DontDestroyOnLoad를 사용하여 해결하였다.
스크립트가 씬을 넘어감에 따라 인게임 내의 UI도 UI매니저가 전부 담당하면 좋을 것 같아 그렇게 구현하려 했지만 그렇게 하기에는 인게임 내의 꽤 많은 UI를 전부 Find 함수를 사용하여 찾아서 할당해야 했고 이는 굉장히 비효율적으로 보였다. 따라서 같은 기능을 공유하는 옵션창과 몇몇 UI만을 담당하게 되었고 인게임 내의 UI는 게임 매니저가 담당하는 것이 되었다. 지금 보면 이러한 분담이 훨씬 자연스러운 것 같다.


Invenmanager(클릭 시 접기/펼치기)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class invenmanager : MonoBehaviour
{
    // Start is called before the first frame update
    public static invenmanager inventory;
    public GameObject inv_pannel;
    public gemData[] gemlist;
    public GameObject[] monoliths;
    public GameObject[] slots;
    public int gemcount=0;


    void Awake()
    {
        if(inventory==null) {
            inventory=this;
        }
        else Destroy(this.gameObject);
    }
   
    public void inven_dead() {
        foreach(GameObject obj in monoliths) {
            weaponmanager wpmn=obj.GetComponent<weaponmanager>();
            wpmn.monolith_clear();
        }
    }

    public void slot_refresh() { // 인벤토리 슬롯을 젬 리스트와 동기화시켜줌
        for(int i=0; i<slots.Length; i++) {
            if(gemlist[i]!=null){
                slots[i].GetComponent<slot>().g=gemlist[i];
            }
            Debug.Log("slot refresh");  
        }
    }

    public void gemlist_refresh() { //인벤토리 내 젬의 위치변경 등이 있을때 리스트에도 반영해줌
        gemcount=0;
        for(int i=0; i<slots.Length; i++) {
            gemlist[i]=slots[i].GetComponent<slot>().g;
            if(gemlist[i]!=null) gemcount++;
        }
        Debug.Log("gemlist refresh");  
    }

    public void add_gem(gemData gd) { //슬롯에 여유가 있다면 젬리스트에 젬 데이터를 넣어줌
        if(gemcount<slots.Length) {
            for(int i=0; i<slots.Length; i++) {
                if(gemlist[i]==null) {
                    gemlist[i]=gd;
                    gemcount++;
                    break;
                }
            }
        }
        else {
            Debug.Log("slot full");
        }
    }

}

게임 내의 인벤토리를 관리하는 인벤토리 매니저이다. 말은 인벤토리 매니저지만 젬의 획득, 인벤토리 갱신 등 간단한 역할을 제외하면 사실상 다른 스크립트들이 인벤토리에 접근할 때 쓸 수 있는 인벤토리 구조체와 같은 역할을 한다.


Slot(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using Unity.VisualScripting;
using TMPro;

public class slot : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
    [SerializeField]
   private gemData pgem;
   public Image slot_img;
   public bool islock=false;
   public bool isfull=false;
   public bool begin_mono=false;
   public int slot_index;
   public GameObject pannel;
   public TMP_Text title;
   public TMP_Text explain;
   public TMP_Text tags;

   public gemData g { //젬 데이터가 있다면 투명화를 해제
    get {return pgem;}
    set {
        pgem=value;
        if(pgem==null) {
            slot_img.color=new Color(1,1,1,0);
            isfull=false;
        }
        else {
            isfull=true;
            slot_img.sprite=g.spr;
            slot_img.color=new Color(1,1,1,1);
        }
        
    }
   }

   void OnDisable() {
    pannel.SetActive(false);
   }

    public void OnPointerClick(PointerEventData eventData) {
        if(eventData.button==PointerEventData.InputButton.Right) {
            if(this.g!=null) {
                g=null;
                gamemanager.instance.gold+=10;
                invenmanager.inventory.gemlist_refresh();
            }
        }
    }

   public void OnPointerEnter(PointerEventData eventData) {
    //마우스 올리면 젬의 정보 패널을 띄움
        if(this.isfull) {
            pannel.SetActive(true);
            title.text=g.gem_name;
            explain.text=g.gem_explain;
            string str="";
            foreach(string s in g.tags) {
                str+=s + ",";
            }
            if(g.ispassive) {
                foreach(string s in g.required_tag) {
                    str+="<color=#800000ff><b>" + s + "</b></color>" + ",";
                }
            }
            str=str.Remove(str.Length - 1, 1);
            this.tags.text=str;
            Debug.Log("mouse enter");
        }
   }

    public void OnPointerExit(PointerEventData eventData) {
        //마우스 뗐을 때 창 사라짐
        if(pannel.activeSelf==true) {
            pannel.SetActive(false);
            Debug.Log("mouse exit");
        }
    }

   
    public void OnBeginDrag(PointerEventData eventData)
    { //슬롯에 젬이 있을시 슬롯을 클릭하면 draggedslot에 그 슬롯의 데이터를 복사해서 넘겨줌
        pannel.SetActive(false);
        if(isfull && !islock) {
            if(this.gameObject.tag=="monoslot") begin_mono=true;
            draggedslot.instance.dragslot=this;
            draggedslot.instance.dragset(slot_img);
            draggedslot.instance.transform.position=eventData.position;
        }
    }

    public void OnDrag(PointerEventData eventData)
    { //마우스 이동에 따라 draggedslot이 이동
        if(isfull && !islock) {
            draggedslot.instance.transform.position=eventData.position;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    { //드래그가 끝났을 시 처음에 클릭했던 슬롯에서 발동하는 함수
    //드래그의 종착점이 monolith인지, 다른 슬롯인지에 따라서 필요한 절차를 진행
        if(draggedslot.instance.is_monolith==true && draggedslot.instance.is_change==false && !islock) {
            this.g=null;
            invenmanager.inventory.gemlist[slot_index]=null;
            draggedslot.instance.is_monolith=false;
        }
        else if(draggedslot.instance.is_change==true && !islock) {
            Debug.Log(draggedslot.instance.change_gd);
            this.g=draggedslot.instance.change_gd;
            int idx=draggedslot.instance.change_idx;
            if(draggedslot.instance.is_monolith) {
                invenmanager.inventory.gemlist[slot_index]=draggedslot.instance.change_gd;
                draggedslot.instance.is_monolith=false;
            }
            else {
                invenmanager.inventory.gemlist[idx]=this.g;
                if(!begin_mono) invenmanager.inventory.gemlist[slot_index]=draggedslot.instance.change_gd;
            }
            draggedslot.instance.change_gd=null;
            draggedslot.instance.change_idx=-1;
            draggedslot.instance.is_change=false;
        }
        if(begin_mono && !islock) {
             foreach(GameObject mono in invenmanager.inventory.monoliths) {
                mono.GetComponent<weaponmanager>().monolith_reset();
            }
        }
        draggedslot.instance.drag_invisible(0);
        draggedslot.instance.dragslot=null;
        begin_mono=false;
        invenmanager.inventory.gemlist_refresh();
    }

    public void OnDrop(PointerEventData eventData)
    { //enddrag보다 먼저 발동하는 함수로 드래그가 끝난 위치에 있는 슬롯에서 발동
    //드래그가 끝난 위치가 monolith라면 젬데이터를 monolith로 넘겨주고 refresh
    //드래그가 끝난 위치가 다른 슬롯이라면 그 슬롯에 draggedslot의 데이터를 넘기고 슬롯의 데이터를 받아옴
        if(draggedslot.instance.dragslot!=null && this.gameObject.tag=="monoslot" && !islock) {
            if(this.g!=null) {
                draggedslot.instance.change_idx=this.slot_index;
                draggedslot.instance.change_gd=this.g;
                draggedslot.instance.is_change=true;
            }
            this.g=draggedslot.instance.dragslot.g;
            foreach(GameObject mono in invenmanager.inventory.monoliths) {
                mono.GetComponent<weaponmanager>().monolith_reset();
            }
            draggedslot.instance.is_monolith=true;
        }
        else if(draggedslot.instance.dragslot!=null && this.gameObject.tag=="slot" && !islock) {
            draggedslot.instance.change_idx=this.slot_index;
            draggedslot.instance.change_gd=this.g;
            this.g=draggedslot.instance.dragslot.g;
            draggedslot.instance.is_change=true;
        }
    }
}

인벤토리의 실질적인 기능을 담당하는 Slot 스크립트이다. 유니티에서 제공하는 EventSystem과 여러 handler들을 사용하여 드래그를 통한 슬롯 내 데이터의 교환 및 이동, 마우스를 슬롯에 올렸을 때 나타나는 슬롯 내 데이터에 대한 설명 등을 구현하였다. 타 블로그에서 본 방식인데 투명한 draggedslot이란 슬롯을 하나 더 두었다가 드래그 앤 드랍을 할 때 draggedslot에 원래 슬롯의 데이터를 복사해주어 이동시킴으로써 원 데이터에 손상이 가지 않게 하는 방식을 차용하였다.


Playermanager(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class playermanager : MonoBehaviour
{
    public invenmanager inv;
    Rigidbody2D rigid;
    public Vector2 inputvec;
    public GameObject pivot;
    SpriteRenderer spriter;
    Animator anim;
    float char_speed=0.1f;

    private void Awake() {
        rigid = GetComponent<Rigidbody2D>();
        spriter = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
    }
    
    public void char_select2() {
        anim.runtimeAnimatorController=(RuntimeAnimatorController)Resources.Load("AcPlayer2");
    }
    public void char_select3() {
        anim.runtimeAnimatorController=(RuntimeAnimatorController)Resources.Load("AcPlayer3");
    }
    void Update() { //캐릭터 이동
        inputvec.x=Input.GetAxis("Horizontal")*char_speed;
        inputvec.y=Input.GetAxis("Vertical")*char_speed;
    }

    public void player_dead() {
        char_speed=0;
        gamemanager.instance.game_over();
        invenmanager.inventory.inven_dead();
    }

    private void FixedUpdate() { //area와 함께 이동시켜줌
        rigid.MovePosition(rigid.position + inputvec);
    }


    private void OnTriggerEnter2D(Collider2D collision) { //젬과 충돌시 인벤의 젬리스트에 추가
        if(collision.gameObject.tag == "gem") {
            Debug.Log("gem");
            gemData gd = collision.gameObject.GetComponent<gem>().GemData;
            inv.add_gem(gd);
            collision.gameObject.SetActive(false);
        }
    }
    void LateUpdate(){ //걷는 애니메이션 재생
        anim.SetFloat("Speed", inputvec.magnitude);

        if (inputvec.x != 0) {
            spriter.flipX = inputvec.x < 0;


        }
    }
    void OnCollisionStay2D(Collision2D collision)
    {
        if (!gamemanager.instance.isLive)
            return;

        if(gamemanager.instance.health >=0) {
            Enemy enemy=collision.gameObject.GetComponent<Enemy>();
            gamemanager.instance.health-=enemy.damage;
            gamemanager.instance.hp_refresh();
            Debug.Log(enemy.damage);
        }
        else if(gamemanager.instance.health < 0)
        {
            for(int index=2; index < transform.childCount; index++)
            {
                transform.GetChild(index).gameObject.SetActive(false);
            }
            player_dead();
            anim.SetTrigger("Dead");
        }
    }
}

플레이어 캐릭터의 이동과 피격, 아이템 습득 등을 담당하는 플레이어 매니저이다. 캐릭터마다 다른 스프라이트를 사용하는데 캐릭터의 스프라이트를 매 애니메이션마다 바꿔줄 수 없기 때문에 캐릭터의 스프라이트는 애니메이터 컨트롤러에 묶여있다. 따라서 다른 캐릭터가 select 되면 애니메이터 컨트롤러를 교체해주어야 하는데 이것이 일반적인 inspector에서 등록해주는 방법으로는 불가능했다. 외국 포럼을 뒤져본 결과 Resources.Load를 사용하여 폴더에서 직접 불러와서 오브젝트에 붙이는 방법을 알아내어 그대로 구현하였다.
캐릭터가 젬과 충돌 시 인벤토리에 젬 데이터를 넣고, 캐릭터가 적과 충돌하면 데미지를 입는 구조이다. 캐릭터 사망 시에는 캐릭터 속도를 0으로 하여 더이상 움직일 수 없게 하고, 게임메니저에서 게임오버 페이즈를 발동하면서 인벤토리에서 자동 발사하던 공격들을 모두 정지시키도록 구현하였다.


Enemy(클릭 시 접기/펼치기)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random=UnityEngine.Random;

public class Enemy : MonoBehaviour
{
    public float speed;
    public float health;
    public float maxHealth;
    public float fireres;
    public float iceres;
    public float lightres;
    public float damage;
    public bool[] cursed=new bool[11];
    public int gold;
    public bool is_boss=false;
    public RuntimeAnimatorController[] animCon;
    public Rigidbody2D target;
    int spriteType;

    public gemspawner gemspawner;

    public GameObject gem_effect;
    Animator gem_anim;
    bool isLive = true;

    Rigidbody2D rigid;
    Collider2D coll;
    Animator anim;
    SpriteRenderer spriter;
    WaitForFixedUpdate wait;


    void Awake() //변수 할당
    {
        rigid = GetComponent<Rigidbody2D>();
        coll = GetComponent<Collider2D>();
        spriter = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
        wait = new WaitForFixedUpdate();
        gemspawner=gamemanager.instance.GetComponent<gemspawner>();
        Array.Fill(cursed,false);
    }
    void FixedUpdate()
    { //몬스터 이동
        if(!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))
            return;
        Vector2 dirVec = target.position - rigid.position;
        Vector2 nextVec = dirVec.normalized * speed * Time.fixedDeltaTime;
        rigid.MovePosition(rigid.position + nextVec);
        rigid.velocity = Vector2.zero;
    }
    void LateUpdate() 
    {
        if(!isLive)
            return;
        spriter.flipX = target.position.x < rigid.position.x;
    }
    void OnEnable() 
    {
        target = gamemanager.instance.player.GetComponent<Rigidbody2D>();
        isLive = true;
        coll.enabled = true;
        rigid.simulated = true;
        spriter.sortingOrder = 2;
        anim.SetBool("Dead", false);
        health = maxHealth;
    }

    public void Init(SpawnData data)
    { //몬스터 스탯 초기화
        Debug.Log(data.spriteType);
        spriteType = data.spriteType;
        anim.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
        damage=data.damage;
        fireres=data.fireres;
        iceres=data.iceres;
        lightres=data.lightres;
        gold=data.gold;
        is_boss=data.is_boss;
        if(is_boss) rigid.mass=50;
    }

    void OnTriggerEnter2D(Collider2D collision) 
    { //공격을 받으면 저항을 계산하여 데미지를 입음
        if (!collision.CompareTag("Bullet") && !collision.CompareTag("melee") && !collision.CompareTag("magic"))
            return;

        if(collision.CompareTag("Bullet")) {
            float dam=collision.GetComponent<projectile>().damage;
            if(collision.GetComponent<projectile>().fire) dam-=dam*(this.fireres-collision.GetComponent<projectile>().anti_fireres);
            else if(collision.GetComponent<projectile>().ice) dam-=dam*(this.iceres-collision.GetComponent<projectile>().anti_iceres);
            else if(collision.GetComponent<projectile>().lightn) dam-=dam*(this.lightres-collision.GetComponent<projectile>().anti_lightres);
            health -= dam;
            float force=collision.GetComponent<projectile>().force;
            StartCoroutine(KonckBack(force));
            audiomanager.instance.PlaySfx(audiomanager.Sfx.Range);
        }
        else if(collision.CompareTag("melee")) {
            float dam=collision.GetComponent<melee>().damage;
            if(collision.GetComponent<melee>().fire) dam-=dam*(this.fireres-collision.GetComponent<melee>().anti_fireres);
            else if(collision.GetComponent<melee>().ice) dam-=dam*(this.iceres-collision.GetComponent<melee>().anti_iceres);
            else if(collision.GetComponent<melee>().lightn) dam-=dam*(this.lightres-collision.GetComponent<melee>().anti_lightres);
            health -= dam;
            float force=collision.GetComponent<melee>().force;
            StartCoroutine(KonckBack(force));
            audiomanager.instance.PlaySfx(audiomanager.Sfx.Melee);
        }
        else if(collision.CompareTag("magic")) {
            float dam=collision.GetComponent<magic>().damage;
            if(collision.GetComponent<magic>().fire) dam-=dam*(this.fireres-collision.GetComponent<magic>().anti_fireres);
            else if(collision.GetComponent<magic>().ice) dam-=dam*(this.iceres-collision.GetComponent<magic>().anti_iceres);
            else if(collision.GetComponent<magic>().lightn) dam-=dam*(this.lightres-collision.GetComponent<magic>().anti_lightres);
            health -= dam;
            StartCoroutine(KonckBack());
        }

        if(health > 0){
            //체력이 남아있을시 hit
            anim.SetTrigger("Hit");
            audiomanager.instance.PlaySfx(audiomanager.Sfx.Hit);
        }
        else {
            //사망 과정 진행
            isLive = false;
            coll.enabled = false;
            rigid.simulated = false;
            spriter.sortingOrder = 1;
            if(this.is_boss) {
                if(this.spriteType==4) gamemanager.instance.game_clear();
                else StartCoroutine(open_merchant());
            }
            anim.SetBool("Dead", true);
            audiomanager.instance.PlaySfx(audiomanager.Sfx.Dead);
        }
    }

    IEnumerator KonckBack(float knockforce=3)
    {
        yield return wait; //애니메이션 중첩을 막기위해 유예를 줌
        Vector3 playerPos = gamemanager.instance.player.transform.position;
        Vector3 dirVec = transform.position - playerPos;
        rigid.AddForce(dirVec.normalized * knockforce, ForceMode2D.Impulse);
    }

    void Dead(){ //몬스터 사망 시 현재 위치에 젬을 떨어뜨리고 active false
        int i=Random.Range(0,11);
        if(i==0) {
        var gem=gemspawner.gem_spawn();
        gem.transform.position=this.transform.position;
        var vfx=Instantiate(gem_effect);
        gem_anim=vfx.GetComponent<Animator>();
        vfx.transform.position=this.transform.position;
        }
        gamemanager.instance.gold+=this.gold;
        gamemanager.instance.kill++;
        gameObject.SetActive(false);
    }

    IEnumerator open_merchant() {
        yield return new WaitForSeconds(1f);
        gamemanager.instance.merchant_phase();
    }
}

게임에서 플레이어를 따라다니는 Enemy 오브젝트들을 담당하는 Enemy 스크립트이다. 밑에서 다룰 Spawner 스크립트에 의해 미리 지정된 데이터를 가진 적 오브젝트들이 생성된다. 적은 플레이어의 위치를 받아 플레이어를 향해 계속해서 이동하고 melee, projectile, magic 등의 태그를 가진 플레이어의 공격과 충돌하면 데미지를 입는다.
게임의 기획 상 넉백이 크고 데미지 상승폭이 완만하며 저점이 높은 물리 공격과, 넉백이 적고 초반엔 저항에 막혀 저점이 낮은 대신 데미지 상승폭이 커 고점이 높은 원소 공격, 두 가지의 방향성으로 기획했기 때문에 데미지 계산식이 원소 공격일 때에는 각 원소 저항 퍼센티지만큼 감소한 데미지를 입도록 설계하였다.
적이 보스일 때, 즉 적의 is_boss 플래그가 true일때 사망 시 상점 페이즈가 열려 스테이지가 넘어가도록 하려 했으나, 적의 dead 애니메이션이 나오고 오브젝트가 비활성화 되는 시점이 코루틴의 발동 시점보다 빨라 제대로 작동하지 않았다. 따라서 적의 비활성화보다 상점 페이즈의 발동이 더 빠르도록 yield return new WaitForSeconds(1f); 후에 함수가 사용되도록 하였다.


Spawner(클릭 시 접기/펼치기)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random=UnityEngine.Random;

public class Spawner : MonoBehaviour
{
    public Transform[] spwanPoint;
    public SpawnData[] spawnData;
    public int stage=1;
    public int level;
    public bool boss_spawned=false;
    public Dictionary<Tuple<int,int>,int[]> dic;
    float timer;

    void Awake() 
    {
        spwanPoint = GetComponentsInChildren<Transform>();  
        dic=new Dictionary<Tuple<int,int>,int[]>();
        dic_add();  
    }

    void dic_add() {
        dic.Add(new Tuple<int, int>(1,0),new int[]{0});
        dic.Add(new Tuple<int, int>(1,1),new int[]{0,1});
        dic.Add(new Tuple<int, int>(1,2),new int[]{0,1,3});
        dic.Add(new Tuple<int, int>(2,0),new int[]{0});
        dic.Add(new Tuple<int, int>(2,1),new int[]{7,1});
        dic.Add(new Tuple<int, int>(2,2),new int[]{0,1,6});
        dic.Add(new Tuple<int, int>(3,0),new int[]{7,8});
        dic.Add(new Tuple<int, int>(3,1),new int[]{7,8,9});
        dic.Add(new Tuple<int, int>(3,2),new int[]{7,8,9,5});
        dic.Add(new Tuple<int, int>(4,0),new int[]{10,11});
        dic.Add(new Tuple<int, int>(4,1),new int[]{10,11,12});
        dic.Add(new Tuple<int, int>(4,2),new int[]{10,11,12,4});
    }

    void Update() //게임 진행 시간에 따라 게임의 스테이지 레벨이 증가
    {
        timer += Time.deltaTime;
        level = Mathf.Min(Mathf.FloorToInt(gamemanager.instance.gameTime / 120f), 2);

        if(level<spawnData.Length && timer > spawnData[level+stage/3].spawnTime)
        {
            timer = 0;
            Spawn();
        }
    }
    void Spawn() //풀에서 몬스터를 정해진 스폰포인트에서 랜덤하게 pulling
    {
        var t=new Tuple<int, int>(stage,level);
        int[] arr=dic[t]; int leng=arr.Length;
        int rand=Random.Range(0,leng);
        int idx=arr[rand];
        if(spawnData[idx].is_boss) {
            if(boss_spawned) {
                GameObject Enemy = gamemanager.instance.poolmng.pulling(2);
                Enemy.transform.position = spwanPoint[Random.Range(1, spwanPoint.Length)].position;
                Enemy.GetComponent<Enemy>().Init(spawnData[0]);
            }
            else {
                GameObject Enemy = gamemanager.instance.poolmng.pulling(2);
                Enemy.transform.position = spwanPoint[Random.Range(1, spwanPoint.Length)].position;
                Enemy.GetComponent<Enemy>().Init(spawnData[idx]);
                boss_spawned=true;
            }
        }
        else {
            GameObject Enemy = gamemanager.instance.poolmng.pulling(2);
            Enemy.transform.position = spwanPoint[Random.Range(1, spwanPoint.Length)].position;
            Enemy.GetComponent<Enemy>().Init(spawnData[idx]);
        }
    }
}

[System.Serializable]
public class SpawnData
{
    public float spawnTime;
    public int spriteType;
    public int health;
    public float speed;
    public float damage;
    public float fireres;
    public float iceres;
    public float lightres;
    public int gold;
    public bool is_boss=false;
}

적 오브젝트의 스폰을 담당하는 Spawner 스크립트이다. 게임의 기획에 따르면 게임에는 상점 페이즈로 구분되는 stage가 있고 한 stage 내에서 시간이 지날수록 올라가는 level, 두 변수가 있다. level은 시간에 따라 올라가서 적의 스폰 간격을 줄이고 더 강한 적이 나오게 하지만 stage는 보스가 사망 시에 넘어가므로 시간과 무관한 변수이다. 따라서 두 개의 변수를 key로 가진 hash 자료구조가 필요했는데 C#의 Dictionary와 Tuple 자료구조를 사용하여 구축하였다. Unity에서 Dictionary는 다른 일반적인 배열 등과 다르게 Inspector 창에서 값을 입력하는 것이 불가능하므로 Awake 시에 값을 모두 입력시키도록 하였다.
보스가 스폰된 이후엔 is_boss 플래그가 있는, 즉 보스는 더 이상 스폰되지 않도록 하였다.


Weaponmanager(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using System;
using Random=UnityEngine.Random;

public class weaponmanager : MonoBehaviour
{
    public int prefabid; //할당된 액티브 젬의 id
    public float damage; //피해
    public float delay_percent=1;
    public int element=0; //속성
    public float force=3; //넉백 강도
    public int count; //발사 수
    public int penet; //관통
    public float radius; //반경
    public float speed; //탄속
    public int gem_color; //젬 색깔
    public List<int> curse; //저주 목록
    public GameObject player; //플레이어 위치
    public GameObject pivot; //플레이어의 회전축
    public GameObject pivot1;
    public gemData[] gems; //석판의 젬 목록
    public bool active_on=false;
    public int slot_index=0; //새로 개방된 슬롯 숫자
    public slotback[] expand_slots; //아직 열리지 않은 슬롯 목록
    public slot[] mono_slots; //석판의 슬롯 목록
    public GameObject special_manager;
    Tween tween;
    Coroutine crt;
    Coroutine spcrt=null;

    void Start() {
    }

    public void slot_expand() { //새로 열린 슬롯 개수가 3이 될때까지 개방 가능
        if(slot_index<3) {
            expand_slots[slot_index].slot_active();
            slot_index++;
        }
        else Debug.Log("all slot expanded");
    }


    IEnumerator magicuse(float delay) {//count만큼 마법을 pulling하여 발동시킴
    //wave 마법의 경우 중첩되면 밸런스가 무너지므로 count를 1로 고정
        while(true) {
            yield return new WaitForSeconds(delay);
            for(int i=0; i<count; i++) {
                GameObject mag=gamemanager.instance.poolmng.pulling(prefabid);
                if(prefabid==5) count=1;
                mag.transform.position=this.transform.position;
                mag.GetComponent<magic>().init(this.prefabid,this.damage,this.radius, this.element, player.transform);
            }
        }
    }
    IEnumerator projectile(float delay)
    { //delay마다 count만큼 fire 함수를 발동
        while(true)
        {
            yield return new WaitForSeconds(delay);
            for(int i=0; i<count; i++) {
                StartCoroutine(fire());
            }
        }
    }

    IEnumerator shotgun_fire(float delay) {
        while(true)
        {
            yield return new WaitForSeconds(delay);
            float dam=damage*2/10;
            float x=1.73f;
            StartCoroutine(shotgun(x, 1.3f, dam));
            StartCoroutine(shotgun(x,-1.3f,dam));
            for(int i=0; i<count-2; i++) {
                float y=Random.Range(-1.3f,1.3f);
                StartCoroutine(shotgun(x,y,dam));
            }
        }
    }

    IEnumerator fire() {
        //캐릭터 전방 180도 범위중 랜덤으로 투사체를 발사하는 함수
        GameObject dagger = gamemanager.instance.poolmng.pulling(prefabid);
        dagger.transform.position = player.transform.position;
        dagger.GetComponent<projectile>().init(damage, penet,element, curse, force);
        float x=Random.Range(0, 30);
        if(this.prefabid==10) x=Random.Range(-30,0);
        float y=Random.Range(-30,30);
        Vector3 dir=new Vector3(x,y,0);
        dir=dir.normalized;
        dagger.transform.rotation=Quaternion.FromToRotation(Vector3.up,dir);
        Rigidbody2D rigid=dagger.GetComponent<Rigidbody2D>();
        rigid.velocity=Vector2.zero;
        rigid.velocity=dir*speed;
        yield return null;
        StartCoroutine(daggerfalse(dagger));
    }

    IEnumerator shotgun(float a, float b, float dam) {
        //캐릭터 전방 180도 범위중 랜덤으로 투사체를 발사하는 함수
        GameObject dagger = gamemanager.instance.poolmng.pulling(prefabid);
        dagger.transform.position = player.transform.position;
        dagger.GetComponent<projectile>().init(dam, penet,element, curse, force);
        dagger.transform.localScale=new Vector3(0.35f,0.35f,0.35f);
        Vector3 dir=new Vector3(a,b,0);
        dir=dir.normalized;
        dagger.transform.rotation=Quaternion.FromToRotation(Vector3.up,dir);
        Rigidbody2D rigid=dagger.GetComponent<Rigidbody2D>();
        rigid.velocity=Vector2.zero;
        rigid.velocity=dir*speed;
        yield return null;
        StartCoroutine(daggerfalse(dagger));
    }

    IEnumerator daggerfalse(GameObject used_dagger) {
        yield return new WaitForSeconds(5f);
        used_dagger.SetActive(false);
    }

    IEnumerator swing(float delay) {
        //캐릭터 전방 180도만큼 근접무기를 휘두르는 함수
        while(true) {
        yield return new WaitForSeconds(delay);
        GameObject melee=gamemanager.instance.poolmng.pulling(prefabid);
        melee.transform.parent=pivot.transform;
        melee.transform.position=pivot.transform.position+new Vector3(0,1,0);
        melee.GetComponent<melee>().init(damage, penet, curse, element, radius, force);
        tween=pivot.transform.DORotate(new Vector3(0,0,180f),0.75f)
        .SetEase(Ease.OutQuart)
        .OnKill(()=> {
            pivot.transform.localEulerAngles=new Vector3(0,0,0);
            melee.SetActive(false);
        })
        .OnComplete(()=> {
            pivot.transform.localEulerAngles=new Vector3(0,0,0);
        });
        StartCoroutine(swing_false(melee));
        }
    }

    IEnumerator whirlwind() {
        GameObject melee=gamemanager.instance.poolmng.pulling(prefabid);
        melee.transform.parent=pivot1.transform;
        melee.transform.position=pivot1.transform.position+new Vector3(0,2,0);
        melee.GetComponent<melee>().init(damage, penet, curse, element, radius, force);
        tween=pivot1.transform.DORotate(new Vector3(0,0,360),2f, RotateMode.FastBeyond360)
        .SetEase(Ease.Linear)
        .SetLoops(-1)
        .OnKill(()=> {
            pivot1.transform.localEulerAngles=new Vector3(0,0,0);
            melee.SetActive(false);
        });
        yield return null;

    }

    IEnumerator swing_false(GameObject melee) {
        yield return new WaitForSeconds(0.65f);
        melee.SetActive(false);
    }

    public void skill_use() { //gem color에 따라 종류에 맞는 함수를 발동시킴
        if(gem_color==1) {
            if(this.prefabid==8) {
                crt=StartCoroutine(shotgun_fire(2f*delay_percent));
            }
            else {
                crt=StartCoroutine(projectile(2f*delay_percent));
            }
        }
        else if(gem_color==2) {
            crt=StartCoroutine(magicuse(4f*delay_percent));
        }
        else if(gem_color==3) {
            if(this.prefabid==4) crt=StartCoroutine(swing(3f*delay_percent));
            else crt=StartCoroutine(whirlwind());
        }
    }

    public void monolith_reset() { //인벤토리에서 monolith에 젬을 장착시켰을 때
    //슬롯의 젬 데이터를 monolith로 가져오는 함수
        Debug.Log("gem set");
        for(int i=0; i<3+slot_index; i++) { //향후 3을 열린 슬롯 개수로 수정
            if(mono_slots[i].gameObject.activeSelf==true) {
                gems[i]=mono_slots[i].g;
            }
        }
    }

    public void monolith_clear() { //공격의 중복발동을 방지하기 위해 공격 발동 전에 초기화해주는 함수
        this.damage=0;
        this.count=0;
        this.prefabid=0;
        this.gem_color=0;
        this.speed=0;
        this.radius=0;
        this.penet=0;
        this.element=0;
        this.force=3;
        this.delay_percent=1;
        active_on=false;
        curse.Clear();
        tween.Kill();
        if(crt!=null) StopCoroutine(crt);
        if(spcrt!=null) special_manager.GetComponent<special>().StopCoroutine(spcrt);
    }
    public void monolith_active() {
        monolith_clear();
        //인벤토리를 끌 때 monolith가 가진 젬들을 계산하여 weaponmanager가 최종적으로 스킬을 발동함
        for(int i=0; i<gems.Length; i++) {
            if(gems[i]!=null) {
                if(gems[i].isactive && i!=0) {
                    gemData tmp=gems[0];
                    gems[0]=gems[i];
                    gems[i]=tmp;
                } 
            }
        }
        foreach(gemData gd in gems) {
            if(gd==null) continue;
            if(gd.isactive && !active_on) {
                this.damage=gd.damage;
                this.count=gd.count;
                this.prefabid=gd.id;
                this.gem_color=gd.color;
                this.speed=gd.speed;
                this.radius=gd.radius;
                this.penet=gd.penet;
                this.element=gd.element;
                this.force=gd.force;
                active_on=true;
                skill_use();
            }
            else if(gd.ispassive) {
                bool flag=true;
                foreach(string s in gd.required_tag) {
                    if(gems[0]!=null && !gems[0].tags.Contains(s)) flag=false;
                }
                if(gd.required_tag.Contains("범용")) flag=true;
                if(flag) {
                    if(gd.curse!=0) curse.Add(gd.curse);
                    this.damage+=gd.damage;
                    this.speed*=gd.speed;
                    this.radius*=gd.radius;
                    this.penet+=gd.penet;
                    this.count+=gd.count;
                    this.element=gd.element;
                    this.force+=gd.force;
                    this.delay_percent*=gd.delay_reduct;
                }
            }
            else if(gd.isspecial) {
                spcrt=special_manager.GetComponent<special>().init(this);
            }
        }
    }
}

게임의 핵심적인 부분, 각 석판에 장착된 젬을 모두 계산하여 실제 스킬을 사용하는 웨폰 매니저이다. 인벤토리를 끌 때, 상점 페이즈가 끝나고 스테이지가 넘어갈 때 등에 monolith_active 함수가 가동하여 석판에 장착된 젬들을 모두 계산한다. 계산하는 과정은 다음과 같다.
첫째로 석판에 장착된 젬들 중 액티브 젬을 배열의 첫번째로 두도록 정렬한다. 이 과정은 꼭 필요한 부분인데 그 이유는 foreach 함수가 반복을 돌 때 배열의 처음부터 순서대로 돌기 때문이다. 이 웨폰 매니저는 액티브젬과 동일한 스탯을 가지고 패시브젬의 스탯을 더한 다음 발동하게 되는데, 배열의 첫번째가 액티브 젬이 아니면 패시브 젬의 스탯이 더해진 것을 다시 액티브 젬의 스탯으로 초기화하게 되는 문제가 생긴다.
둘째로 gems 배열을 foreach로 순회하며 액티브 젬이라면 웨폰매니저의 스탯을 초기화하고 패시브 젬이라면 그에 맞는 스탯을 가산한다. 액티브 젬의 스탯대로 초기화 한 후에는 active_on 플래그를 true로 만들어 더이상 스탯의 초기화가 일어나지 않도록 한다. 패시브 젬에서 중요한 것은 ‘태그’ 시스템인데 모든 젬은 젬에 대한 간략한 설명을 단어로 표현한 ‘태그’가 붙어 있다. 이 게임은 어떤 패시브 젬이 어떤 액티브 젬을 보조하는가를 이 ‘태그’를 통해서 구분한다. 패시브 젬이 가진 ‘필수 태그’가 액티브 젬의 ‘태그’ 중 하나이면 그 젬을 보조하는 것이 가능해지는 식이다. 이는 C#의 Array가 포함하고 있는 Contains 함수를 사용하여 구현하였다.
이렇게 모든 젬을 계산한 뒤 젬의 타입에 따라서 swing, magic, fire 등의 직접적으로 스킬을 사용하는 함수를 호출하게 된다. 이렇게 호출한 함수는 코루틴을 통해 일정한 딜레이를 가지고 자동적으로 계속해서 발동하게 된다.
석판은 기본적으로 6개의 슬롯을 가지고 있는데 이 중 3개는 처음부터 사용 가능하고 나머지 3개는 잠겨있다. 잠긴 3개의 슬롯은 Shopmanager의 함수에서 호출하는 함수로 골드를 소모하고 잠금을 해제할 수 있다.


Shopmanager(클릭 시 접기/펼치기)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class shopmanager : MonoBehaviour
{
   public invenmanager inv;
   public GameObject merchant_pannel;
   public Button slot_button;
   public Button link_button;
   public Button[] monolith_arrows;
   public GameObject goldless_pannel;
   GameObject[] monoliths;
   bool in_slot=false;
   bool in_link=false;

   void Start() {
      this.monoliths=inv.monoliths;
   }

   public void slot_lock() { //상점페이즈 내에서 인벤토리와 석판의 슬롯에 락을 걸어줌
      foreach(GameObject obj in inv.slots) {
         slot s=obj.GetComponent<slot>();
         s.islock=true;
      }
      foreach(GameObject obj in monoliths) {
         weaponmanager wpn=obj.GetComponent<weaponmanager>();
         foreach(slot s in wpn.mono_slots) {
            if(s.gameObject.activeSelf==true) {
               s.islock=true;
            }
         }
      }
   }

   public void slot_unlock() { //상점페이즈에서 나갈때 걸려있던 락을 모두 풀어줌
      foreach(GameObject obj in inv.slots) {
         slot s=obj.GetComponent<slot>();
         s.islock=false;
      }
      foreach(GameObject obj in monoliths) {
         weaponmanager wpn=obj.GetComponent<weaponmanager>();
         foreach(slot s in wpn.mono_slots) {
            if(s.gameObject.activeSelf==true) {
               s.islock=false;
            }
         }
      }
   }

   public void slot_open() { //슬롯 개방 메뉴에 들어갈 시 필요한 패널과 버튼을 띄워줌
      in_slot=true;
      slot_button.gameObject.SetActive(false);
      link_button.gameObject.SetActive(false);
      inv.inv_pannel.SetActive(true);
      inv.slot_refresh();
      slot_lock();
      foreach(Button btn in monolith_arrows) {
         btn.gameObject.SetActive(true);
      }
   }

   public void open_monolith0() { //각 석판의 개방 함수
      weaponmanager wpn=monoliths[0].GetComponent<weaponmanager>();
      if(gamemanager.instance.gold>=50) {
         wpn.slot_expand();
         gamemanager.instance.gold-=50;
      }
      else StartCoroutine(no_gold());
   }

   public void open_monolith1() {
      weaponmanager wpn=monoliths[1].GetComponent<weaponmanager>();
      if(gamemanager.instance.gold>=50) {
         wpn.slot_expand();
         gamemanager.instance.gold-=50;
      }
      else StartCoroutine(no_gold());
   }

   public void open_monolith2() {
      weaponmanager wpn=monoliths[2].GetComponent<weaponmanager>();
      if(gamemanager.instance.gold>=50) {
         wpn.slot_expand();
         gamemanager.instance.gold-=50;
      }
      else StartCoroutine(no_gold());
   }

   public void open_monolith3() {
      weaponmanager wpn=monoliths[3].GetComponent<weaponmanager>();
      if(gamemanager.instance.gold>=50) {
         wpn.slot_expand();
         gamemanager.instance.gold-=50;
      }
      else StartCoroutine(no_gold());
   }

   IEnumerator no_gold() {
      goldless_pannel.SetActive(true);
      yield return new WaitForSecondsRealtime(2f);
      goldless_pannel.SetActive(false);
   }

   public void return_button() { //슬롯개방 화면일땐 원래 상점으로, 상점 메인화면에선 스테이지로 돌아감
      if(in_slot==true) {
         in_slot=false;
         foreach(Button btn in monolith_arrows) {
            btn.gameObject.SetActive(false);
         }
         slot_button.gameObject.SetActive(true);
         link_button.gameObject.SetActive(true);
         slot_unlock();
         inv.inv_pannel.SetActive(false);
      }
      else if(in_link==true) {

      }
      else {
         Time.timeScale=1;
         Debug.Log("merchant close");
         foreach(GameObject obj in monoliths) {
            weaponmanager wpmn=obj.GetComponent<weaponmanager>();
            wpmn.monolith_active();
         }
         goldless_pannel.SetActive(false);
         merchant_pannel.SetActive(false);
      }
   }
}

마지막으로 볼 것은 상점 페이즈 내의 여러 작동과 UI를 관리하는 샵 매니저이다. 상점 페이즈 내에서는 슬롯의 조작이 불가능하도록 하기 위해 슬롯에 lock을 거는 함수와 푸는 함수가 있고 각 석판의 잠긴 슬롯을 해제하기 위해 석판의 weaponmanager에서 slot_expand 함수를 호출하는 함수가 있다.