강호형(hohyeong kang)
강호형(hohyeong kang)

Categories

Tags

0. 서론

본 포스트는 제가 2021년 1학기 자바프로그래밍실습 수업에서 진행한 기말 팀프로젝트에 대한 기록입니다. 이 수업을 통해 처음으로 JAVA 를 다루게 되었는데 그동안 다양한 언어들을 다뤘던 짬도 있고 객체지향 프로그래밍이라는 페러다임이 처음도 아니여서 그리 어렵지 않게 배웠던 것 같습니다. 하지만 수업 내용이 JAVA 라는 언어에 대해 깊이 공부한다거나 객체지향적으로 코드를 짜는 방법을 심도있게 탐구했다기 보다는 “자바로 코딩을 해보자! “ 라는 느낌이어서 조금은 아쉬웠던 것 같네요. 이번에 이야기할 프로젝트에도 객체지향을 무시하고 다소 자바스럽지 않게 코딩한 부분이라던가 구체적인 이해없이 어떻게든 가져다 쓴 코드들이 여기저기 남아있어 부끄럽지만 코드를 짜던 당시의 기억을 더듬으면서 왜 그렇게 했을까에 대한 회고와 반성을 해보도록 하겠습니다.

목차

  1. 아이디어
  2. 구현
    1. 게임 화면 설계 및 구현
    2. 게임 배경음악 및 효과음 구현
    3. 카드 다중 선택 로직 구현
    4. 글로벌 스코어 보드 구현
  3. 결과물


1. 아이디어

본 프로젝트는 저 포함 두 명의 팀으로 진행되었습니다. 자유 주제였기 때문에 처음 팀원분과 회의를 통해 무엇을 만들어볼까 고민하던 도중 둘 다 게임을 만들어보고 싶다는 마음이 통하여 게임으로 일단 주제를 정하게 되었습니다. 저는 컴퓨터그래픽스개론 수업을 통해 3D 게임을 만들어본 경험이 있기 때문에 2D 게임은 좀 쉽지 않을까 하는 안일한 마음에 다소 복잡한 컨샙의 게임을 제안하였는데, 팀원분께서 그것은 너무 어려울 것 같다고 단호하게 거절하시더군요. 그 당시에는 상당히 아쉬운 마음이 들었지만 프로젝트를 위한 시간(3주)이 많지 않았고 JAVA 와 GUI 구현이 그리 익숙한 상태는 아니었기 때문에 마감 기한에는 팀원분이 거절해주신게 참 다행이라는 생각을 하였습니다.

그렇게 다시 정한 게임 주제는 memory matching 게임으로도 불리는 카드 뒤집기 게임입니다. 흔히 아는 카드를 뒤집어 놓고 같은 그림인 카드 쌍을 찾는 게임입니다. 이것을 콘솔창에 텍스트로 구현하는 것은 저에게는 그다지 도전적인 일이 아니라고 생각했기 때문에 역할 분담을 통해 팀원분께 카드 뒤집기 게임의 콘솔창 버전 개발을 부탁드리고 제가 그것을 바탕으로 GUI를 구현하고 소캣 프로그래밍을 통해 글로벌 스코어 보드를 구현하는 것으로 정하게 되었습니다.(팀원분은 이번 학기에 처음 프로그래밍을 접하신 분이셨습니다.)

그 후 1주가 지난 뒤 팀원분께서 만든 콘솔 버전을 받고 저는 거기에 GUI를 적용하기 시작했습니다. 그런데 하다보니 카드 뒤집기 게임을 구글에 치면 수두룩하게 구현 코드가 나올 만큼 아이디어 자체가 너무 식상하고 게임 자체도 재미가 없다고 느껴져서 카드를 뒤집어서 쌍을 맞춘다는 로직은 유지한 채 흥미 요소를 추가할 방법을 고민하게 되었습니다. 그 결과 카드를 기억한다는 것에 영감을 받아 ‘살인자의 기억법’ 이라는 영화가 떠올랐고 살인자가 자신의 범죄를 기억하는 것을 카드를 뒤집는 행위에 비유하여 공포스럽게 표현해보자는 생각을 하게 되었습니다. 저는 바로 컨셉에 맞는 메인 화면을 제작한 뒤 팀원분께 이러한 아이디어를 제안드렸고 다행히도 흔쾌히 받아주셔서 저희의 최종 목표는 공포 컨셉을 가진 카드 뒤집기 게임 구현으로 정해지게 되었습니다.

main-screen

처음 제작한 컨셉 메인 화면


2. 구현

이미 구현된 게임 로직에 GUI를 입히는 일이 그리 어렵지 않을 것이라 생각했지만 GUI 구현의 문제 뿐만 아니라 콘솔로 구현된 게임을 그래픽 인터페이스를 가진 게임으로 포팅하면서 추가적으로 고려해야할 사항들이 생겨 쉽지만은 않은 프로젝트가 되었습니다. 제가 최종적으로 구현한 부분은 다음과 같습니다.

  1. 게임 화면 설계 및 구현
  2. 게임 배경음악 및 효과음 구현
  3. 카드 다중 선택 로직 구현
  4. 글로벌 스코어 보드 구현

2-1. 게임 화면 설계 및 구현

screen-design

게임 화면 설계

먼저 전반적인 화면 설계와 컴포넌트별 기능 및 상호작용 등을 기술한 스토리보드를 제작하였습니다. 처음 기획 단계부터 단순히 카드 뒤집기 게임을 구현하는 것이 목표가 아닌 다양한 기능과 재미요소, 깔끔한 UI, 유연한 로직을 갖춘 Well-Made 게임 제작을 목표로 하였기 때문에 화면 설계 시에도 화면 전환의 유연성직관적인 컴포넌트의 배치를 고려하였습니다.

screen-real

실제 게임 화면

실제 게임은 위 이미지와 같이 구현되었습니다. 게임을 간단하게 소개해드리자면 게임에는 경쟁전커스텀 총 두 가지의 게임 모드가 존재하며 경쟁전은 소캣 프로그래밍을 통해 구현된 서버를 통해 모든 플레이어들과 기록 경쟁을 하는 모드이고 커스텀은 본인이 게임 난이도를 마음대로 설정하여 플레이하는 모드입니다. 게임이 시작되면 최초에 공개된 앞 면을 기억해두었다가 정해진 목숨 수와 제한 시간 내에 모든 카드 쌍을 맞추는 것을 목표로 합니다.

GUI는 자바의 AWTSwing 을 활용하여 구현하였습니다. 가장 까다로웠던 점은 컴포넌트들을 추가, 제거하거나 여러 시각적 효과를 주었을 때 변화가 즉각적으로 반영되지 않거나, 변화되기 이전의 모습이 남아 있는 등 화면 갱신이 깔끔하게 되지 않는다는 점이었습니다. 예를 들어 불투명요소를 사용했을때 화면을 갱신할 때마다 투명도가 중첩된다던가, 화면 전환 시 이전 화면의 일부분이 남아있는다던가, 컴포넌트를 삭제하였을때 그 컴포넌트의 뒷 부분이 다시 그려지지 않고 비워져있거나 하는 식의 문제들을 겪게 되었습니다. 이 부분에 대해서는 JAVA AWT 와 Swing 의 painting 메커니즘에 대해 제대로 이해하지 못해서 완전히 해결하지는 못했습니다. 컴포넌트 추가, 제거 등 동적인 변화에 대한 화면 갱신 문제는 스택오버플로우 등을 참고하여 컴포넌트 구성의 변화가 필요할 때마다 repaint() 메소드를 호출하는 식으로 해결하였고, 깜빡임이나 잔상 문제는 더블버퍼링이라는 기법으로 해결 가능하다고 하는데 본 게임은 많은 요소가 다이나믹하게 움직이지는 않기 때문에 불투명 요소를 최대한 배제하는 식으로 거의 문제가 느껴지지 않게끔 구현하였습니다. 당시에는 마감 기한 안에 구현을 하는 것에 집중하여 문제를 이해하는 것보다 해결하는데에 집중하였지만 본격적인 학습을 통해 모호함을 없애고자 하는 분들과 저를 위해 참고 자료를 공유합니다.

또 다른 문제점은 카드 개수가 늘어났을 때 게임 초기화 시에 카드 이미지를 불러오는 시간이 증가한다는 것이었습니다. 특히 카드 개수가 최대 8x8 까지 자유롭게 설정할 수 있도록 하였기 때문에 카드 개수에 따라 그에 맞게 카드 이미지의 크기를 조절해야 했습니다. 이 문제에 대해 카드 앞 면의 경우에는 각각 로드 후 리사이징 해줘야 하지만 뒷 면은 모두 동일하기 때문에 한번만 리사이징 작업을 하면 된다는 것을 파악하였고, 이를 위해 static initializer block 을 사용하게 되었습니다. 정적 초기화 블럭은 인스턴스 생성시 마다 실행되는 것이 아니라 클래스가 초기화될 때 딱 한번만 실행되는 코드 블럭입니다. 이를 통해 뒷 면 이미지 처리에 소요되는 시간을 최적화할 수 있었습니다.

static {
		Image card_back_origin = card_back_image.getImage();
		Image card_back_resize = card_back_origin.getScaledInstance(PlayPage.GAME_SCREEN_WIDTH/PlayPage.card_col, PlayPage.GAME_SCREEN_HEIGHT/PlayPage.card_row, Image.SCALE_FAST);
		card_back_image = new ImageIcon(card_back_resize); 
	}

본 프로젝트에서는 여기서 최적화를 그만두었지만 추가적으로 더 최적화 할 수 있었던 부분을 생각해보자면 애초에 게임을 실행할 때에 필요한 카드 이미지를 모두 미리 로드해두고, 게임 플레이 직전에는 카드 수에 따라 리사이징만 진행하게 하고, 앞 면 또한 한 쌍은 같은 이미지이므로 두 번 리사이징 하지 않도록 할 수 있을 것 같습니다. 또한 사용자 경험까지 고려한다면 로딩 화면을 만들어 사용자에게는 로딩 및 리사이징이 완료된 화면만 보여주는 방법도 적용할 수 있을 듯 합니다.

2-2. 게임 배경음악 및 효과음 구현

소리 구현을 위해 JLayer 라는 라이브러리를 사용하였습니다. Thread 클래스를 상속받는 Music 클래스를 생성하여 배경음악과 효과음 모두 해당 클래스로 재생되도록 하였으며, 배경음악은 무한히 반복되도록, 효과음은 한번만 실행되도록 하였습니다. 소리는 게임 로직에 방해되지 않아야하고 여러 소리가 동시에 나는 것이 가능하도록 하기 위해 독립적인 Thread에서 작동하도록 구현하여야 합니다. 본 프로젝트에서는 모든 소리가 재생될 때마다 해당 파일에 대한 input stream 을 생성하여 실행하도록 구현하였는데, 반복해서 쓰이는 효과음같은 경우에는 그때마다 디스크의 파일에 접근해서 읽는 것이 아니라 해당 파일을 미리 읽어두어 메모리에 할당해두고 실행시켰다면 더 최적화 되었을까하는 생각이 듭니다.

2-3. 카드 다중 선택 로직 구현

팀원분께서 구현해주신 콘솔 버전의 카드 선택 로직은 다음과 같습니다.

첫번째 카드 위치 입력 -> 카드 오픈 -> 두번째 카드 위치 입력 -> 카드 오픈 -> 짝 확인

위 로직에서는 카드가 동시에 여러장 선택되는 경우가 존재하지 않습니다. 반드시 사용자로부터 텍스트 인풋을 통해 카드를 확인 후 다음 카드를 입력받기 때문입니다. 하지만 이것을 그래픽 버전으로 구현하다보니 한 번에 한 장의 카드만 다루는 로직은 게임성을 심각하게 저해하였습니다. 가장 큰 문제는 짝 확인 과정에서 일단 선택된 두 카드들의 앞 면을 일정 시간동안 보여준 뒤 (플레이어도 짝이 맞았는지 아닌지 확인해야 하므로) 그 다음에 짝이 맞는지 검사하고 난 뒤에야 플레이어는 다음 카드를 선택할 수 있다는 것이었습니다. 즉 짝 확인 에 소요되는 시간동안 플레이어는 아무것도 할 수가 없는 것이죠. 제한 시간 안에 최대한 빠르게 카드 짝을 맞춰야 하는 게임 특성상 플레이어는 빠르게 카드를 선택하고 넘어가야 하는데 한 번에 딱 한 쌍만 일정 시간을 기다리면서 플레이해야 한다는 것은 재미를 떨어트리는 치명적인 요소라고 생각했습니다. 그래서 저는 카드 오픈과 짝 확인 로직을 분리하고자 하였습니다.

방법은 다음과 같습니다. 플레이어가 카드를 선택할 때마다 해당 카드를 Queue 자료구조에 저장합니다. 그러다가 Queue 에 짝수 개의 카드가 저장되면 짝 확인 Thread 를 실행합니다. 이는 게임 플레이 Thread와 독립적이므로 플레이어는 아직 짝 확인 이 끝나지 않았더라도 다음 카드를 선택할 수 있게 되고, 동시에 여러 짝 확인 과정이 진행될 수도 있습니다. 짝 확인 Thread가 실행되면 Queue 에서 두 개의 카드를 꺼냅니다. Queue 는 선입선출이므로 플레이어가 오픈한 카드 순서대로 짝 확인 이 이루어지게 됩니다. 이때 카드 쌍이 맞다면 카드 컴포넌트를 제거하고, 다르다면 다시 뒷 면으로 바꿉니다.

위와 같은 방법을 적용하지 않았다면 플레이어는 시작하자마자 모든 카드를 다 외웠다고 하더라도 [카드 쌍 수 x 짝 확인 시간 ] 만큼의 시간이 필요하지만, 위 방법을 통해 클릭만 빨리한다면 1초만에 라도 모든 짝을 확인 할 수 있게 됩니다.

multi-card

동시에 여러 카드가 선택된 모습

2-4. 글로벌 스코어 보드 구현

플레이어가 자신의 기록을 다른 플레이어들과 겨뤄볼 수 있도록 글로벌 스코어 보드 를 구현하였습니다. 네트워크 통신은 소캣을 통해 구현하였으며 서버는 각각의 클라이언트를 각각의 쓰레드에서 처리하도록 하여 동시에 복수의 클라이언트에 대응할 수 있도록 구현하였습니다. 이 게임의 두 가지 게임 모드 중에서 경쟁전 선택 시 사용자에게 플레이어명을 입력받게 됩니다. 또한 서버가 열려있지 않거나 연결할 수 없더라도 플레이어가 기록 저장을 하지 않고 컨텐츠를 즐길 수 있도록 처리하였습니다.

connect

위 : 서버 연결 시 / 아래 : 서버 연결 불가 시

기록 저장은 텍스트 파일을 이용하였습니다. 데이터베이스를 활용하는 편이 좋겠지만 새롭게 JDBC를 익히기에는 시간이 부족했고 반드시 필요할 정도의 규모도 아니라고 판단하여 텍스트 파일에 저장하는 방식으로 진행하였습니다. 동시에 여러 클라이언트가 하나의 텍스트파일에 접근하기 때문에 race condition 방지를 위해 synchronized 를 사용하여 텍스트 파일을 건드는 작업은 한번에 하나의 클라이언트만 가능하도록 처리하였습니다. 플레이어가 모든 카드 쌍을 맞추는데에 성공한다면 성공 화면과 함께 자신의 랭킹이 기록된 표를 확인할 수 있습니다. 랭킹은 남은 시간이 많을수록 높은 등수를 가지게 되며, 시간이 같다면 남은 목숨 수가 많은 사람이 우선되며, 둘 다 같을 경우 나중에 플레이한 플레이어가 우선됩니다. 플레이어가 게임 클리어에 실패한다면 실패화면과 함께 다른 사람들의 기록을 표로 확인할 수 있습니다.

ending

위 : 성공 시 / 아래 : 실패 시


3. 결과물

최종적으로 제출한 게임의 플레이 화면과 소스코드 입니다.


4. 마무리

저의 첫 JAVA 프로젝트로 익숙치 않은 자바 문법과 더불어 짧은 기간동안 구현하는데 급급하여 디테일에 많이 신경을 못 쓴 프로젝트였던 것 같습니다. 코딩 뿐만 아니라 배경 이미지, 버튼 이미지 까지도 직접 만들어야했기에 시간이 더 촉박했었던 것 같네요. 자바 GUI의 painting 메커니즘, input stream, synchronized, JDBC 등 아직 조금 더 명확히 공부해야 할 것이 정말 많은 것 같습니다. 모호함을 없애고 호불호 강한 개발자가 되기 위해 다시 정진하겠습니다.