May 28, 2020

디바이스마트 미디어:

[58호]Battleship 2인용 게임기

58 ict 배틀쉽 (15)

2019 ICT 융합 프로젝트 공모전 참가상

Battleship 2인용 게임기

글 | 국민대학교 오재홍, 이강민

1. 심사평
칩센 보드게임을 디지털화하였다는 것에서 재미있는 발상이라 생각합니다. 하드웨어 구성에 있어서 게임기 조작부는 기성 조이스틱 등을 사용하여 조금 더 심플한 방법을 추구하고, Display 부분에 dot/character 보다 더 유려한 형태의 구성으로 했다면 재미있어 보이지 않았을까 합니다. 모듈 문제로 적용하지 못한 블루투스 기능 또한 아쉬움으로 남습니다.
뉴티씨 한때 유행하던 8비트 임베디드 게임기를 상상하게 만든 작품이었습니다. 실제로 블루투스를 넣어서 서로 통신하여 게임할 수 있도록 된 점도 잘된 점이라고 생각합니다. 다만, 스마트폰을 LCD 표현용으로 사용하고 스마트폰과 연결하여 게임기로 쓸 수 있도록 하였다면, 보조장치로 더 좋지 않을까 생각합니다.
위드로봇 보드 게임을 임베디드 시스템으로 구현한 부분이 재미있는 작품입니다.
펌테크 과거 오락실 게임을 연상시키는 흥미로운 작품이라고 생각합니다. 단순해 보이지만 프로그래밍 과정을 통해 게임 구성에 관한 Algorithm 구현, GLCD를 통한 상태 표시 등은 기술적 구현이 쉽지는 않았을 것으로 예상됩니다. 비록 2대의 게임기기 상호 간 무선통신을 통한 구현을 최종 완성시키지 못했지만 유선상의 구현만으로도 충분히 가치가 있는 작품이라고 생각합니다.

2. 작품 개요
· 2대의 ATmega128을 이용한 유선 통신 기능 구현
· 기존의 종이나 장난감으로 하는 보드게임을 디지털화하여 시중에서 판매되는 게임기와 비슷한 외형의 디지털 게임기 제작

3. 작품 설명

58 ict 배틀쉽 (1)

이 작품은 battleship 보드게임 원리에 착안해 만들었다. 기존의 battleship 보드게임은 10×10의 보드에서 이루어지고, 우선 각자 자신의 보드에 각각 크기가 5,4,3,3,2인 다섯 개의 배(종류는 4종류, 각각 Aircraft(크기 5), Battleship(크기 4), Cruiser(크기 3), Destroyer(크기 2))를 가로 혹은 세로로 겹치지 않게 배열한다. 그리고 서로 돌아가며 상대의 배의 위치를 추측한다. 한 번씩 돌아가며 상대의 배가 있을 걸로 예상되는 위치(게임 보드 위의 한 점)를 지목한 뒤, 그 결과를 듣고 다시 그 결과를 바탕으로 추측하여 먼저 상대의 배가 있는 위치를 모두 맞춘 쪽이 승리하는 게임이다. 이 작품은 기존 battleship 보드게임과 달리 상대방의 5칸 크기의 배를 파괴하거나 5칸 크기의 배가 아닌 배를 3척 파괴하는 것이 승리조건이다.

58 ict 배틀쉽 (2)
차이점은 이 작품에서는 크기가 5,4,3,2인 배가 있고 한 칸 크기의 함정이 있다는 것이다. 이 함정은 배와 겹치지 않게 배열하고 상대방이 함정을 공격했을 때 배를 공격했다고 착각하게 만드는 역할을 하여 배의 모양을 쉽게 예측하지 못하게 만들었다는 것이다. 마지막으로 스캔 기능이 추가되었다. 자신이 공격에 성공한 배가 어떤 배인지 알기 위한 기회가 3번이 주어진다. 자신의 공격 턴에 지정한 좌표에 스캔 버튼을 누르면 배의 종류를 알려준다.
이러한 게임 규칙의 변화, 아이템 사용 효과 추가, 아날로그 게임의 디지털화를 통해서 기존 battleship 보드게임에서 보다 더 재미의 요소를 추가하였다.

3.1. 주요 동작 및 특징
가. 스위치 동작

58 ict 배틀쉽 (1)
(1) 리셋 스위치
(2) 상하좌우 스위치
(3) 포격/확인 스위치
(4) 회전/스캔 스위치

나. 게임 진행 순서

58 ict 배틀쉽 (3)

① 게임기 상단에 부착된 스위치를 이용해 각자의 배 배치를 완료한다.

58 ict 배틀쉽 (4)

② 배 배치가 완료된 후 한 사람씩 공격을 하게 되는데 상대방의 배 위치를 맞췄을 때는 X표시가 되며 한 번 더 차례를 진행하고 공격에 실패했을 때는 *표시가 되며 상대방의 턴으로 넘어간다. 특정 알파벳 배의 모양을 완전히 파괴하게 되면 화면 하단에 있는 알파벳 현황판 숫자가 달라진다.

58 ict 배틀쉽 (2)

③ 게임기 스위치 좌측 상단에 LED는 자신의 턴일 때는 초록색, 상대방의 턴일 때는 빨간색에 불이 들어온다.

58 ict 배틀쉽 (3)

④ 공격 시 파란색 버튼을 이용하여 스캔 기능을 사용함으로써 상대방 특정 위치의 배 종류를 알 수 있다. 스캔 기능은 각 플레이어에게 3번의 사용 기회가 주어진다.

58 ict 배틀쉽 (5)

 

 

58 ict 배틀쉽 (6)

 

⑤ 게임 진행 중 승리조건이 달성되면 승리 문구가 표시되고 흰색 버튼을 눌러 새로운 게임을 시작할 수 있다.

3.2. 전체 시스템 구성
가. 스위치 구성

58 ict 배틀쉽 (7) 58 ict 배틀쉽 (8)

한 대의 게임기 안에 있는 7개 스위치는 모두 풀업 저항 회로로 구성되어 있고 스위치를 누를 때 연결된 포트의 HIGH 신호를 LOW 신호로 바꾸어 입력 신호를 변경한다. 스위치는 모두 폴링(Polling)방식으로 동작되며 RESET 단자에 연결된 스위치를 제외한 나머지는 모두 PORTD에 연결하였다.

나. 사운드

58 ict 배틀쉽 (9)

게임의 효과음은 전기 신호를 소리로 변환시켜 주는 부저(Buzzer)를 통해서 제작하였다.
부저에는 내부에 전자석과 진동을 일으키는 판으로 이루어져 있으며, 전자석이 진동판을 아주 빠르게 흔들어 소리를 발생 시키는 원리를 가지고 있어 타이머/카운터를 이용하여 주파수를 정해주어 소리를 발생시켰다.
하나의 효과음은 여러 가지 주파수의 배열로 만들었고 타이머/카운터 레지스터를 조절하는 함수의 인자로 사용하였다.

다. 통신 방법

58 ict 배틀쉽 (10)

게임기의 통신은 USART(Universal Synchronous and Asynchronous Receive and Transmitter) 통신 방식을 사용한다. USART 통신을 하기 위해서 2대의 ATmega128 Tx/Rx 포트를 서로 교차하여 선을 연결하고 양쪽 ATmega128 모두 송수신 속도, 전송 문자의 데이터 비트수 등 데이터를 송수신 하는데 필요한 레지스터들을 동일하게 설정한다.
정보를 보낼 때 UDR0에 데이터를 넣어 인터럽트를 발생시키고 그 데이터를 받을 때는 미리 선언한 flag 값만 변경시켜 인터럽트를 통해 실시간으로 정보를 주고받는다.

라. 배터리

58 ict 배틀쉽 (4)

게임기의 GLCD 백라이트와 부저에 전원을 공급하기 위해서 약 5V의 전압이 필요하다. 따라서 게임기의 하단부에 AA배터리 소켓을 4개 부착하였고 1.5V AA건전지 4개를 직렬 연결하여 총 6V의 전압을 공급할 수 있도록 설계하였다.

마. 게임 알고리즘

58 ict 배틀쉽 (11)

3.3. 개발 환경(개발 언어, Tool, 사용 시스템 등)

58 ict 배틀쉽 (12)

4. 단계별 제작 과정
4.1. 코드 제작

58 ict 배틀쉽 (13)

① Visual Studio와 Code Vision AVR 모두 C언어를 기반으로 한 플랫폼이기 때문에 먼저 Visual Studio로 게임 알고리즘을 제작한 후 Code Vision AVR에 부합하는 코드로 변환하여 GLCD에 영상을 띄웠다.

58 ict 배틀쉽 (5)

② ATmega128간의 USART 유선통신의 데이터 송수신을 간단한 스위치 동작으로 확인하였다.

4.2. 하드웨어 제작

58 ict 배틀쉽 (6)

① 스위치와 리드선을 글루건으로 고정시키고 스티로폼에 스위치만 보이게 하여 실제 게임기 스위치 같은 외형을 제작하였다.

58 ict 배틀쉽 (14)

② 아크릴판으로 틀을 제작하고 게임기의 윗면에 GLCD와 스위치를 아랫면에는 ATmega128, 부저, 브레드보드를 위치시켰다.

58 ict 배틀쉽 (7)

③ 2대의 게임기를 붙여 게임기 하단에 배터리를 넣으면 게임이 작동하도록 제작하였다.

5. 기타
5.1. 소스코드
가. 배 배치 알고리즘

void print_ship(int x, int y, int ship_length, char ship_name)
{
if(turn==0)
for (i = 0; i < ship_length; i++)
board_char[y][x+i] = ship_name;

else
{
for (i = 0; i < ship_length; i++)
board_char[y+i][x] = ship_name;
}
}

print_ship 함수는 x,y 좌표, 배의 길이, 배 이름을 인수로 받아 전역변수 turn이 0이면
배의 이름을 board_char 배열에 가로로 넣고 turn이 1이면 board_char 배열에 세로로 넣는다.

void MapDraw(void)
{
for(i = 0; i < 10; i++)
{
for(j = 0; j <10; j++)
{
GLCD_TextGoTo(i,j);
GLCD_WriteChar(board_char[j][i]);
}
}
}

MapDraw 함수는 board_char 배열에 저장된 데이터를 GLCD에 찍어주는 역할을 한다.

switch(PIND)
{
case 0xfd: // 아래로 움직이는 키를 눌렀을 때 (PORTD1)
if (turn == 0 && player_y != 9)
{
player_x+=0; player_y+=1;
_delay_ms(3000);
}
if (turn == 1)
{
if(board_char[player_y + ship_length][9] == ‘.’){
player_x+=0; player_y+=1;
_delay_ms(3000);
}
}

배를 이동하기 코드의 일부를 발췌한 부분이다.
turn이 0이면 배가 가로로 되어있는 상태이고 1이면 세로로 되어있는 상태이기 때문에 turn이 0이고 y좌표가 (x,9) 가 아닌 경우에 한 칸 내려갈 수 있고,
turn이 1이고 y좌표와 배의 길이를 더한 좌표에 있는 문자가. 맵의 기본 모양이라면 한 칸 내려갈 수 있기 때문에 y좌표에 1을 더해주었다. GLCD의 좌표는 첫 번째 줄의 y좌표는 0이고 아래로 내려갈수록 +1씩 된다.

void enter_ship(int x, int y, int ship_length, char ship_name)
{
if (turn == 0)
for (i = 0; i<ship_length; i++)
{
grid[y][x + i] = ship_name;
}
else
for (i = 0; i<ship_length; i++)
{
grid[y + i][x] = ship_name;
}
placement_cnt++;
enter_cnt++;
}

enter_ship 함수는 turn의 값을 통해 배의 방향을 판단하고 반복문을 이용해 grid 배열에 배치할 배의 이름을 넣어주는 함수이다.

void erase_ship(int x, int y, int ship_length)
{
if (turn == 0)
for (i = 0; i < ship_length; i++)
board_char[y][x + i] = grid[y][x + i];

else
for (i = 0; i < ship_length; i++)
board_char[y + i][x] = grid[y + i][x];
}

erase_ship 함수는 x,y 좌표, 배의 길이를 인수로 받아 board_char 배열에 grid 배열을 넣어준다. 이 함수는 좌표값을 이동시켰을 때 이전에 print_ship 함수로 board_char 배열에 넣어주어 Mapdraw 함수로 GLCD에 배열을 표현했을 때 잔상이 남는 것을 지워주는 역할을 한다. board_char 배열에 넣어주는 grid 함수는 배를 배치했을 때 배열 안에 값이 저장된다.

while(1)
{
print_ship
-> MapDraw
-> swich case 문을 이용하여 버튼 눌렀을 때 좌표 이동 및 배 배치
-> erase_ship
}

나. 부저 사운드 함수

float start3[]= {329.6, 329.6, 329.6, 293.7, 329.6, 0, 329.6, 329.6 ,329.6, 293.7, 392.0, 349.2, 329.6,-1}; // start 사운드
float end_win[] = {261.6, 329.6, 392.0, 523.3, -1}; // 승리 사운드
float end_lose[] = {277.2, 261.6, 246.9, 223.1,-1}; // 패배 사운드
float fire[]= {261.6, -1}; // 포격 성공 사운드
float wrecked[] = {130.8, 123.5, -1}; // 침몰사운드
float miss[] = {392.0 ,-1}; // miss 사운드
float scan2[] = {440.0, -1}; // scan 사운드
void Buzzer(float arr[])
{
int i = 0;
DDRB |=(1<<DDB4); //5번 핀으로 소리 출력
TCCR0 = 0x1D;
TCCR1A = 0×00;
TCCR1B = 0×04;
TCNT0 = 0;
TCNT1 = 65536 – 55000;

while(arr[i] != -1)
{
OCR0 = (int)(8000000/(arr[i]*256));
while((TIFR & 0×04) == 0);
TIFR |= 0×04;
TCNT1 = 61250;
//=====================
OCR0 = 0;
while((TIFR & 0×04) == 0);
TIFR |= 0×04;
TCNT1 = 55000;
i++;
}
TCCR0 = 0;
}

타이머/카운터 레지스터를 설정하고 while문의 인자에는 arr[i] 배열의 인자(주파수)를 하나씩 실행 시키는데 -1이 나오면 효과음이 끝나도록 설정 하였고 하나의 효과음이 모두 나온 후에 다음 동작(화면이 바뀌거나 스위치를 작동하는 행위)이 실행된다.
빠른 게임 진행을 위해서 게임 시작, 게임 종료를 나타내는 효과음 이외의 배열(arr[i])의 길이를 짧게 설정하였다.

다. 통신 방법

void Init_interrupt_usart(void) // 인터럽트 레지스터 설정
{
UCSR0A = 0×00;
UCSR0C = 0×06;
UCSR0B = 0xd8; // 1101 1000

UBRR0H = 0×00;
UBRR0L = 0×07; // 115200bps
SREG|=0×80;
}
ISR(USART0_TX_vect) // led 제어, 끝나는거 제어
{
tx_done=0;
}

ISR(USART0_RX_vect)
{
data = UDR0;

if(data==1) ////////////////////////
led_turn=1;
if(data==0) ////////////////////////
led_turn=0;

if(data==Game_set)
{
end_flag=1;
loss_cnt++;
}
if(data == re_game)
re_game_flag2=1;

if(data == C_zero)
C_zero_flag=1;

if(data == B_zero)
B_zero_flag=1;

if(data == S_zero)
S_zero_flag=1;

if(data == M_zero)
M_zero_flag=1;

if(data == D_zero)
D_zero_flag=1;
}

인터럽트 ISR(USART0_TX_vect)은 메인문에서 UDR0 값을 넣어줬을 때 tx_done=0 으로 만들면서 ISR을 빠져나가게 되면서 while(tx_done);을 다음 코드로 진행할 수 있게 구성하였다.
인터럽트 ISR(USART0_TX_vect)에서 정보는 최소화 하였고 보내는데 필요한 정보들은 flag 변수를 이용하여 주로 처리하였다.
인터럽트 ISR(USART0_RX_vect)은 UDR0 값을 받아 변수 data에 저장 후 필요한 flag를 1로 set 하는 용도로 쓰였다.

5.2. 아쉬운 점
블루투스 페어링(pairing) 오류

58 ict 배틀쉽 (8)

기존 작품 설계 방향은 무선통신을 이용한 2인용 게임기를 만드는 것 이 목표였기 때문에 아두이노를 이용하여 블루투스를 페어링 하려 했지만 블루투스 모듈 문제로 페어링이 되지 않아 유선통신을 하게 되었다. 그림은 블루투스 모듈인 hc-05와 hc-06을 페어링 하고 있는 모습이다.

휴대하기에는 큰 부피
기존에 보유하고 있던 큰 크기의 브레드보드를 기준으로 회로를 설계하고 게임기 외형을 제작하였기 때문에 휴대하기에는 큰 부피로 작품이 제작되었다.

시간제한 기능 미사용
타이머 인터럽트를 사용해서 사용자가 공격하는데 30초의 시간제한을 두려 하였지만 여러 개의 인터럽트를 동시에 사용하여 충돌이 생겨 타이머가 제대로 작동하지 않았다.
아래 내용은 타이머 인터럽트만을 사용하여 GLCD에 30초 타이머 기능을 구현한 코드와 코드의 설명이다.

ISR(TIMER0_COMP_vect) // 타이머 ISR (30초 카운트)
{
total_count++;
if ( total_count == 1000 ) // total_cound는 1ms
{
if(++sec_first>9){ s=0; // 1초 단위
if(++sec_second>2){ s1=0; //10초 단위
}
}
//sec_first와 sec_second는 전역변수
GLCD_TextGoTo(2,1); GLCD_WriteChar(sec_first+’0’); // GLCD에 1초 단위 표기
GLCD_TextGoTo(1,1); GLCD_WriteChar(sec_second+’0’); // GLCD에 10초단위 표기
_delay_ms(1);

total_count = 0;
}
}

void Init_Timer(void) // 타이머/카운터0 레지스터 설정을 통한 타이머 Init
{
TCCR0 |= (1<<CS02); // 프리스케일러로 64를 선택
TCCR0 |= (1<<WGM01); // CTC 모드
TCNT0 = 0; // 카운터 초기화
OCR0 = 249; // TOP 정의
TIMSK |= (1<<OCIE0); // 타이머/카운터0 Compare Match 인터럽트 활성화
sei(); // 전역 인터럽트 활성화
total_count = 0; // total_count 초기화
}

void Quit_Timer(void) // Timer 레지스터 설정을 통한 Timer Quit
{
TIMSK = 0×00; // 타이머/카운터의 interrupt를 비활성화
sec_first = 0; // sec_first를 초기화
sec_second = 0; // sec_second를 초기화
total_count = 0; // total_count를 초기화
}

타이머 기능: Init_Timer에서 타이머/카운터0의 레지스터 설정을 통해 1ms마다 total_count가 1씩 증가하는 타이머를 만들었다. 각 레지스터의 설정은 다음과 같다. Init_Timer함수의 주석을 통해서 설명되어 있다.
Init_Timer를 통해서 타이머/카운터0이 활성화 되면 TCNT가 OCR과 같아지는 순간 타이머0 ISR(TIMER0_COMP_vect)로 진입하여total_count가 1ms마다 증가하며, total_count가 1000이 되는 순간 sec_first를 1씩 증가시킨다. sec_fisrt가 10이 되는순간 sec_first는 0으로 초기화되고, sec_second가 1씩 증가한다. sec_second가 3이되는 순간 sec_second는 0으로 초기화 된다.
코드를 통해, 위 내용을 확인할 수 있으며, sec_first와 sec_second는 각각 1초 단위 10초 단위를 의미한다.
종합 의견 : 완성된 작품에서 무선통신과 타이머 기능을 추가하고 작은 크기의 브레드보드를 사용하여 외형을 제작한다면 보다 편리하고 직관적인 게임기의 기능을 갖추게 될 것이다.

5.3. 참고문헌
· 뉴티씨 (newtc) : http://www.newtc.co.kr/dpshop/bbs/board.php?bo_table=m41&wr_id=745&page=5
· 인터럽트 레지스터 설정 : https://webnautes.tistory.com/984
· USART 통신 : https://blog.naver.com/xisaturn/220750649418
· 그림 1,2 battleship 보드게임 예시 : https://en.wikipedia.org/wiki/Battleship_(game)
· 그림 3. ATmega128 data sheet : http://www.siphec.com/item/AVR-ATmega128-ATmega1281-ATmega2561.html
· 그림 6. 주파수와 피에조 부저의 음계 : https://kocoafab.cc/tutorial/view/626
· 그림 7. ATmega USART Tx/Rx 연결 회로도 : https://blog.naver.com/iintuition_/220587315291

 

 

 

 

Leave A Comment

*