ClueBall Wiki - UAVisonline/Portfolio GitHub Wiki

BGM Game

보드게임 'Clue'와 '숫자야구'를 결합한 게임을 제작, 이를 2가지 UI로 플레이해 선호도를 조사하는 HCI 프로젝트

해당 게임은 'Clue'와 '숫자야구'를 결합한 디자인으로, 플레이어는 일정 기회만큼 추리를 진행해 용의자, 범행도구, 범행장소에 대한 정보를 얻는다. 이러한 정보를 얻는 과정은 숫자야구 식으로 진행되며, 기회가 전부 소진되거나 정보가 충분하다고 판단하면 최종추리를 진행해 정답을 맞출 수 있다. 또한 얻었던 정보를 기록할 수 있는 정보 Note가 제공되어 플레이어는 이를 이용해 이전 정보를 프로그램 내에서 기록할 수 있다. 플레이어는 여기서 2가지 UI를 이용해 게임을 플레이 가능하다.

해당 프로젝트는 2022년 4월 HCI를 수강하면서 주제를 직접 선정하는 프로젝트를 진행할 때, 'UI간 선호도'라는 주제를 선정하고 이를 조사하기 위해 진행, 완료한 게임 프로젝트이다.

해당 프로젝트를 진행하면서 그래픽 요소에 대해 아래와 같은 UnityAsset을 활용했으며, 유저 스터디를 위해 단톡방에 배포를 진행하였다.

  • Bags & boxes icons, Character avatar icons windows, fairytale icons megapack windows
  • fantasy icons megapack, modern tools icons, monsters avatar icons windows
  • fantasy gui, GUI PRO Kit - Sci Fi

해당 위키에서는 프로젝트에 대해 아래 네 항목에 관해 설명하고자 한다.

  • 두 가지 UI를 게임 시스템에서 어떻게 구현하였는가?

  • 버튼 내 다양한 기능을 어떻게 구현하였는가?

  • 정보 Note를 어떻게 구현했는가?

  • 프로젝트에서 아쉬운 점은 무엇인가?

두 가지 UI를 게임 시스템에서 어떻게 구현하였는가?

1. UI 인터페이스를 위한 기본 작업 및 Script 상속 (InterfaceManager Script 및 이를 상속받은 Script)

Game에서는 기본형, 방향형 인터페이스가 존재하며 플레이어는 게임을 시작할 때 어떤 인터페이스를 사용할 지 선택해 플레이할 수 있다. 이러한 인터페이스는 버튼의 위치가 다를 뿐, 그 외에는 기본적으로 같은 기능을 사용한다. 따라서 두 인터페이스를 구현하는데 도움이 되는 부모 InterfaceManager Script를 작성, 이는 아래와 같은 기능을 가지고 있다.

  • 9개의 Button UI Script 변수

  • 위 Button을 전부 SetActiveFalse하는 함수 ( set_active_false )

  • 추리 버튼에 대해 button 상호작용을 꺼버리는 함수 ( set_interactable_false_first_button, set_interactable_false_eighth_button )

  • Button UI에 대해 CommandObject 기능을 할당하는 함수 ( set_button_event(int index, CommandObject value, string str="") )

위 Script의 기능을 통해 기본적인 청사진을 만들었으면 각 UI interface는 위 Script를 상속받아 제작한다. 이 때 위 기능을 interface Script가 그대로 사용할 수 없는 경우, 해당 function을 오버라이딩해 맞춤 변경해주도록 한다.

이를 통해 표준형 interface Script(StandardInterfaceManager Script)를 아래와 같이 작성하였다.

33  public void set_number_of_choice(int value) // y_pos 설정을 위한 함수 (전체 보기의 갯수를 불러온다)

39  public override void set_button_event(int index, CommandObject value, string str = "") // 오버라이딩
    {
        ........
        int tmp = current_button;
        if(index==9) // back button에 대한 경우
        {
            tmp = 9;
        }
        else // 그렇지 않은 경우
        {
            current_button++;
        }

        switch (tmp)
        {
            case 1:
                first_button.GetComponent<RectTransform>().anchoredPosition = new Vector2(0.0f, y_pos);
                first_button.set_command_object(value, str);
                break;
            case 2:
                second_button.GetComponent<RectTransform>().anchoredPosition = new Vector2(0.0f, y_pos);
                second_button.set_command_object(value, str);
                break; ....

위 2개의 function은 StandardInterface에서 주로 사용하는 기능으로, 39 Line은 기존 Button UI에 Command Object를 할당하는 기능을 오버라이딩해 Button UI의 위치도 설정하는 기능을 추가하였으며 이를 위해 해당 페이지에서 button UI를 몇 개 사용하는지 받는 set_number_of_choice function을 제작하였다.

그리고 방향형 interface Script(DirectionalInterfaceManager Script)는 아래와 같이 작성하였다.

29  public override void set_active_false()
    {
        base.set_active_false();
        back_button.gameObject.SetActive(false); // Back Button False (Directional Interface는 따로 BackButton을 Button UI로 사용)
        Circle.SetActive(false); // Circle Obejct False
    }

    public override void set_button_event(int index, CommandObject value, string str = "")
    {
        if(index!=0) 
        {
            if(Circle.activeSelf==false)
            {
                Circle.SetActive(true);// 가시성 Object를 True 상태로 변경
            }
        }

        switch (index)
        {
            case 0:
                back_button.set_command_object(value, str, false); // BackButton 할당
                break;
            case 1:
                first_button.set_command_object(value, str, false);
                break;
            case 2:
                second_button.set_command_object(value, str, false);
                break; ...

Directional Interface Script는 부모 Script의 함수를 오버라이딩해 자기 자신이 필요하도록 변경했는데, set_active_false함수는 backButtonUI와 Circle Object를 비활성화 하는 부분을 추가했으며, set_button_event함수는 backButtonUI가 아닌 UI를 할당할 때 가시성을 위해 만들었던 Circle Object를 활성화 하는 부분을 추가했다.


2. GameManager를 통한 UI interface 선택 실행(GameManager Script)

위 UI를 게임을 시작할 때 선택할 수 있어야 하는데 우선 Player가 무슨 UI를 선택했는지 시스템은 기록해야 한다. 이는 게임 전체에 있어서 데이터를 관리하는 GameManager Script내 아래 내용을 통해 해결하였다.

40  private Interface_mode interface_mode;

42  [SerializeField] private GameObject mainScreenCanvas;
    [SerializeField] private GameObject standardInterfaceCanvas;
    [SerializeField] private GameObject directionInterfaceCanvas;
...
158 public void set_interface_mode(Interface_mode value)
    {
        interface_mode = value;
        mainScreenCanvas.SetActive(false); // Main화면 Off

        switch(interface_mode) // Interface mode에 따른 InterfaceCanvas 상태 설정
        {
            case Interface_mode.standard:
                standardInterfaceCanvas.SetActive(true);
                directionInterfaceCanvas.SetActive(false);
                break;
            case Interface_mode.direction:
                standardInterfaceCanvas.SetActive(false);
                directionInterfaceCanvas.SetActive(true);
                break;
        }
    }
...

위 내용 40 Line에서 어떤 Interface를 선택했는지 저장하며, 42~44 Line에서 해당 Interface Object 및 메인화면 Object를 연결한다. 그 뒤 Interface mode를 선택하는 버튼을 누르면 해당 버튼에서 위 set_interface_mode을 호출한다. 해당 함수에서는 Main화면을 종료하고 interface 종류에 맞는 Canvas를 ActiveTrue하여 실제 게임에 있어서 해당 Interface를 사용할 수 있게 해준다.
InterfaceImage

버튼 내 다양한 기능을 어떻게 구현하였는가?

1. Button과 CommandObject (CommandObject Script 및 ButtonUI Script)

Game에는 Interface 선택 외에도 페이지 이동, 뒤로가기, 용의자/범행장소/범행도구 선택과 같은 기능을 요하고 있으며, 이 중 일부 버튼의 기능들은 Interface Canvas 내 존재하는 Button에서 사용해야 한다. 그러나 이러한 Interface의 Button을 기능마다 만드는 것은 굉장히 시간이 많이 소모되며 관리도 어렵기 때문에, 한 개의 Button에서 기능을 교체하는 작업이 필요하며 이를 위해 Button에 넣을 기능은 전부 동일한 Script 상속을 통해 만들어야 한다.

public interface Command // Design Pattern - Command
{
    void execute();

    //void de_execute();
}

public abstract class CommandObject : MonoBehaviour, Command
{
    abstract public void execute();

    //abstract public void de_execute();
}

Button에 넣어서 사용할 기능을 위 Script의 Design Pattern - Command를 이용해 제작, 클래스 CommandObject를 정의하여 Button의 기능을 CommandObject를 상속받아 execute 함수를 오버라이딩하는 것으로 구현할 수 있다. 그 중 Interface에 따라 Button의 기능을 교체하는 것은 ButtonCommandObject를 통해 구현했으며 이를 이용해 아래 기능을 구현하였다.

  • 뒤로가기 기능 (BackSpace_Command Script)
  • 용의자/범행도구/장소 선택 기능 (Human_Command, Place_Command, Tool_Command Script)
  • 단순 Page 이동 기능 (ActiveCommand Script)

그리고 Interface Canvas에 존재하는 다양한 Button들은 아래 Script의 set_command_object 함수를 통해 CommandObject와 연결할 수 있으며, click_event 함수를 통해서 실행조건이 맞는 경우 CommandObject의 execute 기능을 실행하는 역할을 할 수 있다.

    public void click_event()
    {
        //Debug.Log(this.gameObject.name);

        if (command_object!=null && GameManager.gamemanager.get_pause()==false) // 일시정지 시 입력 불가능하게 수정
        {
            GameManager.gamemanager.click_play(); // 클릭 소리 재생
            interface_manager.set_active_false(); // Canvas 내 버튼 오브젝트 비활성화
            command_object.execute(); // Command Objecct 기능 실행
        }
    }

    public void set_command_object(CommandObject value, string str, bool number_marking = true)
    {
        command_object = value; // command obejct 할당
        set_text(str, number_marking); // 버튼 text 설정 (숫자도 보이게 할 것인가?)
        this.gameObject.SetActive(true); // 버튼 Setactive True
    }

ButtonImage

정보 Note를 어떻게 구현했는가?

1.정보Note 구현 및 시각화(Information_update Script 및 Information_List Script)

정보Note는 플레이어의 이전 추리를 통해 얻은 정보를 저장하고 있으며, 플레이어가 소거법을 통해 정보를 배제하는데 무엇을 배제할 지 시각적으로 큰 도움을 주고 있다. 이는 Information_List Script를 통해 처리하고 있는데, 해당 Script는 아래와 같은 기능을 가지고 있다.

  • 용의자/범행도구/범행장소와 같은 ScriptableObject List 및 각 카테고리에 해당하는 Information_update Script
  • 위 List 내부 ScriptableObject 안에 있는 지역변수에 접근할 수 있는 함수
  • 게임 시작 시 ScriptableObject 내 지역변수를 기본값으로 초기화하는 함수 (init_information)
  • 플레이어의 추리결과에 따른 ScriptableObject 지역변수 auto_cacl을 설정하는 함수 (zero_match, more_match)

위 기능을 통해 시스템은 플레이어의 추리를 ScriptableObject 내 auto_cacl 변수를 설정해주는 것으로 기록해줄수 있다. 또한 이러한 정보 Note를 시각화하는 것은 내부 Information_update Script를 호출하는 것으로 가능한데 해당 Script안에는 다음과 같은 코드가 존재한다.

20  public void set_information_computer(int index, information value) // Computer가 설정한 정보값을 반영 (Sprite 변경)
    {
        computer_informations[index] = value;
        switch (value)
        {
            case information.none:
                computer_information_image[index].sprite = none;
                break;
            case information.wondering:
                computer_information_image[index].sprite = wondering;
                break;
            case information.not_this:
                computer_information_image[index].sprite = not_this_one;
                break;
            case information.this_one:
                computer_information_image[index].sprite = this_one;
                break;
        }
    }

해당 코드에서 computer_information_image는 각 ScriptableObject에 대한 추리 정보를 이미지로 표시할 수 있도록 연결되었으며, 위 set_information_computer 함수를 통해 특정 index 이미지를 변경하는 것이 가능해진다. Information_list Script 내 init_information, zero_match, more_match 함수에서 이를 호출, 갱신된 추리 정보를 시각화한다.

이러한 방식은 시스템이 계산한 추리 결과만을 반영하고 있으며, 플레이어는 이것만을 이용해서 소거법을 진행하기는 힘들다. 따라서 Information_Update Script 내 플레이어의 정보를 반영할 수 있는 Child_information_Update Script를 List 형태로 참조해주도록한다.

Child_information_Update Script는 Unity EventSystem을 이용해 이미지 클릭이 감지되면 정보를 변경하는 기능을 가지고 있는데 플레이어는 이를 이용해 정보를 자신이 원하는 방향으로 변경하여 소거법을 진행할 수 있다.

InformationNoteImage1


2.각 페이지에 따른 시각화(Information_List Script)

Game에서는 정보Note에 접근하는 Page외에도 최종추리에 있어서 지금까지 기록한 정보 일부를 플레이어가 볼 수 있도록 제공해주어야 한다. 그러나 이를 위해 새로운 게임오브젝트를 만드는 것은 성능을 더 잡아먹을 뿐더러 정보를 기록하는 관리 부분에서도 훨씬 비효율적이다. 이를 Information_List Script는 아래 함수를 통해 Animator를 호출하는 방식으로 처리하고 있다.

209 public void set_visualize(bool value) // 정보 Note를 Animator를 이용해 보일지 안보일지 설정
    {
        if(animator!=null)
        {
            animator.SetBool("Visualize", value);
        }
    }

    public void start_animation(string value) // value 이름을 가진 애니메이션을 Animator에서 재생
    {
        if(animator!=null)
        {
            animator.Play(value, -1, 0.0f);
        }
    }

이 함수는 GameManager Script에도 연결되어 있으며 페이지가 이동될 때 정보Note가 보여야 하는 순간에는 GameManager 내 set_visualize 함수를 새로 Open되는 페이지가 호출하며, 일부 정보만 보여야 하는 순간에는 GameManager 내 information_animation_start 함수를 호출하는 것으로 구현할 수 있다.
이를 통해 아래와 같이 페이지 위치에 따라 정보 Note의 특정 부분을 출력하는 것이 가능하다.

InformationNoteImage2

프로젝트에서 아쉬웠던 점은 무엇인가?

기본적으로 ButtonCommandObject를 상속받은 Script들은 모두 게임 진행에 있어서 사용이 되며, 이러한 진행은 모두 페이지 이동을 수반하고 있다. 따라서 이전 페이지를 비활성화하고 새 페이지를 활성화하는 기능이 필요한데 이를 off_gameobjects, on_gameobjects라는 GameObject List에 연결해 사용하고 있다.

그러나 상속받은 Script들은 이러한 기능을 전부 따로 구현하였는데 이를 위한 Script를 하나 더 생성하고 이를 상속받아 작성했으면 중복된 기능을 매 Script마다 작성하는 일은 생기지 않았을 것이다.

⚠️ **GitHub.com Fallback** ⚠️