'DevSource'에 해당되는 글 20건
- 2012.12.27 갤러리에서 사진 얻어오기
- 2012.12.27 모바일 맞고 게임10(RMS를 이용한 게임 데이터 세이브/로드)
- 2012.12.27 모바일 맞고 게임9(사운드 출력과 진동)
- 2012.12.27 모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현)
- 2012.12.27 모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현)
- 2012.12.27 모바일 맞고 게임7-2(디스플레이 요소 추가 #2)
- 2012.12.27 모바일 맞고 게임7-1(디스플레이 요소 추가 #1)
- 2012.12.27 모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 )
- 2012.12.27 모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현)
- 2012.12.27 모바일 맞고 게임6-1(맞고 게임룰 설명)
모바일 맞고 게임10(RMS를 이용한 게임 데이터 세이브/로드)
DevSource/맞고 2012. 12. 27. 22:21 |드디어 이 맞고 게임 강좌의 마지막 시간입니다.
이번 강좌에서는 RMS에 게임 데이터를 읽고 쓰는 시간을 갖도록 하겠습니다.
RMS(Record Management System)이란, 사용자의 핸드폰에 데이터를 읽고 Record Store라는 DB를 생성하고,
그 곳에 바이너리 파일 형식으로 핸드폰 상에 비휘발성 장소에 저장되어 영구히 보존됩니다.
따라서 RMS에 저장된 데이터는 핸드폰을 껐다 켜도 데이터가 그대로 남아 있게됩니다.
RMS에 데이터를 저장하려면 보통 다음과 같은 순서를 거칩니다.
* 데이터를 저장할 Record Store 생성.
* Record Store를 연다.
* 현재 오픈된 Record Store에 데이터 기록.
* Record Store를 닫는다.
Record Store는 byte 배열 형태로 구성되어 있으며, 데이터를 저장하기 위한 공간입니다.
이 Record Store는 하나의 프로그램에서 몇개라도 생성이 가능하지만, 각각의 Record Store를 식별하기 위해
존재하는 Rrecord Store의 이름은 중복되어서는 안됩니다.
먼저 Record Store에 데이터를 읽고 쓰기 위한 twogo1p.GameInfo 클래스를 추가합니다.
/**
* Constructor.
*/
public GameInfo()
{
money = new long[2]; // 0: 플레이어 / 1: 컴퓨터.
initDefaultValue(); // 기본 설정 저장.
rsName = "TwogoInfo";
/*
* Record store 생성.
*/
try {
recordStore = RecordStore.openRecordStore(rsName, false);
readGameInfo(); // 이미 RS가 존재한다면, Game Info를 읽어옴.
}
catch (RecordStoreNotFoundException e) {
// RS가 존재하지 않는다면, RS를 만들고 기본값 저장.
saveGameInfo();
}
catch (RecordStoreFullException e) {}
catch (RecordStoreException e){}
}
|
GameInfo 클래스의 생성자에서는 Record Store(이하 RS)가 이미 존재하는지 체크 하고,
이전에 만들어진 RS가 없다면, 새로운 RS를 생성하고 디폴트 값의 데이터를 저장합니다.
RS에 저장할 데이터는 돈(플레이어/컴퓨터), 사운드 가능 여부, 진동 가능여부. 이렇게 4가지입니다.
먼저, String형 변수인 rsName을 만들고 RS의 이름을 변수로 관리 할수있도록 합니다.
다음은 RS를 생성하는 부분인데,
/* * Record store 생성. */ try { recordStore = RecordStore.openRecordStore(rsName, false); readGameInfo(); // 이미 RS가 존재한다면, Game Info를 읽어옴. } catch (RecordStoreNotFoundException e) { // RS가 존재하지 않는다면, RS를 만들고 기본값 저장. saveGameInfo(); } catch (RecordStoreFullException e) {} catch (RecordStoreException e){} |
RecordStore를 생성할때, 다음과 같이 openRecordStore() 라는 메소드를 사용합니다.
RecordStore recordStore = null;
recordStore = RecordStore.openRecordStore(rsName, false);
그런데 여기서 oepnRecordStore() 메소드의 마지막 인자를 true로 준다면, RS가 존재하지 않을때,
그냥 아무말 없이 자동으로 새로운 RS를 생성합니다.
그러나 마지막 인자를 false로 준다면, RS가 존재하지 않을때, RecordStoreNotFoundException이 발생하죠.
따라서, 먼저 false값을 인자로 주어서 RS 오픈을 시도한 뒤 RS가 존재한다면 RS에서 정보를 읽어오고,
RS가 존재하지 않는다면, RS를 새로 생성하고, 그 안에 기본 정보를 저장해 주어야 하겠지요.
먼저, saveGameInfo() 메소드를 보겠습니다.
/** * Record store가 존재하지 않으면, * 새로 Record store를 생성하고 기본값 기록. */ |
맨 윗줄의 startWrite()는, 인자로 String형 변수인 rsName을 받아서 rsName이라는 RS를 생성합니다.
이 startWrite() 함수는 잠시 후에 설명할 DataManager라는 클래스에 정의되어 있습니다.
RS에 데이터를 기록하는 것은 의외로 쉽습니다.
단순히, DataOutputStream 객체의 writeLong()/writeInt()/writeShor() .... 과 같이 기록하고자 하는 형과 일치하는
메소드를 호출해서 데이터를 기록해 주면 됩니다.
그러나, 여기서는 따로 writeXXX()/readXXX()를 정의해서 DataOutputStream 객체의 wrapper 메소드를 정의했습니다.
따라서 우리가 사용할 형에 따라서 writeLong()/writeInt()와 같이 기록해 주면 됩니다.
그 다음에 나오는 readGameInfo() 메소드 역시 마찬가지 입니다.
/** * Record store에서 데이터를 읽어옴. */ |
readLong()/readInt() 등의 메소드들은 DataInputStream의 객체의 readXXX() 메소드들의 wrapper 메소드입니다.
RS에서 데이터를 읽어올때에는 RecordStore 객체의 enumerateRecords() 메소드를 이용해서,
RecordEnumeration의 객체를 얻어오고, RecordEnumeration 객체의 hasNextElement() 메소드로,
뒤에 남은 데이터가 더 있는지 체크해서, 루프를 돌면서 계속해서 읽어올 수가 있습니다.
그러나, 사실... 우리 프로그램에서는 저장할 변수가 4개(돈,사운드,진동)로 한정되어 있기 때문에,
사실 RecordEnumeration객체를 얻어올 필요는 없지만, RMS를 사용하는 예를 보여드리려고 위와같이 해주었습니다.
좀더 우리 프로그램에 맞게 고친다면, while문 없이 while문 안의 내용만 있으면 되겠죠.
다음으로 , RS에 데이터를 기록하는 작업을 도와주는 common.DataManager 클래스를 만들어 봅시다.
package common; import java.io.*; import javax.microedition.rms.*; |
먼저, RS에 데이터를 기록하기 전에 호출하는 startWrite() 메소드를 보면,
/**
* RMS에 데이터를 기록할 준비를 함.
*/
protected void startWrite(String rsName)
{
try {
recordStore = RecordStore.openRecordStore(rsName, true);
}
catch (RecordStoreNotFoundException e) {}
catch (RecordStoreFullException e) {}
catch (RecordStoreException e){}
baos = new ByteArrayOutputStream();
dos = new DataOutputStream(baos);
} |
처음 부분에서는 단순히 처음에 RS를 생성해 주는 것 밖에 없습니다.
그 다음에 ByteArrayOutputStream 객체와 DataOutStream 객체를 생성해 주는 부분이 있는데,
앞에서 말씀 드렸다시피, RS는 바이트 배열로 이루어져 있기 때문에, RS에 데이터를 기록하기 위해서는
데이터를 바이트 배열로 변경해줄 필요가 있습니다.
따라서, 위와같이 dos와 baos를 선언하게 되면, dos에 데이터를 기록하면,
baos에 이 데이터를 바이트형 배열로 변환되어 기록이 됩니다.
따라서, 데이터를 모두 삽입 한 후에 baos의 내용을 RS에 복사해주기만 하면 끝나게 되는 것이지요.
그러한 역할을 하는 것이 바로 endWrite() 메소드 입니다.
/**
* baos의 내용을 RS에 복사 후, RS를 닫는다.
*/
protected void endWrite()
{
byte[] byteArray = baos.toByteArray();
for(int i=0; i<byteArray.length; i++)
System.out.println(byteArray[i]);
try {
// bos에 기록한 내용을 RS에 추가한다.
recordStore.addRecord(byteArray, 0, byteArray.length);
} catch(RecordStoreException rse) {}
closeRS();
dos = null;
baos = null;
recordStore = null;
System.gc();
} |
앞서 말씀드린 내용이 그대로 들어가 있죠?
boas에 바이트 배열 형태로 기록이 된 데이터를 RecordStore 객체의 addRecord() 메소드를 이용해서 기록합니다.
addRecord() 메소드의 인자들은....
앞에서부터 차례로 삽입할 ByteArrayOutputStream 객체, 시작 index, 마지막 index입니다.
기록이 끝났다면, closeRS() 메소드로 오픈되어있는 RS를 닫고, 객체들을 초기화 합니다.
/** * 현재 열려있는 Record store를 닫는다. */ protected void closeRS() { try{ recordStore.closeRecordStore(); } catch(RecordStoreException rse) {} } /** * 특정 RS를 삭제한다. * @param recordStoreName */ protected void deleteRS(String recordStoreName) { try{ RecordStore.deleteRecordStore(recordStoreName); } catch(RecordStoreException rse) {} } |
다음에 나오는 closeRS() 와 deleteRS()는, RS를 닫고, 삭제 하는 역할을 하는 메소드입니다.
뭐 이거야, RecordStore 객체의 메소드들을 호출해 준 것 뿐이므로, 별다른 설명은 필요 없으리라고 봅니다.
자, 이제 지금까지 만들어준 두개의 클래스 DataManager와 GameInfo만 있다면,
RMS에 데이터를 읽고 쓰는 것은 문제도 아닙니다.
그러나, 아직 게임 내에서 RMS에 기록된 데이터를 읽어와서 게임에 반영하는 부분은 구현이 안되어있네요.
데이터는 게임을 시작할때 읽어오고, 게임을 종료할때 변경된 데이터를 저장하는 구조로 합니다.
게임 데이터가 변경될 때마다 RS에 데이터를 읽고 쓰고를 반복한다면 오버헤드도 크고 파일이 잘못될 확율도 높죠.
따라서, 게임 중에 변경된 데이터는 따로 특정 변수에 기억만 하고 있다가, 게임이 끝날때에 변경된 값을 기록하는것이지요.
게임 데이터를 사용하는 곳은 GameView 클래스와, Option 클래스 뿐입니다.
왜냐하면, GameView 클래스에서는 사용자와 컴퓨터의 돈, 그리고 진동 가능 여부 만을 확인 하면 되고,
Option 클래스에서는 게임 설정을 변경해야 하므로 당연히 변경된 게임 데이터를 저장할 수 있어야하겠죠.
따라서, RMS에서 읽어온 게임 데이터는 기본적으로는 GameInfo 클래스에서 관리하며,
Midlet에서 GameInfo의 값을 리턴해 주거나 설정해주는 방식으로 하는 것이 좋겠네요.
다음은 GameInfo 클래스의 게임 데이터 관리용 메소드입니다.
/** * 금액 설정. * @param userType * @param money */ public void setMoney(int userType, long money) { this.money[userType] = money; } /** * 보유 금액 리턴. * @param userType * @return */ public long getMoney(int userType) { return money[userType]; } /** * 사운드 재생이 가능한가. * @return */ public boolean isPlayableSound() { return sound; } /** * 진동이 가능한가. * @return */ public boolean isPossibleVibration() { return vibration; } /** * mute 설정. * @param mute */ public void setMute(boolean mute) { sound = !mute; // 반대의 값을 집어넣어준다. } /** * 진동 설정. * @param bVib */ public void setVibration(boolean bVib) { vibration = bVib; } |
뭐 별것은 없고, 단순히 get은 변수 값을 리턴, set은 변수 값을 세팅 해 주는 것이지요.
twogo1p.Midlet에서도 마찬가지로, 위의 메소드들과 동일한 메소드 명을 갖는 메소드들이 선언이 되어 있으며,
차이점은 gameInfo 클래스의 함수를 불러온 다는 것 뿐입니다.
GameView 클래스의 init 부분에서는 Midlet.getMoney() 메소드를 이용해서 각 플레이어의 돈을 얻어오고,
게임이 끝나고 결과가 나올때, 변경된 돈을 Midlet.setMoney() 메소드를 이용해서 저장하는 방식으로 해주면 되겠죠.
< 환경 설정 화면 > |
< 게임 대기 화면 > |
위 이미지들은 모두 환경 설정을 한후, 게임을 한판 한후, 에뮬레이터를 종료 시켰다가 다시 게임을 실행 시킨 모습입니다.
그러나 여전히 환경설정은 이전에 설정했던 설정 그대로이며, 사용자와 컴퓨터의 돈 역시 그대로입니다.
최종 맞고 소스 다운 받기
휴... 이걸로 맞고 게임 강좌가 모두 끝났습니다.
맞고 게임 강좌는 모두 끝났지만, 맞고 게임은 완벽하지는 않습니다.
A.I.도 최대한 쉽게 설명하기 위해 최대한 간단히 코딩한 면이 없지 않기 때문에, 컴퓨터가 그렇게 똑독하지 않습니다.
나머지 부분들은 이제 여러분들이 지금 까지 배운 내용을 바탕으로 여러가지 요소들을 추가해 보시고,
이미지가 마음에 들지 않으시는 분들은 직접 웹에서 구한 이미지로 변경해 자신만의 맞고를 만들어 보시는 것도 좋겠네요.
강좌는 끝났지만, 질문/답변 게시판은 열려 있으니, 언제든 궁금 하신 사항이 있다면 질문 올려 주시길 바랍니다.
그럼 모두들 수고하셨습니다.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임9(사운드 출력과 진동) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임9(사운드 출력과 진동)
DevSource/맞고 2012. 12. 27. 22:20 |이번 시간에는 게임에서 빠져서는 안될 중요한 사운드를 출력해보는 시간을 갖도록 하지요.
사운드를 출력하는 부분 역시 SK-VM 자체에서 상당부분 라이브러리화 되어 있기 때문에,
우리는 단순히 라이브러리를 사용하는 방식만을 익혀주면 되겠습니다.
사운드를 재생/정지 하기 위한 SoundThread라는 클래스를 하나 작성해 보겠습니다.
* common.SoundThread.java.
package common; import java.io.InputStream; import com.skt.m.AudioClip; import com.skt.m.AudioSystem; /** * 소리를 낼때 사용하는 thread * 잠시 기다렸다가, 소리를 낼때 깨서 다시 기다리는 과정을 반복한다. */ public class SoundThread extends Thread { private AudioClip currentClip = null; private byte[] sndtmp; private byte[][] snd; private int currentIndex; private boolean bPlaying=false; private String path; private InputStream is = null; private boolean bReady; private boolean bOn; /** * Constructor #1. */ public SoundThread() { this(11); } /** * Constructor #2. * @param maxSound */ public SoundThread(int maxSound) { bOn = true; snd = new byte[maxSound][]; try { currentClip = AudioSystem.getAudioClip("mmf"); } catch(Exception e) {} } /** * mix해 놓은 사운드 data 파일의 경로를 설정해 놓는다. * @param path */ public void setSoundData(String path) { this.path = path; } public void cleanUp() { is = null; System.gc(); } /** * mix해 놓은 사운드 data를 해당 byte 배열에 읽어 놓는다. * @param index * @param off * @param len */ public void addSound(int index, int off, int len) { try { is = getClass().getResourceAsStream(path); snd[index] = new byte[len]; is.skip(off); is.read( snd[index], 0, len); is.close(); } catch (Exception e) { } } /** * 지정해 놓은 index로 사운드를 재생한다. * @param index */ public synchronized void play(int index) { closeClip(); waitForReady(); currentIndex = index; try { currentClip.open(snd[currentIndex], 0, snd[currentIndex].length); } catch (Exception e) { } bPlaying = true; this.notify(); } /** * 임시로 어떤 파일을 재생할때 사용한다. * @param path */ public synchronized void play(String path) { closeClip(); waitForReady(); try { is = getClass().getResourceAsStream(path); sndtmp = new byte[is.available()]; is.read( sndtmp, 0, is.available()); is.close(); currentClip.open(sndtmp,0,sndtmp.length); } catch(Exception e){ is = null; } currentIndex = -1; // 임시로 재생 하는 번호 bPlaying = true; this.notify(); } /** * 처리하고 있는 중이라면 잠시 기다린다. */ private void waitForReady() { int count=0; try { while(bReady==false) { this.wait(100); Thread.yield(); count++; if(count>=50) break; // sleep을 사용하면 안됨. } } catch(Exception e) {} } /** * 현재 재생중인 사운드를 정지한다. */ private void closeClip() { try { currentClip.stop(); } catch(Exception e) {} try { currentClip.close(); } catch(Exception e) {} } public void run() { while (true) { synchronized (this) { bReady = true; try { // 잠시 기다린다. while (!bPlaying) { this.wait(); } } catch (Exception e) {} bReady = false; } try { if(bOn==true) { // 사운드가 켜져 있을때만 소리를 낸다. currentClip.play(); } synchronized (this) { bPlaying = false; } } catch (Exception e) { bPlaying = false; } finally { sndtmp = null; try { currentClip.close(); } catch (Exception e) {} } } } /** * 사운드 재생 : On */ public void turnOn() { bOn = true; } /** * 사운드 재생 : Off */ public void turnOff() { bOn = false; } /** * 볼륨을 1 증가한다. */ public void volUp() { try { AudioSystem.setVolume("mmf", AudioSystem.getVolume("mmf")+1); } catch (Exception e) {} } /** * 볼륨을 1감소한다. */ public void volDown() { try { AudioSystem.setVolume("mmf", AudioSystem.getVolume("mmf")-1); } catch (Exception e) {} } /** * 볼륨을 설정한다. * @param volume */ public void setVol(int volume) { try { AudioSystem.setVolume("mmf", volume); } catch (Exception e) {} } /** * 현재 설정된 볼륨값을 전달한다. * @return 현재volume값 */ public int getVol() { int vol = 0; try { vol = AudioSystem.getVolume("mmf"); } catch (Exception e) {} return vol; } } |
위의 SoundThread 클래스를 추가하는 것만으로 사운드를 출력하기 위한 모든 준비는 끝납니다.
그럼 클래스를 좀더 자세히 살펴보도록 하지요.
먼저 생성자입니다.
/** * Constructor #1. */ public SoundThread() { this(11); } /** * Constructor #2. * @param maxSound */ public SoundThread(int maxSound) { bOn = true; snd = new byte[maxSound][]; try { currentClip = AudioSystem.getAudioClip("mmf"); } catch(Exception e) {} } |
생성자가 두개가 있는데요. Default 생성자를 호출하면 두번째 생성자로 11이라는 값을 넘겨 줍니다.
두번째 생성자는 인자로 받은 maxSound 만큼의 사운드 배열을 생성해 주는 역할을 합니다.
또, com.skt.m.AudioSystem 클래스의 getAudioClip() 메소드를 이용해서, mmf 오디오 클립을 얻어옵니다.
그리고 다음의 setSound() 메소드는, mix된 사운드 데이터 파일의 경로를 기억하는 메소드입니다.
/** * mix해 놓은 사운드 data 파일의 경로를 설정해 놓는다. * @param path */ public void setSoundData(String path) { this.path = path; } |
mix된 사운드 data 파일은, mmf 형식의 사운드 파일들을 LBMMIXER를 이용해서 sound.dat에 mix 해 놓았습니다.
LBMMIXER는 다들 기억 하고 계시리라고 생각합니다.
원래 LBM, PNG 파일을 dat 파일로 mix하는 용도의 유틸리티이지만, mmf 파일을 mix하는데도 사용될수 있습니다.
왜냐하면 이 LBMMIXER 자체가 이미지의 헤더를 별도로 수정하거나 하는 것이 아니고,
단순히 여러개의 파일들의 내용을 하나의 파일에 일렬로 기록해 주는 기능만을 하기때문에,
실제로 LBM 파일 사이즈의 합이 mix후 생성된 dat파일의 사이즈와 동일하죠.
그러므로, mmf 파일을 이 유틸리티를 이용해서 mix 한다고 해서 특별히 mmf 파일에 손상이 가거나 하는 일은 없겠죠.
그러나 LBMMIXER 자체에서 읽어들일 수 있는 확장자를 LBM, PNG로 고정시켜 놓았기 때문에
mmf 파일을 LBMMIXER에서 직접 읽어 올 수는 없습니다. 때문에, mmf 파일의 확장자를 lbm으로 변경 시킨 후,
LBMMIXER에서 읽어와서 mixing 버튼을 눌러주면 짜잔 하고 dat 파일이 생성이 되겠죠.
그렇게 해서 합쳐 놓은 파일이 바로 sound 폴더의 sound.dat 파일입니다.
/** * mix해 놓은 사운드 data를 해당 byte 배열에 읽어 놓는다. * @param index * @param off * @param len */ public void addSound(int index, int off, int len) { try { is = getClass().getResourceAsStream(path); snd[index] = new byte[len]; is.skip(off); is.read( snd[index], 0, len); is.close(); } catch (Exception e) { } } |
다음으로 위의 addSound() 메소드는,
사운드 배열 snd[]의 index에 sound.dat 파일의 offset부터 len 만큼을 byte 배열로 저장합니다.
/** * 지정해 놓은 index로 사운드를 재생한다. * @param index */ public synchronized void play(int index) { closeClip(); waitForReady(); currentIndex = index; try { currentClip.open(snd[currentIndex], 0, snd[currentIndex].length); } catch (Exception e) {} |
대망의 play 메소드입니다. 당연히 지정한 index의 사운드를 재생하는 역할을 하는 메소드이구요.
현재 재상할 클립(currentClip)을 오픈해서 notify()를 호출하면 사운드가 재생됩니다.
위에서 호출된 waitForReady()는 사운드를 출력할 준비가 아직 안되었다면,
사운드를 출력할 준비가 될때까지 루프를 돌면서 기다리는 역할을 하는 메소드입니다.
/** * 처리하고 있는 중이라면 잠시 기다린다. */ private void waitForReady() { int count=0; |
bReady가 false인 동안 계속해서 100 ms씩 간격을 두고 bReady가 true가 될때까지 기다린 후,
5초 이상(100ms * 50)을 기다렸는데도 bReady가 풀리지 않는 다면, 그냥 루프를 탈출합니다.
/** * 현재 재생중인 사운드를 정지한다. */ private void closeClip() { try { currentClip.stop(); } catch(Exception e) {} try { currentClip.close(); } catch(Exception e) {} } |
closeClip() 메소드는 현재 재생중인 사운드를 정지시켜 줍니다.
현재 재생중인 사운드를 정지(stop())하고, 열려 있는 클립을 닫아주면(close()) 끝나죠.
이제 사운드를 출력하기 위한 준비는 모두 끝났습니다. 이제 준비된 SoundThread 클래스를 사용하기 위해
twogp1p 패키지의 Midlet 클래스에, initSound() 라는 메소드를 정의해줍니다.
/** * 사운드를 설정한다. */ private void initSound() { sound.setSoundData("/sound/sound.dat"); sound.addSound(SND_ABELL,0,199); sound.addSound(SND_CARD, 199,510); sound.addSound(SND_CHOICE,709,203); sound.addSound(SND_GAMEOVER,912,2931); sound.addSound(SND_GAMESTARTED,3843,4016); sound.addSound(SND_GO,7859,1424); sound.addSound(SND_LONGBELL,9283,203); sound.addSound(SND_MATCH,9486,728); sound.addSound(SND_MYTURN,10214,1632); sound.addSound(SND_NOMATCH,11846,593); sound.addSound(SND_VOLUMEKEY,12439,148); sound.cleanUp(); } |
addSound 메소드를 이용해서 각 인덱스 별로 오프셋과 mmf의 사이즈(byte 단위)를 지정해 줍니다.
각각의 사운드 인덱스 상수 별로 간단히 사운드의 용도를 설명드리자면,
SND_ABELL | : 짧은 확인 사운드 |
SND_LONGBELL | : 긴 확인 사운드 |
SND_CHOICE | : 플레이어의 턴에서 고/스톱, 흔듬/폭탄, 국진등의 선택을 해야할때 출력 |
SND_MYTURN | : 플레이어의 턴일때 출력 |
SND_CARD | : 카드를 나눠 줄때 출력 |
SND_MATCH | : 카드를 냈을때 바닥에 동일한 카드가 있을때 출력 |
SND_NOMATCH | : 카드를 냈지만 바닥에 동일한 카드가 없을때 출력 |
SND_GO | : 고를 했을때 출력 |
SND_RESULT | : 게임이 끝나고 결과가 출력될때 출력 |
SND_VOLUMEKEY | : 볼륨키를 눌렀을때 출력 |
이제 사운드를 출력하고 싶다면 다음과 같이 play() 메소드에 인덱스 상수를 넘겨주면 됩니다.
sound.play( 사운드 인덱스 상수 ); |
단순히 위의 한 문장을 삽입한 것이므로 따로 설명을 할 필요는 없을 것 같네요.
위의 상황이 발생하는 곳에다가 play() 메소드를 이용해서 사운드를 출력해 주시면 되리라고 봅니다.
이제 다음으로는 진동을 출력하는 방법을 배워보죠.
진동은 사용자나 커퓨터가 흔들었을때, 폭탄했을때, 쪽/뻑/따닥이 났을때, 쓸일때 등에서 진동이 발생합니다.
진동을 하기 위해서는 SK-VM에서 제공하는 Vibration 이라는 클래스를 이용하면 간단히 해결됩니다.
이 게임에서 진동을 사용할 일은 위에서 열거한 사항들(흔듬/폭탄/쪽/뻑/따닥/쓸) 뿐이므로,
진동이 사용되는 곳이 GameView 클래스로 한정됩니다. 그러므로 GameView 클래스에,
import com.skt.m.Vibration; |
위와 같이 Vibration 클래스를 import 해 주기만 하면 됩니다.
이제 진동을 사용하기 위해서는,
Vibration.start(level, timeout); |
level은 진동의 강도인데, 아직 SK-VM에서 지원은 하지 않고, 임시로 만들어져 있는 것이므로,
현재는 그냥 단순히 0으로 지정을 해 주시면 됩니다.
timeout은 진동이 울릴 시간을 입력해 주시면 되며, 단위는 ms 입니다.
예를 들어 진동을 300 ms 동안 울리게 하고 싶다면,
Vibration.start(0, 300); |
위와 같이 해 주시면 되겠죠?
이걸로 사운드 출력과 진동 처리는 모두 알아본 것 같네요.
이제 사운드 출력과 진동을 처리 해 주면, 게임에서 멋진 사운드가 흘러 나올겁니다.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임10(RMS를 이용한 게임 데이터 세이브/로드) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현)
DevSource/맞고 2012. 12. 27. 22:19 |두장의 카드 중에서 하나의 카드를 선택해야 할때...
☆ ☆ ☆ ☆ ☆ ☆ |
/**
* 카드 둘중 하나를 선택할때...
* @param g
*/
private void drawSelectTwoCards(Graphics g)
{
byte index;
drawRunningGame(g);
/* 카드 두장 중 하나를 선택한다. */
dialog.drawBorder(g, CyDialog.SELECTTWOCARD);
// 두 종류의 카드를 보여줌.
if(room.slot[0] != room.CARD_NULL)
index = 0;
else if(room.slot[1] != room.CARD_NULL)
index = 1;
else return;
drawCard(40, 66, room.getMonth(room.slot[index])[0]);
drawCard(65, 66, room.getMonth(room.slot[index])[1]);
// 컴퓨터 턴이라면 AI가 선택.
if(curTurn == User.COMPUTER)
{
Sleep.sleep(500);
selectTwoCards(index, User.COMPUTER,
agent.selectTwoCards(index));
}
} |
두장의 카드 중에서 하나의 카드를 선택 하는 장면을 그려주는 함수인 drawSelectTwoCards() 함수에서
처리를 해 주는 것이 가장 좋겠죠. 그림을 그려준 후에 만약 현재 턴이 컴퓨터 턴이라면,
Agent 클래스의 selectTwoCards() 함수를 호출해서 A.I.가 카드를 선택할 수 있도록 해 줍니다.
Agent 클래스의 selectTwoCards() 함수를 한번 보도록 하죠.
/**
* A.I.가 두 장의 카드 중 하나를 선택한다.
* @return
*/
public byte selectTwoCards(byte index)
{
byte i;
/* 광 체크. */
for(i=0; i<2; i++)
{
// 두개의 패중에 광이 있다면,
if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_KWANG
|| gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_BIKWANG)
{
System.out.println(" # selectTwoCards MSG : 광 발견!");
return (byte)(i+1);
}
}
/* 고도리, 청단, 홍단, 초단 체크 */
for(i=0; i<2; i++)
{
// 고도리 검색 허용 상태이거나 상대방이 고도리를 두장 이상 가지고 있다면,
if(bNoFindGodori == false
|| (room.getCntGodori(Room.PLAYER) >= 2
&& room.getCntGodori(Room.COMPUTER) == 0))
{
if(room.getCntGodori(Room.PLAYER) >= 1)
{
bNoFindGodori = true;
}
else if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_GODORI)
{
// 두개의 패중에 고도리가 있다면,
System.out.println(" # selectTwoCards MSG : 고도리 발견!");
return (byte)(i+1);
}
}
// 청단 검색 허용 상태이거나 상대방이 청단을 두장 이상 가지고 있다면,
if(bNoFindChungDan == false
|| (room.getCntChungDan(Room.PLAYER) >= 2
&& room.getCntChungDan(Room.COMPUTER) == 0))
{
if(room.getCntChungDan(Room.PLAYER) >= 1)
{
bNoFindChungDan = true;
}
else if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_CHUNGDAN)
{
// 두개의 패중에 청단이 있다면,
System.out.println(" # selectTwoCards MSG : 청단 발견!");
return (byte)(i+1);
}
}
// 홍단 검색 허용 상태이거나 상대방이 홍단을 두장 이상 가지고 있다면,
if(bNoFindHongDan == false
|| (room.getCntHongDan(Room.PLAYER) >= 2
&& room.getCntHongDan(Room.COMPUTER) == 0))
{
if(room.getCntHongDan(Room.PLAYER) >= 1)
{
bNoFindHongDan = true;
}
else if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_HONGDAN)
{
// 두개의 패중에 홍단이 있다면,
System.out.println(" # selectTwoCards MSG : 홍단 발견!");
return (byte)(i+1);
}
}
// 초단 검색 허용 상태이거나 상대방이 초단을 두장 이상 가지고 있다면,
if(bNoFindChoDan == false
|| (room.getCntChoDan(Room.PLAYER) >= 2
&& room.getCntChoDan(Room.COMPUTER) == 0))
{
if(room.getCntChoDan(Room.PLAYER) >= 1)
{
bNoFindChoDan = true;
}
else if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_CHODAN)
{
// 두개의 패중에 초단이 있다면,
System.out.println(" # selectTwoCards MSG : 초단 발견!");
return (byte)(i+1);
}
}
}
/* 쌍피 체크 */
for(i=0; i<2; i++)
{
// 두개의 패중에 쌍피가 있다면,
if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_SSANG_PEE)
{
System.out.println(" # selectTwoCards MSG : 쌍피 발견!");
return (byte)(i+1);
}
else if(room.getMonth(room.slot[index])[i] == gCard.CARD_GOOKJIN)
{
// 국진이 있다면, 국진을 먹는다.
System.out.println(" # selectTwoCards MSG : 국진 발견!");
return (byte)(i+1);
}
}
/* 열끗 체크 */
if(room.getNumOfYeul(Room.COMPUTER) >= 5
&& room.getNumOfYeul(Room.PLAYER) <= 2)
{
/* 멍따 가능성이 있다면, 피보다는 열끗을 먹어준다. */
for(i=0; i<2; i++)
{
// 두개의 패중에 열끗이 있다면,
if(gCard.getType(
room.getMonth(room.slot[index])[i]) == CARDTYPE_YEUL)
{
System.out.println(" # selectTwoCards MSG : 열끗 발견!");
return (byte)(i+1);
}
}
}
/* 피 체크 */
for(i=0; i<2; i++)
{
// 두개의 패중에 피가 있다면,
if(gCard.getType(room.getMonth(room.slot[index])[i]) == CARDTYPE_PEE)
{
System.out.println(" # selectTwoCards MSG : 피 발견!");
return (byte)(i+1);
}
}
/* 그 외에는 둘중 아무거나 선택. */
System.out.println(" # selectTwoCards MSG : 암거나 냈음!");
return (byte)(Etc.getRandomInt(2)+1);
}
|
역시 마찬가지로 주욱 여러가지의 상황을 나열해 놓고 비교 하고 있습니다.
순서는 Agent.run() 함수와 동일합니다.
두개의 카드 중 광이나 비광이 있다면 그것을 선택해 주고,
청단, 홍단, 초단이 있다면, 먹을 필요가 있는지 먼저 체크 후, 그것을 선택해 줍니다.
그리고 그 다음으로는 쌍피가 있는지 체크하고, 열끗이 있다면, 멍따 가능성이 있을대에는 열끗을 먹고,
멍따 가능성이 별로 없다면, 그냥 일반 피를 선택해 줍니다.
이전과 마찬가지로 위의 모든 상황에 해당되는 것이 없다면, 아무거나 렌덤으로선택 해 주는 것이고요.
흔듬/폭탄의 여부를 선택해야 할때...
☆ ☆ ☆ ☆ ☆ ☆ |
/**
* 흔듬/폭탄 여부를 선택 중일때...
* @param g
*/
private void drawSelectShake(Graphics g)
{
drawRunningGame(g);
if(shakeType == SHAKE) // 흔듬 선택 중일때,
dialog.drawBorder(g, CyDialog.SELECTSHAKE);
else /* if(shakeType == BOMB) */ // 폭탄 선택 중일때,
dialog.drawBorder(g, CyDialog.SELECTBOMB);
// 컴퓨터 턴일땐 AI가 선택.
if(curTurn == User.COMPUTER)
{
Sleep.sleep(500);
selectShake(User.COMPUTER, agent.selectShake());
}
} |
역시 앞의 drawSelectTwoCards()와 마찬가지로,
현재 턴이 컴퓨터의 턴일때에는, agent.selectShake() 함수를 수행해서 A.I.가 선택할 수 있도록 합니다.
그러나... 흔듬/폭탄의 선택은 다음과 같습니다.
/** * A.I.가 흔듬 여부를 선택한다. * @return */ public byte selectShake() { // 무조건 흔듬(or 폭탄). return 1; } |
너무 간단하죠?
대부분, 흔듬을 선택하고, 흔들지 않아야 할 이유가 별로 없는 것 같아서, 무조건 흔듬으로 지정했습니다.
국진의 사용 여부를 선택해야 할때...
☆ ☆ ☆ ☆ ☆ ☆ |
/**
* 국진의 사용을 선택 중일때,
* @param g
*/
private void drawSelectGookjin(Graphics g)
{
drawRunningGame(g);
dialog.drawBorder(g, CyDialog.SELECTGOOKJIN);
// 컴퓨터 턴일땐 AI가 선택.
if(curTurn == User.COMPUTER)
{
Sleep.sleep(500);
selectGookjin(User.COMPUTER, agent.selectGookjin());
}
} |
역시 이전과 마찬가지로 현재 턴이 컴퓨터의 턴일때에는 agent.selectGookjin()을 호출합니다.
|
국진을 선택하는 것도 그렇게 복잡하지는 않습니다.
열이 다섯장 이상이고, 플레이어가 열을 2장 이하로 가지고 있다면, 멍따의 가능성이 있겠죠?
왜냐하면, 열이 아홉장인데, 컴퓨터가 다섯장을 가지고 있다고 해도, 플레이어가 이미 4장을 가지고 있다면,
멍따의 가능성이 전혀 없으므로, 굳이 열끗을 먹을 필요가 없겠지요.
멍따의 가능성이 없을때에는 국진패는 무조건 쌍피로 처리를 합니다.
고/스톱을 선택해야 할때...
/**
* A.I.가 고/스톱을 선택한다.
* @return
*/
public byte selectGoStop()
{
room.getNumOfCard();
// 플레이어의 광이 4장 이상일때,
if(room.getNumOfKwang(Room.PLAYER) >= 4)
{
// 컴퓨터가 광이 한장도 없다면, (오광 위험!)
if(room.getNumOfKwang(Room.COMPUTER) == 0)
return 2; // 스톱.
}
// 고도리 위험이 있다면,
if(room.getCntGodori(Room.PLAYER) >= 2
&& room.getCntGodori(Room.COMPUTER) == 0)
{
// 점수가 0점이 아니라면 스톱.
if(room.getRealPoint(Room.PLAYER) != 0)
return 2; // 스톱.
}
// 청단 위험이 있다면,
if(room.getCntChungDan(Room.PLAYER) >= 2
&& room.getCntChungDan(Room.COMPUTER) == 0)
{
// 점수가 2점 이상이라면 스톱.
if(room.getRealPoint(Room.PLAYER) >= 2)
return 2; // 스톱.
}
// 홍단 위험이 있다면,
if(room.getCntHongDan(Room.PLAYER) >= 2
&& room.getCntHongDan(Room.COMPUTER) == 0)
{
// 점수가 2점 이상이라면 스톱.
if(room.getRealPoint(Room.PLAYER) >= 2)
return 2; // 스톱.
}
// 초단 위험이 있다면,
if(room.getCntChoDan(Room.PLAYER) >= 2
&& room.getCntChoDan(Room.COMPUTER) == 0)
{
// 점수가 2점 이상이라면 스톱.
if(room.getRealPoint(Room.PLAYER) >= 2)
return 2; // 스톱.
}
// 피가 10장 이상이고,
if(room.getNumOfPee(Room.PLAYER) >= 10)
{
// 열이 7개 이상이라면,
if(room.getNumOfYeul(Room.PLAYER) >= 7)
return 2; // 스톱.
// 플레이어의 열과 띠가 5장 이상이라면,
if(room.getNumOfYeul(Room.PLAYER) >= 5
&& room.getNumOfDdee(Room.PLAYER) >= 5)
return 2;
// 플레이어의 열이 5장 이상이고, 점수가 3점 이상이라면,
if(room.getNumOfYeul(Room.PLAYER) >= 5
&& room.getNumOfYeul(Room.COMPUTER) <= 2
&& room.getRealPoint(Room.PLAYER) >= 3)
return 2; // 스톱.
// 플레이어의 띠가 5장 이상이고, 점수가 3점 이상이라면,
if(room.getNumOfDdee(Room.PLAYER) >= 5
&& room.getNumOfDdee(Room.COMPUTER) <= 2
&& room.getRealPoint(Room.PLAYER) >= 3)
return 2; // 스톱.
}
// 피가 13장 이상이라면,
if(room.getNumOfPee(Room.PLAYER) >= 13)
return 2; // 스톱.
// 플레이어의 점수가 4점 이상이라면,
if(room.getRealPoint(Room.PLAYER) >= 4)
return 2; // 스톱.
// 남은 카드가 한장이거나 혹은 없다면,
if(room.numOfComputerCard <= 1)
return 2; // 스톱.
// 그 외엔 무조건 고.
return 1;
} |
가능하다면, 컴퓨터가 먼저 고를 했다가, 고박을 당하고 지게 된다면, 안되겠죠.
그렇다고 너무 고를 안하고, 원고에서 바로 스톱을 하거나 하면 게임이 재미가 없어질 것이구요.
따라서 최대한 고를 하면서도 안전하게 게임을 진행 할 수 있도록 구현을 해야합니다.
위의 상황 체크는 지극히 제 개인적인 주관에 의해서 체크하는 상황이므로, 따로 설명은 하지 않겠습니다.
게다가 어차피 주석으로 다 설명이 되어 있으니...
마음에 들지 않는 상황이나, 추가 하고 싶은 상황이 있다면, 삭제하거나 상황을 더 추가할 수도 있겠죠.
Alpha-Beta Search에서 한가지 중요한 점은 중요한 상황일 수록 앞에 위치 해야 한다는 것입니다.
별로 중요하지도 않은 상황이 앞에 있다면, 뒤에 더 중요한 상황은 체크도 못해본 채로 함수가 끝나겠죠?
강좌 지면상이라는 이유로 A.I. 프로그래밍을 완벽하게 구현하지는 않았습니다.
딱 보시면 아시곘지만, 모든 상황을 다 체크하고 있는 것은 아니니까요.
그러나, 이렇게 별로 많이 추가하지 않았는데도 컴퓨터가 꽤 합니다. 가끔가다가 한판은 집니다^^;
상황을 더 많이 추가해준다면 컴퓨터가 그 만큼 똑똑해 지겠죠.
이게 바로 Alpha-Beta Search의 장점인 동시에 치명적인 단점이죠...
이번 시간의 화면 인터페이스 자체는 지난시간과 동일하므로, 스크린 샷은 생략하도록 하겠습니다.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임10(RMS를 이용한 게임 데이터 세이브/로드) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임9(사운드 출력과 진동) (0) | 2012.12.27 |
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현)
DevSource/맞고 2012. 12. 27. 22:18 |지난 시간까지 완성한 소스 만으로도 어느 정도의 맞고 게임 진행은 가능했었습니다.
이번 시간에는 지금까지 만든 게임에 A.I.를 추가해서 좀더 그럴듯 한 게임을 만들어보도록 하지요.
A.I. 라는 것이 정말 세밀하고 복잡하게 만들 자면, 한도 끝도 없는 분야입니다.
그러나 단순한 맞고 게임에서 그 정도의 세밀한 A.I.까지 사용할 필요는 없겠지요.
간단하게 만들자면, 그냥 우리가 맞고 게임을 할때 생각하는 과정을 그대로 코딩해 주면 되겠죠?
즉, 예를 들자면 이러한 경우가 있을 수 있겠네요.
바닥에 쌍피인 똥과 일반피인 똥이 있을때, 일반적인 경우라면 일부러 일반 피를 먹을 일은 없겠죠?
당연히 쌍피를 먹어야 합니다.
또, 이런 경우도 있을 수 있습니다.
국진 패를 먹었습니다. 그렇다면 국진 패를 열끗으로 써야 할까요, 쌍피로 써야 할까요?
쌍피가 좋으니까 그냥 무조건 쌍피로 써야 할까요? 아니면 무조건 열끗으로?
이건 그냥 함부로 결정할 수 있는 문제는 아니겠죠.
그때 그때의 상황에 따라서 적절한 판단을 해 주어야 합니다.
즉, 열끗이 5장 이상 있다면, 멍따의 가능성이 있으므로, 열끗으로 사용해주는 것도 좋겠죠.
그러나 열끗이 몇장 없다면, 쌍피로 사용한다면 피 두장과 같은 효과를 가지므로 더 좋겠네요.
이와 같은 알고리즘을 Alpha-Beta Search라고 하지요.
Alpha-Beta Search는 경우의 수를 나열 해 놓고 좀 더 낳은 선택을 고르는 방식이죠.
Alpha-Beta Search는 일단 알고리즘이 간단하고, 쉽게 구현할 수 있다는 장점이 있습니다.
그러나 A.I.를 똑똑하게 하려면, 모든 상황을 전부 다 나열해야 하므로, 소스 코드가 길어지고,
모든 상황을 전부 나열한다고 해도 언제나 그 상황에서 예외는 있기 마련이므로,
어느 한계점에 이르면 더 이상 A.I.가 똑똑해지기 힘들다는 단점들이 있죠.
그러나, 소스 코드 한줄을 추가하는 것만으로도 컴퓨터가 확연히 똑똑해지는 것이 눈에 보이고,
간단한 코딩만으로도 꽤 똑똑한 A.I.를 구현해 낼 수가 있으므로, 주로 보드게임에서 많이 사용되죠.
자, 이제 본격적으로 지난 시간까지 만들었던 소스에 A.I. 코드를 추가해 보도록 하죠.
A.I.를 실행 하려면 게임 진행 중에 어느 시점에서 호출 해 주는 것이 가장 좋을까요?
아무래도 A.I.는 컴퓨터의 턴에 실행이 되는 것이므로,
당연히 턴을 변경한 직후에, A.I.를 호출 해 주는 것이 좋겠지요?
twogo1p.GameView 클래스의 턴을 변경해 주는 함수인 changeTurn() 함수에서 A.I.를 호출해 줍시다.
☆ ☆ ☆ ☆ ☆ ☆ |
/** * 턴을 변경한다. */ private void changeTurn() { // 쓸 체크. if(checkSseul() == true) { curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_SSEUL; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); } // 턴 변경. curTurn = (curTurn==User.PLAYER)? User.COMPUTER : User.PLAYER; if(curTurn == User.COMPUTER) { do { runAI(); } while(agent.wasAdditionalCard() == true); } } |
자, 새롭게 추가된 부분(☆)만 살펴 보도록 하겠습니다.
턴을 변경을 하고 난 후에, 변경된 턴이 컴퓨터의 턴이라면, runAI() 라는 함수를 호출하고 있습니다.
그런데 여기서 runAI()는 AI를 실행해 주는 함수라고 하지만,
runAI() 함수가 do ~ while() 안에서 루프를 돌고 있습니다.
while()문의 조건을 보면, agent라는 클래스의 wasAdditionalCard() 라는 함수를 호출하는데요.
agent 클래스는 A.I.를 담당하는 함수들을 모아놓은 클래스이고, 잠시 후에 살펴 볼 것입니다.
그리고 wasAdditionalCard() 함수는 A.I.가 방금 낸 패가 추가 쌍피였는지를 리턴합니다.
즉, 추가 쌍피를 내면, 중앙의 패를 뒤집어 한장을 갖고, 한번 더 내야 하죠?
그렇기 때문에, 추가피를 냈다면, 루프를 돌며 추가피가 아닌 패를 낼때까지 A.I.를 호출하는 것이죠.
그 다음으로 방금전에 열심히 루프를 돌며 호출했던 runAI() 함수를 한번 보도록 하지요.
/**
* A.I. 실행.
*/
public void runAI()
{
System.out.println("A.I. 수행 시작.");
throwCard( agent.run(room) );
room.sortMonth();
repaints();
} |
twogo1p.GameView 클래스의 runAI() 함수는 단순히, agent.run() 함수를 호출해주는 것 뿐입니다.
agent.run() 함수는 컴퓨터가 들고 있는 10장의 카드 중에서 낼 카드의 번호를 리턴해 줍니다.
그러므로 agent.run() 함수가 리턴한 결과 값을 throwCard() 함수에 인자로 넘겨주면,
agent.run() 함수를 호출 후 선택된 카드를 바닥에 내 주겠죠.
이제 앞에서 몇번 언급이 되었던 A.I.를 담당하는 Agent 클래스를 만들어 봅시다.
/**
* A.I. 수행.
*/
public byte run(Room room)
{
byte result;
wasAdditionalCard = false;
System.out.println("A.I. 수행하고 있음...");
this.room = room;
|
run() 함수에서는 Room 클래스를 인자로 받고 있는데,
Room 클래스의 멤버들은 카드를 한번 낼때마다 각각의 데이터가 계속 변하므로,
A.I.를 수행 할때마다 업데이트 된 Room 클래스를 받아 와야 하겠지요.
일단 run() 함수의 전체적인 A.I.의 흐름만을 본다면 별것이 없습니다.
컴퓨터가 들고 있는 패 중에서, 서비스 패가 있다면, 서비스 패를 내고, 중앙의 패를 한장 먹습니다.
그러나, 컴퓨터가 들고 있는 패 중에 추가 서비스 패가 없다면,
광이 있는지를 체크하고, 광이 없다면, 비광이 있는지 체크를 하고,
일반 광과 비광이 없다면, 고도리, 청단, 홍단, 초단, 등을 체크하고,
그 다음은 쌍피, 피 순으로 체크를 해 줍니다.
위의 상황에 아무것도 해당되는 것이 없다면, 들고 있는 카드 중에서 아무거나 바닥에 내는 것이죠.
가장 먼저 컴퓨터의 패 중에서 추가 서비스 패가 있는지를 체크해서, 서비스 패의 인덱스를 리턴해주는,
findAdditionalCard() 함수를 한번 보도록 하겠습니다.
/**
* 추가 쌍피가 있는지 체크.
* @return
*/
private byte findAdditionalCard()
{
byte i;
/* 서비스 패 체크 */
for(i=0; i<room.numOfComputerCard; i++)
{
// 서비스 패라면,
if( room.isAdditionalCard(room.getUserHand(Room.COMPUTER, i)))
{
return convertIndex(i);
}
}
return -1;
} |
findAdditional() 함수는 컴퓨터의 패 중에서 추가 쌍피가 존재하는지를 체크 합니다.
numOfComputercard 는 컴퓨터가 몇개의 카드를 가지고 있는지를 기억하고 있는 변수입니다.
컴퓨터가 가지고 있는 카드의 수만 큼 루프를 돌아서 추가 쌍피라면, 해당 인덱스를 리턴합니다.
그런데 i가 인덱스인데 convertIndex()라는 함수를 거쳐서 리턴을 해 주고 있는데요.
/**
* 게임 설정에 맞게 인덱스를 변경한다.
* @param convertIdx
* @return
*/
private byte convertIndex(byte convertIdx)
{
/*
* 손에 든 카드의 인덱스는, 0~9가 아니라, 1~9, 0이므로,
* 내기 전에 렌덤하게 선택된 인덱스를 변경해 준다.
*/
if(room.isAdditionalCard(room.getUserHand(Room.COMPUTER,convertIdx)))
{
wasAdditionalCard = true;
}
convertIdx = (++convertIdx == 10)? 0 : convertIdx;
return convertIdx;
}
|
카드 배열은 0~9까지로 되어 있지만, 실제 throwCard에서는 1~9, 0과 같이 계산을 하므로,
0~9를 1~9, 0으로 변환 해 준 뒤에 리턴을 해야 하겠죠?
convertIndex() 함수가 바로 그 작업을 해줍니다.
만약 해당 카드가 서비스 패라면, 추가 서비스 패를 냈었다는 것을 wasAdditionalCard에 기억해둡니다.
그럼 다음으로 findCard() 함수를 볼까요?
findCard() 함수는 현재 컴퓨터의 패 중에서 해당 패가 있는지,
혹은, 이 패를 내면 바닥에서 해당 패를 먹을 수 있는지를 체크 합니다.
/**
* 해당 패를 먹을수 있는지 체크.
* @return
*/
private byte findCard(byte cardType)
{
byte i, j, k;
/* 패 체크 */
for(i=0; i<room.numOfComputerCard; i++)
{
// 낼수 있는 패라면,
if(room.compareFloor(room.getUserHand(Room.COMPUTER, i)) != -1)
{
// 낼수 있는 패가 인자로 받은 Card Type 이라면,
if(gCard.getType(room.getUserHand(Room.COMPUTER, i))
== cardType)
{
return convertIndex(i);
}
// 바닥 검사.
for(j=0; j<12; j++)
{
if(room.getMonth(j)[0] == room.CARD_NULL)
continue;
// 같은 종류의 패가 있는 월을 찾았다면,
if( gCard.getType(room.getMonth(j)[0])
== gCard.getType(room.getUserHand(Room.COMPUTER, i)))
{
for(k=0; k<4; k++)
{
if(room.getMonth(j)[k] == room.CARD_NULL)
break;
// 해당 월에 해당 Card Type이 있다면,
if(gCard.getType(room.getMonth(j)[k]) == cardType)
{
return convertIndex(i);
}
}
break;
}
}
}
} // for(i=0; i<room.numOfComputerCard; i++).
return -1;
} |
낼 수 없는 패는 체크 할 필요도 없겠죠. 따라서 낼 수 있는 패만을 검사합니다.
낼 수 있는 패라면, 해당 패가 인자로 받은 cardType과 동일한 카드 타입인지 체크한 후,
같다면 해당 카드의 인덱스를 리턴하고, 카드 타입이 일치하지 않는다면, 바닥을 검사해서,
바닥에 cardType과 동일한 카드타입의 카드가 존재하는지를 체크해서 있다면 인덱스를 리턴하죠.
결국, cardType과 동일한 종류의 카드를 들고 있거나, 해당 카드를 먹을 수 있는지를 체크하는거죠.
그런데 고도리, 청단, 홍단, 초단을 체크할때에는 조건이 조금 많죠?
/* 고도리 체크 */ if(bNoFindGodori == false || (room.getCntGodori(Room.PLAYER) >= 2 && room.getCntGodori(Room.COMPUTER) == 0)) { if(room.getCntGodori(Room.PLAYER) >= 1) { bNoFindGodori == true; } |
가장 먼저 bNoFind... 로 시작되는 boolean 변수를 체크하죠.
bNoFind... 변수들은 고도리, 청단, 홍단, 초단 패를 검색할 필요가 있는지를 체크하는 변수입니다.
예를 들어, 사용자가 이미 고도리 패를 가지고 있다면, 고도리 패는 이제 그냥 일반 열끗일뿐이죠.
따라서, bNoFindGodori 가 false라고 해도, 사용자가 한장이라도 고도리 패를 가지고 있다면,
고도리 패를 애써 먹으려고 노력할 필요는 없으므로 먹지 않을 수 있는 것이죠.
그러나 비록 사용자가 고도리 패를 가지고 있다고 해도, 사용자가 고도리를 할 위기에 처했다면,
고도리 패를 먹음으로써, 사용자의 고도리를 막아야 하겠지요.
만약 플레이어의 고도리가 2장인데, 우리가 한장도 가지고 있지 않다면, 그 패를 어서 먹어야 하겠지요.
청단, 홍단, 초단도 모두 마찬가지의 상황을 체크 하고 있습니다.
// 위의 상황에 해당 하는 것이 없다면, |
run() 함수의 가장 마지막 부분입니다.
위에서 광, 청단, 홍단, 초단, 그리고 쌍피와 피 등등의 체크 과정을 모두 거쳤는데도
위의 상황에 아무것도 해당 되는 것이 없다면, 그냥 무작정 아무 패나 낼 수는 없고,
가능하다면 아무거나 패를 획득 할 수 있는 패를 내 주어야 하겠죠.
역시 마찬가지로 루프를 돌면서 먹을수 있는 패가 있다면, 그것을 내고,
아무것도 먹을 수 있는 것이 없다면, 렌덤하게 아무 카드나 내 줍니다.
사실 이렇게 처리를 해주면, 먹을게 없을때, 광이나 쌍피와 같이 중요한 카드를 내버리는 경우가 있겠죠.
물론 이러한 경우를 대비한 상황도 처리를 해 주어야 하지만, 강좌가 길어지므로, 일단은 생략했습니다.
그러나 이 부분은 뭐 그렇게 까다로운 부분도 아니고 해서 소스 코드에만 첨가를 시켜 놓았으므로,
소스 코드를 참조하시면 되리라고 봅니다.
이걸로 어느 정도 간단한 A.I.는 구현이 되었습니다. A.I.를 좀더 향상 시키고 싶다면,
각종 상황을 좀더 추가를 시켜 주면 되겠죠?
그러나 아직, 흔듬 여부 선택이나, 국진의 사용 여부 선택, 고/스톱 선택 등등의 상황에서
컴퓨터가 상황에 맞게 선택하는 기능이 구현되어 있지 않네요.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임9(사운드 출력과 진동) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2)
DevSource/맞고 2012. 12. 27. 22:18 |이번 시간에는 게임내에서 사용자에게 어떤 메시지를 전달하기 위한 다이얼로그들을 구현해 보겠습니다.
뭐, 메시지를 전달하기 위한 다이얼로그라고 해봤자 별건 아니구요,
쪽, 뻑, 따닥, 쓸 과 같은 메시지를 화면에 출력해 주는 그런 내용입니다. 이전시간과 전반적인 흐름은 같습니다.
먼저 순서대로 쪽 부터 구현을 해보도록 하지요.
☆ ☆ ☆ ☆ ☆ ☆ |
case 2: // 손에서 낸 카드와, 뒤집어 낸 카드 둘뿐이라면, /* 쪽! */ curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_JJOK; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); // 쪽일때에는, 우선 두장 다 먹고, room.addPae(curTurn, room.getMonth(handCardIdx)[0]); room.addPae(curTurn, room.getMonth(handCardIdx)[1]); room.getMonth(handCardIdx)[0] = room.CARD_NULL; room.getMonth(handCardIdx)[1] = room.CARD_NULL; // 상대방의 피를 하나 가져온다. room.robPee(curTurn); repaints(); break; |
전체 함수를 모두 올리는건 지면상 오버헤드가 큰것 같아서 쪽 부분만을 따로 떼어 내었습니다.
예전의 소스와 별 다를 것이 없지만, 앞 부분에 몇가지 부분이 추가 되었습니다.
먼저, State 클래스에 GAME_SHOWMESSAGE 라는 상태 상수가 추가 되었지요.
curViewState가 GAME_SHOWMESSAGE 상태일때에는 어떤 메시지를 보여주고 있는 상태를 뜻합니다.
현재 보여주고 있는 메시지의 종류는 아래 보이는 curMsgState 라는 변수에 저장을 하고 관리합니다.
지금 이 상태에서는 CyDialog 클래스의 MSG_JJOK 이라는 상태 상수가 저장되었으므로,
현재 보여지는 메시지가 "쪽"과 관련된 메시지 라는 것을 알 수 있겠죠.
메시지를 일정 시간 보여주고 난 뒤에는 다시 게임으로 돌아와서, 화면을 다시 그려주면 되겠지요.
< 쪽이 발생했을때의 메시지 > |
다음으로 뻑일때의 메시지 출력을 구현해 보도록 하겠습니다.
☆ ☆ ☆ ☆ ☆ ☆ |
case 3: // 손에서 낸 카드와, 뒤집어 낸 카드와 원래 카드 한장이라면, room.incCntPuck(curTurn); // 뻑 횟수 +1. curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_PPUCK; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); // 3번 뻑이라면, if(room.getCntPuck(curTurn) >= 3) { // 7점에 해당하는 금액을 상대방에게 받고, room.setPoint(curTurn, 7); // 플레이어 점수를 7점으로 세팅. // 게임 종료. curViewState = State.GAME_RESULT; repaints(); // 게임 끝. curViewState = State.GAME_RESULT; repaints(); return; } break; |
뻑 역시 마찬가지입니다.
curViewState를 GAME_SHOWMESSAGE로 변경하고,
curMsgState에 MSG_PPUCK이 저장되었으므로, "뻑" 메시지와 관련된 이미지를 출력해 줍니다.
뻑이 나면, 강렬한 응가 이미지로 뻑이 났다는걸 플레이어가 좀더 쉽게 인식할 수 있게 해주지요^^
< 뻑이 났음을 알려주는 메시지 > |
다음 따닥 부분을 보도록 하죠.
☆ ☆ ☆ ☆ ☆ ☆ |
else { /* 카드가 슬롯에 가득 차 있다면, 따닥! */ if(i == 3) { /* 따닥! */ curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_DDADAK; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); // 모두 먹고, for(j=0; j<4; j++) { room.addPae(curTurn, room.getMonth(handCardIdx)[j]); room.getMonth(handCardIdx)[j] = room.CARD_NULL; } // 상대방의 피를 하나 가져온다. room.robPee(curTurn); } } |
역시 마찬가지죠. curMsgState가 MSG_DDADAK으로 변경된것 뿐이군요.
< 따닥이 발생했을때의 메시지 > |
쓸이 발생했을때에는 처리해 줄 것이 조금 있죠.
쓸이라는건, 플레이어가 패를 낸 후, 먹을 수있는 패를 다 먹었을때, 바닥에 패가 한장도 없다면, 쓸이죠.
따라서, 턴이 변경되기 바로 전에, 쓸을 체크 하는 것이 좋겠네요.
좀더 편하게 작업하기 위해서, 턴을 변경 하는 것을 따로 함수로 만들었습니다.
/** * 턴을 변경한다. */ private void changeTurn() { // 쓸 체크. if(checkSseul() == true) { curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_SSEUL; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); } // 턴 변경. curTurn = (curTurn==User.PLAYER)? User.COMPUTER : User.PLAYER; } |
단순히 턴을 변경하는 것이 아니라, 턴을 변경 하기 전에 checkSseul() 함수를 이용해서 쓸을 체크한후,
쓸이라면, 쓸 메시지를 일정시간 보여준 후, 턴을 변경해 줍니다.
쓸을 체크 하는 함수는 다음과 같습니다.
/** * 쓸의 여부 체크. */ private boolean checkSseul() { for(byte i=0; i<12; i++) { // 한군데의 월이라도 카드가 있다면, 쓸이 아님. if(room.getMonth(i)[0] != room.CARD_NULL) return false; } // 쓸. return true; } |
12개의 월을 루프를 돌면서 검사를 해서, 카드가 없다면, true를 리턴, 있다면 false를 리턴합니다.
카드가 없어야 쓸이니, true가 리턴이 되면 쓸이라는 의미가 되겠죠.
그리고 앞 시간의 결과 메시지를 출력하는 부분에서 그냥 단순히 결과 메시지만을 출력하고 끝났었죠.
그러나, 그렇게 되면, 아무런 키 입력도 먹지를 않기 때문에, 플레이어는 다운이라도 된줄 알겠죠.
결과 창에서, 확인키를 눌렀을 때 보여줄 메뉴 상자를 하나 더 추가해줍니다.
이 메시지 상자는 게임이 끝난 후, 게임을 계속 할 것인지를 묻는 의미가 강하기 때문에,
ContinueGame이라고 이름 지었습니다.
ContinueGame 메뉴 상자를 그려주는 함수는 drawContinueGame() 함수이며, 다음과 같습니다.
/** * 게임중의 메뉴화면을 그려준다. * @param g */ public void drawContinueGame(Graphics g) { if(preViewState == State.GAME_WAIT) drawWaitingGame(g); else drawRunningGame(g); dialog.drawBorder(g, CyDialog.CONTINUEGAME); } |
curViewState가 State.GAME_CONTINUEGAME 이라면, 이 ContinueGame 상자가 화면에 뜹니다.
그러나 이 ContinueGame 메뉴 상자는 게임이 끝났을 때에만 나오는 것이 아니라,?
게임 중간에도 취소 키나 메뉴 키를 누르면, 이 메뉴 상자가 뜨도록 했습니다.
그래서, ContinueGame 메뉴 상자 출력 전에는 현재 상태를 preViewState라는 곳에 저장을 해야 합니다.
또, preViewState가 GAME_WAIT인 상태에서 취소키를 누르면, 배경을 drawRunningGame()으로 그려주는것이아니라.
drawWaitingGame(g)으로 그려주어야 제대로 된 화면 위에 메뉴 상자가 출력이 되겠죠.
< ContinueGame 메뉴 상자 > |
drawContinueGame() 함수를 호출하면, 위와 같은 메뉴 상자가 출력 되겠죠.
그러나 출력은 되었다고 해도 키 입력은 아무런 처리도 안했기 때문에, 그냥 단순히 저 화면만 뜨겠죠?
이제 키 입력을 처리해 보도록 하겠습니다.
else if(curViewState == State.GAME_CONTINUEGAME) { if(numKey == 1) /* 계속 */ { if(preViewState == State.GAME_RESULT) { room.gameOver(); init(); curViewState = State.GAME_WAIT; } else curViewState = preViewState; } else if(numKey == 2) /* 나가기 */ { enterMainMenu(); } else if(numKey == 3) /* 겔러리로 가기 */ { gallery = new Gallery(midlet); gallery.savePreCanvas(this); midlet.setCurrentDisplay(gallery); gallery = null; System.gc(); } repaints(); } |
위 부분은 키입력을 받는 switch()문 안의 lsdefault: 블럭 안에 삽입된 내용입니다.
1을 누르면, 계속 이므로, GAME_RESULT에서 발생했다면, 게임을 다시 시작하고,
다른 곳에서 발생했다면, 사용자가 취소키나 메뉴키를 눌러서 불러낸 것이므로, 원래 상태로 돌아갑니다.
2번을 누르면 enterMainMenu() 함수를 통해서 메인 메뉴로 빠져나갈수 있도록 해주면 되겠죠.
아직 제대로 구현은 되어 있지 않지만, 게임에서 딴 게임머니로 겔러리에서 그림을 구입할수 있습니다.
3번을 누르면 겔러리 화면을 보여줍니다.
Midlet 클래스에서 gallery를 화면에 보여주는것과 같이 겔러리에서 돌아갈 현재 캔버스를 저장하고,
현재 display를 겔러리로 설정해 주면, 겔러리 화면이 화면에 그려지겠죠?
이걸로 어느정도 디스플레이 요소는 모두 추가를 시켜 주었네요.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |
모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1)
DevSource/맞고 2012. 12. 27. 19:02 |이번 시간에는 게임에 각종 디스플레이 요소를 씌워보는 작업을 하도록 하겠습니다.
이번 시간을 거쳐서 이제 진짜 그럴듯한 게임으로 거듭나는 것을 감상하실 수 있습니다.
가장 먼저 게임 내에서 디스플레이 요소를 출력하기 위한 CyDialog 클래스를 구현해보도록 하지요.
package common; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Image; import javax.microedition.lcdui.Graphics; import com.skt.m.Graphics2D; /** * 다이얼로그와 관련된 기능을 모아놓은 클래스. */ public class CyDialog { public final static byte SELECTTWOCARD = 0; // 두장의 카드 선택. public final static byte SELECTGOOKJIN = 1; // 국진 선택. public final static byte SELECTGOSTOP = 2; // 고/스톱 선택. public final static byte SELECTSHAKE = 3; // 흔듬 선택(플레이어). public final static byte SELECTBOMB = 4; // 폭탄 선택(플레이어). public final static byte RESULT = 5; // 결과 확인. public final static byte CONTINUEGAME = 6; // 게임 메뉴. public final static byte MSG_GO = 7; // 고. public final static byte MSG_STOP = 8; // 스톱. public final static byte MSG_PPUCK = 9; // 뻑. public final static byte MSG_JJOK = 10; // 쪽. public final static byte MSG_DDADAK = 11; // 따닥. public final static byte MSG_SSEUL = 12; // 쓸. public final static byte MSG_SHAKE = 13; // 흔듬(컴퓨터 턴). public final static byte MSG_BOMB = 14; // 폭탄(컴퓨터 턴). private Canvas parentCanvas; private short width, height; /* * Border Images. */ private Image imgSelectTwoCard; private Image imgSelectGookjin; private Image imgSelectGoStop; private Image imgSelectShake; private Image imgSelectBomb; private Image imgResult; private Image imgContinueGame; // 게임 중의 메뉴 화면. /* * Message Images. */ private Image imgMsgGo; // 고를 선택했을때의 메시지. private Image imgMsgStop; // 스톱을 선택했을때의 메시지. private Image imgMsgPpuck; // 뻑을 당했을때의 메시지. private Image imgMsgJjok; // 쪽을 했을때의 메시지. private Image imgMsgDdadak; // 따닥 했을때의 메시지. private Image imgMsgSseul; // 쓸을 했을때의 메시지. private Image imgMsgShake; // 흔들었을때의 메시지. private Image imgMsgBomb; // 폭탄일대의 메시지. /** * Constructor. * @param parentCanvas */ public CyDialog(Canvas parentCanvas) { this.parentCanvas = parentCanvas; width = (short) parentCanvas.getWidth(); height = (short) (parentCanvas.getHeight()+16); loadImages(); // 이미지를 읽어옴. } /** * 다이얼로그에 필요한 각종 이미지를 로드한다. */ private void loadImages() { try { /* * Load Border Images. */ // 두장의 카드 선택 border. imgSelectTwoCard = Image.createImage("/image/gasktwocard.lbm"); // 두장의 카드 선택 border. imgSelectGookjin = Image.createImage("/image/gaskgook.lbm"); // 고/스톱 선택 border. imgSelectGoStop = Image.createImage("/image/gaskgostop.lbm"); // 흔들기 선택 border. imgSelectShake = Image.createImage("/image/gaskshake.lbm"); // 폭탄 선택 border. imgSelectBomb = Image.createImage("/image/gaskbomb.lbm"); // 결과 화면. imgResult = Image.createImage("/image/resultbg.lbm"); // 게임 메뉴 화면. |
역시나 별건 없죠? 예전과 동일합니다.
처음에 생성자에서 Canvas를 저장하고, 화면 사이즈를 초기화 한 후, 모든 이미지를 로드합니다.
그리고 drawBorder() 함수는, 단순히 화면 중앙에 Border 형식의 다이얼로그를 출력해줍니다.
다음에 나오는 drawMessage() 역시 마찬가지 함수입니다.
이 drawMessage()는 메시지 형식의 다이얼로그를 출력해준다는 것 만이 다르죠.
여태까지의 게임에서는 g.drawString() 함수를 이용해서, 모든 메시지를 처리했죠.
게임 종료 처리도 구현이 안되어 있었구요. 이번시간에 그걸 해보도록 하겠습니다.
먼저 두장의 카드중 하나를 선택하는 부분을 수정해보도록 할까요?
/** * 카드 둘중 하나를 선택할때... * @param g */ private void drawSelectTwoCards(Graphics g) { byte index; drawRunningGame(g); /* 카드 두장 중 하나를 선택한다. */ dialog.drawBorder(g, CyDialog.SELECTTWOCARD); // 두 종류의 카드를 보여줌. if(room.slot[0] != room.CARD_NULL) index = 0; else if(room.slot[1] != room.CARD_NULL) index = 1; else return; drawCard(40, 66, room.getMonth(room.slot[index])[0]); drawCard(65, 66, room.getMonth(room.slot[index])[1]); } |
지난시간과 거의 다름이 없지만, g.drawString() 이 함수가 삭제되고,
dialog.drawBorder(g, CyDialog.SELECTTWOCARD);
위 부분이 추가 되었죠.
그리고 이전에는 단순히 문자열만이 출력되어서, 어떤 카드중에 하나를 선택하라는 건지 알수가 없었죠.
이제는 맨 아래의 drawCard() 함수 두개가 선택해야 하는 카드를 화면에 그려주어,
어떤 카드 중에 선택을 하라는 것인지 확실히 알 수가 있습니다.
< 두장의 카드를 선택하는 화면 > |
이제, 흔들기/폭탄시의 다이얼로그를 출력 해 보도록 할까요?
/** * 흔듬/폭탄 여부를 선택 중일때... * @param g */ private void drawSelectShake(Graphics g) { drawRunningGame(g); |
흔들기/폭탄 선택 다이얼로그는 단순히, 이미지 한장을 출력해 주면 됩니다.
단지, 흔들기 일때의 이미지와, 폭탄일때의 이미지가 다르므로, 그것만 구분해서 출력해 주면 되겠죠.
< 흔듬 여부를 선택하는 화면 > |
다음에 나오는 국진 선택화면이나, 고/스톱 선택화면은 모두 화면 중앙에 이미지를 출력하는것 뿐입니다.
/** * 국진의 사용을 선택 중일때, * @param g */ private void drawSelectGookjin(Graphics g) { drawRunningGame(g); dialog.drawBorder(g, CyDialog.SELECTGOOKJIN); } /** * 고/스톱을 선택 중일때... * @param g */ private void drawSelectGoStop(Graphics g) { drawRunningGame(g); dialog.drawBorder(g, CyDialog.SELECTGOSTOP); } |
그냥 현재 게임 화면을 먼저 그려준 후 , (drawRunningGame())
그 위에 각 상황에 맞는 이미지를 하나 출력 해 준 것 뿐이죠.
아래는 각각의 상황의 출력 결과입니다.
< 국진의 선택 화면 > |
< 고/스톱 선택 화면 > |
이번에는 결과 화면을 출력해 주도록 하겠습니다.
예전에는 그냥 g.drawString() 함수를 이용해서, "플레이어 승!", "컴퓨터 승!" 혹은 "나가리!" 와 같이,
단순히 문자열만 출력해주는 아~주 초라했던 인터페이스를 가지고 있었죠.
drawResult() 함수를 CyDialog 클래스를 이용해서 좀더 수정해 보았습니다.
☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ |
/** * 결과 화면을 그려준다. * @param g */ private void drawResult(Graphics g) { drawRunningGame(g); // 결과 창을 그려줌. dialog.drawBorder(g, CyDialog.RESULT); // 나가리라면... if(room.getNagari() == true) { if(gameEnd == false) gameEnd = true; g.setColor(0); g.drawString("나가리", width/2, 73, Graphics.HCENTER|Graphics.TOP); } else { /* 벌칙. */ byte goBak; // 고박. byte peeBak; // 피박. byte kwangBak; // 광박. byte mungDda; // 멍따. int obtainedMoney=0; // 획득 머니. switch(curTurn) { case User.PLAYER: // 플레이어의 턴일때, g.setColor(0); g.drawString("플레이어 승", width/2, 73, Graphics.HCENTER|Graphics.TOP); // 상대방이 고를 한번 이상 했었다면, - 고박일경우 돈 두배 획득. goBak = (room.getCntGo(User.COMPUTER) >= 1)? (byte)2 : (byte)1; |
☆ 파란색 별은 새롭게 추가된 부분, ☆ 주황색 별은 수정된 부분을 의미합니다.
소스가 조금 길긴 하지만, 자세히 보면, 이미지랑 문자열 출력해주는것 밖에 추가 된 것이 없죠^^;
가장 윗 부분을 보시면, 이제까지와 마찬가지로,
CyDialog 클래스의 drawBorder() 함수를 이용해서, 결과 화면을 그려주었습니다.
그리고 주황색 별(☆)로 표시된 부분은 지난시간과 똑같지만, 문자열의 좌표가 약간 변경되었습니다.
그 다음에 추가된 부분들은 모두, 결과창 이미지의 좌표에 맞게 문자열을 출력해주는 부분들일 뿐입니다.
문자열들의 좌표는 포토샾에서 계산을 해서 집적 숫자 값으로 넘겨 주었습니다.
< 게임이 끝난후의 결과 화면 > |
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |
모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현) (0) | 2012.12.27 |
모바일 맞고 게임6-1(맞고 게임룰 설명) (0) | 2012.12.27 |
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 )
DevSource/맞고 2012. 12. 27. 19:01 |3뻑/쪽/따닥. |
우리가 그냥 넘어가긴 했지만, 지난시간에 구현했던 내용에 이미 쪽과 따닥이 구현되어 있습니다.
그 곳에 쪽과 따닥이 발생 시, 상대방의 피를 한장 가져오는 것만 추가를 해주면 되겠죠.
한판에 3번 뻑이 나면, 게임이 끝나며, 7점에 해당되는 금액을 상대방에게획득하고, 승리합니다.
또, 쪽이란 먹을 수 있는 패가 없어서 아무 패나 냈는데 뒤집은 패가 같은 무늬인 경우이죠.
쪽이 발생했을 경우에는, 패를 먹음과 동시에 상대방의 피 한 장을 가져옵니다.
따닥은 바닥에 같은 무늬 패 두 장이 있고 그걸 먹기 위해 패를 냈는데, 뒤집은 패도 같은 무늬일 경우,
해당 카드를 네 장 모두 먹음과 동시에 상대방의 피 한 장을 가져옵니다.
지난 시간에 뻑을 구현 했는데, 그 안에 쪽과 따닥에 관한 내용도 이미 포함이 되어 있습니다.
// 손에서 냈던 카드와 뒤집어서 낸 카드가 같은 슬롯에 있다면, if(handCardIdx == reverseCardIdx) { int i, j; // 뻑 체크. for(i=0; i<4; i++) { if(room.getMonth(handCardIdx)[i] == room.CARD_NULL) { switch(i) { case 2: // 손에서 낸 카드와, 뒤집어 낸 카드 둘뿐이라면, ☆ /* 쪽! */ // 쪽일때에는, 우선 두장 다 먹고, room.addPae(curTurn, room.getMonth(handCardIdx)[0]); room.addPae(curTurn, room.getMonth(handCardIdx)[1]); room.getMonth(handCardIdx)[0] = room.CARD_NULL; room.getMonth(handCardIdx)[1] = room.CARD_NULL; ☆ // 상대방의 피를 하나 가져온다. ☆ room.robPee(curTurn); repaints(); break; case 3: // 손에서 낸 카드와, 뒤집어 낸 카드와 원래 카드 한장이라면, // System.out.println(" # Msg : 뻑!!"); room.incCntPuck(curTurn); // 뻑 횟수 +1. ☆ // 3번 뻑이라면, ☆ if(room.getCntPuck(curTurn) >= 3) ☆ { ☆ // 7점에 해당하는 금액을 상대방에게 받고, ☆ room.setPoint(curTurn, 7); // 플레이어 점수를 7점으로 세팅. ☆ // 게임 종료. ☆ curViewState = State.GAME_RESULT; ☆ repaints(); ☆ // 게임 끝. ☆ curViewState = State.GAME_RESULT; ☆ repaints(); ☆ return; ☆ } break; } } // end if(room.getMonth(handCardIdx)[i] == room.CARD_NULL). else { ☆ /* 카드가 슬롯에 가득 차 있다면, 따닥! */ if(i == 3) { // 모두 먹고, for(j=0; j<4; j++) { room.addPae(curTurn, room.getMonth(handCardIdx)[j]); room.getMonth(handCardIdx)[j] = room.CARD_NULL; } ☆ // 상대방의 피를 하나 가져온다. ☆ room.robPee(curTurn); } } } } |
앞에 ☆와 같은 별이 달려있는 Line이 새로 추가된 줄입니다.
뻑이 3번 발생하면, 뻑이 3번 발생한 플레이어의 점수를 무조건 7점으로 만들고 게임을 종료 합니다.
왜냐하면, 게임이 끝났을때, 플레이어의 점수에 따라 상대방에게서 금액을 획득하므로,
점수를 7점으로 세팅해 주면, 7점에 해당하는 금액을 받아오게 되겠지요.
그리고 쪽과 따닥은 지난 시간까지의 내용에 상대방의 피를 하나 가져오는 부분만을 추가한 것이죠.
상대방에게서 피를 한장 뺏어오는 robPee() 함수는 twogo1p.Room 클래스에 정의 되어 있습니다.
/** * 다른 사용자로부터, 피를 한장 가져온다. * @param userType * @return */ public byte robPee(byte userType) { byte userLastIdx = -1; byte enemyLastIdx = -1; byte enemyType; byte userPeeIdx = CARD_NULL, enemyPeeIdx = CARD_NULL; byte userSsangPeeIdx = CARD_NULL, enemySsangPeeIdx = CARD_NULL; // 상대방의 타입 설정. enemyType = (userType==COMPUTER)? PLAYER : COMPUTER; // 상대방의 피에 1점자리 피가 있는지 찾는다. for(i=0; i<MAX_PEE; i++) { // 플레이어의 카드가 없다면, if(cardsPee[userType][i] == CARD_NULL) userLastIdx = (byte)(i-1); |
함수가 조금 길긴 하지만, 별건 아닙니다.
먼저, 0부터 MAX_PEE(36) 까지 루프를 돌면서, 각각의 플레이어의 피에서 쌍피와 1점자리 일반피를 찾아냅니다.
만약에 상대방의 피에서 1점 자리 일반 피를 찾았다면, 그 피만 받고 끝나면 되겠죠.
그러나, 상대방의 피에 1점자리 피가 없다면, 쌍피가 있는지 보고,
플레이어가 1점자리 피가 있다면, 쌍피를 받고 1점자리 피를 상대에게 넘겨줍니다.
상대방이 쓰리피가 있다면, 플레이어에게 쌍피가 있는지 보고,
플레이어가 쌍피가 있다면, 쓰리피를 받고 쌍피를 넘겨 줍니다.
이렇게 하면 피 한장을 받을 수 있겠죠?
고/스톱 관리. |
맞고 역시 1:1 고스톱이나 마찬 가지이므로, 고와 스톱이 존재하죠.
투고 까지는 1고는 1점, 2고는 2점. 과 같이 더해지지만,
3고부터는 점수의 2배, 4고는 점수의 4배, 5고는 점수의 8배, .... 이런식으로 늘어납니다.
고/스톱 을 관리 해 주는 checkGoStop() 함수는 다음과 같이 작성해 줍니다.
private void checkGoStop() |
위에서 별로 헷갈릴만한 내용은 없을 거라고 봅니다.
고를 한번도 하지 않은 상태에서는 LIMITPOINT_GOSTOP(GameView에 7로 정의되어있음)을 초과하면,
고/스톱을 물어보지만, 일단 한번 고를 한 상태에서는 추가로 점수가 발생할 때마다 고/스톱을 물어보죠.
이 checkGoStop() 함수는추가로 점수가 발생 하는 곳에다가 모두 삽입을 해주어야 하겠지요.
카드를 냈을때에만, 점수가 발생하는 것이 아니니까요.
흔들었을 때에도, 점수가 2배가 되므로, 고/스톱을 물어 보아야 하고,
국진을 쌍피로 선택 했을때에도, 추가 점수가 발생하므로, 고/스톱을 물어보아야 하고,
일반 적인 경우에도, 점수가 발생했다면, 고/스톱을 체크해야하겠죠.
그런데 이 checkGoStop() 함수에 보면, getRealPoint() 라는 함수를 이용해서 점수를 얻어 오고 있는데,
지난 시간에 우리가 작성했던 점수를 얻어오는 함수는 getPoint() 함수였었죠?
getRealPoint() 함수는, 각종 룰의 공식에 의해 실제적으로 얻어지는 실제 점수를 리턴해 줍니다.
getRealPoint() 함수는 gostop.GameRoom 클래스에 정의되어 있습니다.
/** * 실제 점수를 리턴한다. * @param userType * @return */ public int getRealPoint(byte userType) { int realPoint=getPoint(userType); // 원고나 투고 라면, if(cntGo[userType] == 1 || cntGo[userType] == 2) { // 원고면 1점, 투고면 2점을 더해줌. realPoint += cntGo[userType]; } else if(cntGo[userType] >= 3) // 쓰리고 이상이라면, { // 3고 부터는, 2의 (고횟수-2)승으로 계산. realPoint *= Etc.pow(2, cntGo[userType]-2); } // 흔들기도, 2의 흔든 횟수승으로 계산. realPoint *= Etc.pow(2, cntShake[userType]); return realPoint; } |
고나 흔들기가 발생하면, 점수에 두배를 곱해야 하는데,
현재 점수의 두배가 아니라 최종 점수의 두배이므로, 현재 점수를 함부로 건드려선 안되겠죠.
그래서, 고와 흔들기에 의한 변경된 점수는 getRealPoint() 함수를 통해서 얻어 옵니다.
여기서 3고 부터는 2의 (고횟수-2)승 이라는 것까지는 이해가 가시겠죠?
그런데 여기서 제곱승을 구하는 함수가 Etc.pow() 함수가 쓰였습니다.
제곱승을 구해오는 pow() 함수는 표기 된 데로 common.Etc 클래스에 정의되어 있습니다.
/** * a의 n제곱승을 리턴. * @param a * @param n * @return a의 n승. */ public static int pow(int a, int n) { byte i; short nPowOfa=1; for(i=0; i<n; i++) { nPowOfa *= a; } return nPowOfa; } |
그냥 단순히 인자로 받은 a의 n승을 리턴해 주는 함수입니다.
for 루프를 n번 돌아서 1에 a를 곱해 주면 a의 n승을 구해 올 수 있겠죠.
나가리. |
나가리란, 두명의 사용자가 모든 카드를 다 냈는데도, 게임이 끝나지 않았을때,
즉, 그 판에서 아무도 7점을 내지 못하거나 상대방이 고를 했는데 추가 점수를 내지 못했을 경우를 말합니다.
나가리 시에는 게임을 끝내고 결과를 보여 주어야 하겠죠.
/** * 나가리를 체크. * @return 나가리 여부. */ private boolean checkNagari() { // 플레이어의 카드와 컴퓨터의 카드가 없다면, if(room.numOfPlayerCard == 0 && room.numOfComputerCard == 0 ) { repaints(); // 나가리. room.setNagari(true); // 게임 끝. curViewState = State.GAME_RESULT; repaints(); return false; } return true; } |
플레이어의 카드와 컴퓨터의 카드가 한장도 없는데, 게임이 끝나질 않았다면, 나가리로 처리하고,
결과 화면을 보여줍니다.
결과 출력. |
이제 그럼 결과 화면을 출력해 보도록 하겠습니다.
/** * 결과 화면을 그려준다. * @param g */ private void drawResult(Graphics g) { drawRunningGame(g); // 나가리라면... if(room.getNagari() == true) { if(gameEnd == false) gameEnd = true; g.drawString("나가리!", 0, 0, Graphics.LEFT|Graphics.TOP); } else { /* 벌칙. */ byte goBak; // 고박. byte peeBak; // 피박. byte kwangBak; // 광박. byte mungDda; // 멍따. switch(curTurn) { case User.PLAYER: // 플레이어의 턴일때, g.drawString("플레이어 승!", 0, 0, Graphics.LEFT|Graphics.TOP); // 상대방이 고를 한번 이상 했었다면, 고박. (고박일경우 돈 두배 획득) goBak = (room.getCntGo(User.COMPUTER) >= 1)? (byte)2 : (byte)1; |
만약에 나가리로 게임이 끝났다면, 화면에 그냥 나가리라고 출력해 줍니다.
만약에 나가리가 아니라면, 현재 턴의 플레이어가 스톱을 했을테니 현재 턴의 플레이어가 이겼겠죠.
이곳에서 고박, 피박, 광박, 멍따 등의 벌칙 룰을 적용해 줍니다.
// 상대방이 고를 한번 이상 했었다면, 고박. (고박일경우 돈 두배 획득) goBak = (room.getCntGo(User.COMPUTER) >= 1)? (byte)2 : (byte)1; // 상대방의 피가 7장 이하라면, 피박. (피박일경우 돈 두배 획득) peeBak = (room.getNumOfPee(User.COMPUTER) < 7)? (byte)2 : (byte)1; // 상대방의 광이 없다면, 광박. (광박일경우, 돈 두배 획득) kwangBak = (room.getNumOfKwang(User.COMPUTER) == 0)? (byte)2 : (byte)1; // 상대방의 열이 7장 이상이라면, 멍따. (멍따일 경우, 돈 두배 획득) mungDda = (room.getNumOfYeul(User.COMPUTER) >= 7)? (byte)2 : (byte)1; |
상대방이 고를 한번이라도 했다면, 고박으로 돈을 두배로 획득하게 해주죠.
이때 goBak에 대입해주는 정수를 곱하므로, 고박이라면 획득 금액에 2를 곱해주고, 아니면 1을 곱하겠죠.
나머지도 모두 마찬가겠죠?
따로 gameEnd라는 boolean 변수를 두어서 gameEnd가 false일때에만, 플레이어의 돈이 증감하게 합니다.
그래야 drawResult() 함수가 중복 호출 되었을때, 돈이 이중으로 빠져나가지 않게 할 수 있겠죠.
이번 시간까지의 내용으로 전반적인 맞고 게임의 틀은 모두 갖춘것 같네요.
이번 시간에 작성한 게임의 스크린 샷은 지난 시간과 별다를게 없으므로 생략하도록 하겠습니다.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현) (0) | 2012.12.27 |
모바일 맞고 게임6-1(맞고 게임룰 설명) (0) | 2012.12.27 |
모바일 맞고 게임5-4(획득 카드 보기 화면 구현) (0) | 2012.12.27 |
모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현)
DevSource/맞고 2012. 12. 27. 19:00 |구현은 앞서 살펴본 맞고의 규칙 순서대로 구현을 해 나가도록 하겠습니다.
오광. |
먼저 광 처리를 다시 한번 살펴보면,
광은 기본적으로 위의 5개가 있습니다.
그 중에, 광이 3개 있다면, 3점 이지만, 비광이 있으면 2점이라고 했죠.
그 외에는 비광의 포함 유무에 관계 없이, 4광이면 4점, 5광이면 15점을 획득합니다.
자, 그럼 광의 점수 계산 코드를 어디에 삽입하면 좋을까요?
당연히, 광을 획득하는 그 순간, 광에 대한 점수 계산을 해 주어야 하겠지요?
광의 획득은 addKwang() 이라는 함수를 따로 지정 해놓고 광을 추가 해주었었던 것을 기억하실 겁니다.
그러니, 당연히, addKwang() 함수에 광의 점수 계산 코드를 삽입해야 하겠지요.
/** * 광을 추가함. * @param userType * @param cardIdx */ public void addKwang(byte userType, int cardIdx) { int numOfKwang = 0; // 비광을 먹었다면, 비광의 소유 여부 기록. if(gCard.getType((byte)cardIdx) == CARDTYPE_BIKWANG) bHaveBiKwang[userType] = true;
switch(numOfKwang) { case 3: // 광이 3개라면, /* 비광이 있다면, */ if(bHaveBiKwang[userType] == true) { addPoint(userType, 2); bCalculatedBiKwang = true; // 비광 때문에 2점을 추가했음. } else // 비광이 없다면, addPoint(userType, 3); // 3점 추가. break; case 4: // 광이 4개라면, if(bCalculatedBiKwang) // 비광때문에 2점을 더해주었었다면, { /* * 비광의 유/무에 상관 없이, * 카드가 4장일 때에는, 4점 이므로, * 비광 때문에 2점을 더해주었었다면, * 1점을 더해준다. */ addPoint(userType, 1); bCalculatedBiKwang = false; } // 광이 하나 추가되었으므로, 1점 추가. addPoint(userType, 1); break; case 5: // 오광 이라면, /* * 오광의 경우, 15점이 추가 된다. * 광이 4개일때의 점수가 4점이었으므로, 11점을 추가 해준다. */ addPoint(userType, 11); } // switch(numOfKwang). } // end public void addKwang(byte userType, int cardIdx). |
위 소스에서 회색 음영 처리 된 부분은 예전에 이미 설명했던 내용이므로, 부가 설명은 않겠습니다.
그럼 추가 된 부분을 한번 보도록 하지요.
먼저, 광의 점수를 정확히 계산하기 위해서는,
플레이어의 슬롯에 비광이 포함되어 있는지 확인하는 것이 반드시 필요하겠지요.
그래서 함수의 두번째 줄에,
// 비광을 먹었다면, 비광의 소유 여부 기록. if(gCard.getType((byte)cardIdx) == CARDTYPE_BIKWANG) bHaveBiKwang[userType] = true; |
위와 같이 해주어서, 현재 먹은 카드(cardIdx)가 비광인지 비교 해서 맞다면,
전역으로 선언된 bHavaeKwang 이라는 boolean 변수에 true를 대입합니다.
그 다음 부분은, 광이 3장째 일때에, 광 3장 중에 비광이 있다면, 2점을 추가해주고,
bCalculatedBiKwang 을 true로 만들어 줍니다.
bCalculatedBiKwang 은 광이 4장일때에, 비광이 있어서 2점이 추가 되었었는지 체크하기 위한 변수이며,
광이 4장일때에, 비광 때문에, 2점을 추가해 주었다면, 먼저 1점을 더해주어서 3점을 만든후,
다시 1점을 추가 해 주어서, 광이 4장일때에, 4점이 추가되도록 해줍니다.
즉, 광이 3장일때 비광이 없었다면,
3점(광 3장) + 1점(추가 한장) = 4점. 이렇게 계산이 되지만,
광이 3장일때 비광이 있었다면,
2점 + 1점 + 1점(추가 한장) = 4점. 이렇게 계산이 되는 것이지요.
또, 광이 5장인, 즉, 오광일 때에는 15점이 추가 되어야 하지만, 이미 앞에서 4점이 추가되었었으므로,
단순히 그 뒤에 11점을 추가 해주면 되겠지요. 그래서 11점을 더 해주는 것입니다.
열끗. |
이제, 열끗 처리를 한번 또 구현해 볼까요?
열끗 처리 역시 광의 처리와 거의 흡사합니다.
열끗의 경우, 아래 보이는것과 같이 모두 9장이며, 5장부터 점수로 계산이 됩니다.
5장일때 1점, 6장일때 2점, 7장일때 3점, .... 이렇게 한장이 추가될때마다 1점씩 추가됩니다.
또 7장 이상이라면, 멍따로 처리 되며, 승리시 두배의 금액을 획득하게 됩니다.
또, 열에서는 한가지 더 생각해 주어야 할 것이 있습니다. 바로 고도리이죠!
고도리는 다음의 3장의 카드를 모두 소유 하고 있다면, 플레이어의 점수에 5점이 합산됩니다.
역시 이 열끗의 처리 역시, 열끗을 먹는 동시에 처리하는 것이 좋겠죠?
광과 마찬가지로 addYeul() 함수에 해당 코드를 구현합니다.
/** * 열을 추가함. * @param userType * @param cardIdx */ public void addYeul(byte userType, int cardIdx) { int numOfYeul = 0; /* 지금 먹은 열이 고도리 패중 하나라면, */ if(gCard.getType((byte)cardIdx) == CARDTYPE_GODORI) { incCntGodori(userType); } // 고도리 패가 3장 있다면, -> 고도리. if(bGodori[userType] == false && getCntGodori(userType) == 3) { addPoint(userType, 5); // 5점 추가. bGodori[userType] = true; }
// 열은 5장 부터 1점씩 추가. if(numOfYeul >= 5) addPoint(userType, 1); } |
마찬가지로 회색 음영 처리 된 부분은 이전시간에 이미 설명했던 부분입니다.
가장 먼저 고도리패를 체크 해줍니다.
고도리 패는 GostopCard.java에 CARDTYPE_GODORI라고 정의해놓았었죠?
따라서, 현재 먹은 카드(cardIdx)의 종류를 체크후, 고도리 패면, 먹은 고도리 패의 갯수를 증가시킵니다.
그리고 그 다음 문장 처럼, 고도리의 패장이 3장 있다면, 고도리를 모두 획득한 것이므로,
bGodori를 true로 만들어 주어, userType의 사용자가 고도리 했음을 표시하고, 5점을 추가합니다.
그리고 마지막에서는 열이 5장이 되는 순간부터, 점수를 1점씩 추가시켜 주면 열의 처리는 모두 끝납니다.
그런데 위의 코드에 멍따에 대한 부분은 없는데요. 멍따의 처리는 최종 점수에 두배를 곱해 주는 것이니,
멍따를 포함한 피박, 고박, 광박 등등의 벌칙 룰은 나중에 게임 결과 처리시 처리 해 주는 것이 좋겠지요.
띠. |
띠 역시 광이나 열끗과 마찬가지 방식으로, addYeul() 함수에서 처리하는 것이 좋겠지요.
띠에는 다음과 같이 총 10개의 띠가 있습니다.
앞에서부터 3장을 소유하고 있다면, 홍단. 그 다음 3장은 초단. 그리고 그 다음은 청단이라고 했죠.
비에 초단 비슷한 띠가 달려 있는 패는 초단이 아닙니다.
/** * 띠를 추가함. * @param userType * @param cardIdx */ public void addDdee(byte userType, int cardIdx) { /* 지금 먹은 띠가... */ switch(gCard.getType((byte)cardIdx)) { case CARDTYPE_CHUNGDAN: // 청단 패 중 하나라면, incCntChungDan(userType); break; case CARDTYPE_HONGDAN: // 홍단 패 중 하나라면, incCntHongDan(userType); break; case CARDTYPE_CHODAN: // 초단 패 중 하나라면, incCntChoDan(userType); break; } // 만약 청단 패가 3개 라면, -> 청단. if(bChungDan[userType] == false && getCntChungDan(userType) == 3) { addPoint(userType, 3); // 3점 추가. bChungDan[userType] = true; } // 만약 홍단 패가 3개 라면, -> 홍단. if(bHongDan[userType] == false && getCntHongDan(userType) == 3) { addPoint(userType, 3); // 3점 추가. bHongDan[userType] = true; } // 만약 초단 패가 3개 라면, -> 초단. if(bChoDan[userType] == false && getCntChoDan(userType) == 3) { addPoint(userType, 3); // 3점 추가. bChoDan[userType] = true; } int numOfDdee=0;
// 띠는 5장 부터 1점씩 추가. if(numOfDdee >= 5) addPoint(userType, 1); } |
앞의 열끗에서 고도리를 처리해 주었던과 동일 합니다.
단순히, 종류가 고도리 처럼 하나가 아니라, 청단, 홍단, 초단 3개나 되므로,
3번의 체크가 수행 되어야 하므로 함수가 조금 길어졌을 뿐이죠.
현재 먹은 띠(cardIdx)가 청단이라면 청단, 홍단이라면 홍단, 초단이라면 초단 카운트를 +1 증가시키고,
청단이 3개 라면 청단, 홍단이 3개 라면 홍단, 초단이 3개 라면 초단으로 처리 해 주면되겠죠.
어느 것이든 단을 했다면, 각 단은 3점이므로, 3점의 점수를 추가 시켜주어야 하겠죠?
그리고 마지막 부분에서는 열과 마찬가지로 5장이 되는 순간부터 1점씩 점수를 추가시켜줍니다.
피. |
피는 지금까지의 것들과 달리 특정 피가 여러개 모였을때 발상하는 추가 점수 따위는 없습니다.
그러므로, 단순히, 피가 10장 이상이 되는 그 순간부터, 피 한장당 1점씩 추가를 시켜주면 되겠네요.
그러나! 가장 한가지 중요한 것이 하나있죠.
피가 11장이 있다고 1점을 추가시켜주는 것은 아니겠지요?
피가 11장이라해도, 그 중 쌍피나 쓰리피가 있다면, 쌍피는 두장, 쓰리피는 세장으로 계산을 해야합니다.
/** * 피를 추가함. * @param userType * @param cardIdx */ public void addPee(byte userType, int cardIdx) {
// 피가 10장 이상일때부터 피 1개당 1점씩 추가. if(getNumOfPee(userType) >= 10 ) { if(cardIdx == gCard.CARD_GOOKJIN) // 국진이 피 슬롯에 있다면, addPoint(userType, 2); // 2점 추가. else switch(gCard.getType((byte)cardIdx)) { case CARDTYPE_PEE: // 일반 피라면, addPoint(userType, 1); // 1점 추가. break; case CARDTYPE_SSANG_PEE: // 쌍피라면, addPoint(userType, 2); // 2점 추가. break; case CARDTYPE_THREE_PEE: // 쓰리피라면, addPoint(userType, 3); // 3점 추가. break; } } } |
그냥 단순히 위와 같이 실제 피의 개수가 10개 이상이라면,
현재 먹은 피가 쌍피라면 피를 두장으로, 쓰리피라면 세장으로 처리해서 점수를 더해줍니다.
이걸로 맞고의 기본 점수 계산은 모두 끝냈습니다.
눈치 빠르신 분은 벌써 이상하다고 생각하시고 계실 수도 있겠네요. 아직 한가지 구현 안한게 있습니다.
국진 패를 처리를 안했죠. 국진은, 열끗으로 처리할 수도, 쌍피로 처리할 수도 있지요.
국진 패를 먹는 순간, 국진 패를 열끗으로할지, 쌍피로 처리할 지 사용자에게 입력을 받는게 필요하겠죠.
물론 addYeul() 함수에서 처리를 하면 좋겠지만, addYeul에서 처리를 하려고 하면,
국진패를 획득했는지를 저장한 변수를 이 함수 저 함수 계속 전달해야 하는 수고가 발생합니다.
보통 열패를 플레이어가 획득하게 되면 addPae() 함수를 거쳐, 해당패의 종류에 따라 패를 추가하죠.
그러므로, addPae() 함수에서 해당패가 국진인지 체크를 해주는 것이 낳겠네요.
/** * 패를 추가 한다. * @param userType * @param cardNum */ public void addPae(byte userType, byte cardNum) { /* 지금 먹으려는 열이 국진이라면, */ if(cardNum == CARD_GOOKJIN) { if(bSelectGookjin == false) { // 국진을 열로 쓸지, 피로 쓸지 물어 봄. bSelectGookjin = true; return; } }
|
현재 추가하려는(먹으려는) 패가 국진이라면, 먹지 않고, bSelectGookjin만 true로 만들고 리턴합니다.
그런데 이 addPae를 호출 하는 부분이 GameView.throwCard() 함수인데,
throwCard() 함수에서 addPae()를 호출하고 난 뒤에, bSelectGookjin이 true로 변경되었다면,
국진을 피로 쓸지 열로 쓸지 선택하라는 메시지를 출력해 주어야하겠지요?
throwCard() 함수에서 모든 이벤트를 처리하고 나서, 마지막으로 턴을 변경하기 바로 직전에,
bSelectGookjin이 true인지 체크해서 curViewState를 변경해 줍시다.
// 국진 선택 중이라면, if(room.bSelectGookjin == true) { curViewState = State.GAME_SELECTGOOKJIN; repaints(); return; } |
위와 같이 해주면, 국진의 선택 중이라면, 턴을 변경하지 않고, curViewState만 변경하고 리턴하겠지요.
State 클래스에 GAME_SELECTGOOKJIN이라는 새로운 상수가 추가 되었으니,
GAME_SELECTGOOKJIN에 해당하는 keyPressed()와, paint() 이벤트를 처리해 주어야하겠네요.
/** * 키 입력. */ public void keyPressed(int keyCode) { |
다른 키 입력 처리는 지난 시간까지 모두 다뤄본 내용이므로 생략 했습니다.
별 다른 내용은 없습니다.
curViewState 가 State.GAME_SELECTGOOKJIN일때, 즉, 국진의 사용을 선택중일 때,
1번을 누르면, addYeul()로, 국진을 열에 추가하고,
2번을 누르면, addPee()로, 국진을 피로 추가하는 간단한 내용이죠.
이걸로 맞고 게임의 기본 규칙들은 대부분 완성 했네요.
여기까지만 구현을 해도, 제법 맞고 게임 같은 분위기가 충분히 흘러 나오지만,
뻑이나, 쪽, 따닥, 고/스톱 등의 추가 규칙이 아직 남았죠.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |
모바일 맞고 게임6-1(맞고 게임룰 설명) (0) | 2012.12.27 |
모바일 맞고 게임5-4(획득 카드 보기 화면 구현) (0) | 2012.12.27 |
모바일 맞고 게임5-3(게임 내 카드 처리 구현) (0) | 2012.12.27 |
모바일 맞고 게임6-1(맞고 게임룰 설명)
DevSource/맞고 2012. 12. 27. 18:59 |이번시간에는 드디어, 맞고의 실제 게임 룰을 적용하고,
각 룰에 따라 점수 계산을 해서 승패를 가려주는 부분을 구현해 보도록 하겠습니다.
그럼, 먼저 소스를 보기 전에 간단히 맞고의 룰을 한번 살펴보도록 할까요?
광이 3장이면 이때부터 점수로 계산되며 3점 입니다. 그러나 그 3장중 비광이 포함되어 있으면 2점입니다. 광이 4장이면 4점이며 5광을 하면 15점이 됩니다. 열끗 열끗은 5장을 먹을 때부터 점수로 계산되며 1점입니다. 이후 1장을 추가로 먹어올 때마다 1점씩 가산되며 7장 이상을 먹으면 멍텅구리 따블(멍따)로 계산되어 이겼을 경우에 상대방으로부터 2배의 점수를 빼앗아 옵니다. 띠 띠도 5장을 먹어올 때부터 점수로 계산되며 1점입니다. 이후 1장을 추가로 먹어올 때마다 1점씩 가산 됩니다. 피 피는 10장을 먹어올 때부터 점수로 계산되며 1점 입니다. 이후 피 1장을 추가로 먹어올 때마다 1점씩 가산 됩니다. 국진
고도리(5점) 단.
|
이걸로, 맞고의 기본적인 규칙들은 다 살펴 본 것 같네요.
이제 남은건 실제로 위의 규칙을 하나한 차례로 구현만 해나가면 되겠죠?
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임6-2(기본 게임 룰 및 점수 계산 구현) (0) | 2012.12.27 |
모바일 맞고 게임5-4(획득 카드 보기 화면 구현) (0) | 2012.12.27 |
모바일 맞고 게임5-3(게임 내 카드 처리 구현) (0) | 2012.12.27 |
모바일 맞고 게임5-2(카드 나눠주기 구현) (0) | 2012.12.27 |