드디어 이 맞고 게임 강좌의 마지막 시간입니다.
이번 강좌에서는 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를 생성하고 기본값 기록.
*/ public void saveGameInfo()
{
startWrite(rsName);
try {
// 돈.
writeLong(money[0]);
writeLong(money[1]);
// 사운드/진동.
writeBoolean(sound);
writeBoolean(vibration);
} catch(Exception e) {}
endWrite();
} |
맨 윗줄의 startWrite()는, 인자로 String형 변수인 rsName을 받아서 rsName이라는 RS를 생성합니다.
이 startWrite() 함수는 잠시 후에 설명할 DataManager라는 클래스에 정의되어 있습니다.
RS에 데이터를 기록하는 것은 의외로 쉽습니다.
단순히, DataOutputStream 객체의 writeLong()/writeInt()/writeShor() .... 과 같이 기록하고자 하는 형과 일치하는
메소드를 호출해서 데이터를 기록해 주면 됩니다.
그러나, 여기서는 따로 writeXXX()/readXXX()를 정의해서 DataOutputStream 객체의 wrapper 메소드를 정의했습니다.
따라서 우리가 사용할 형에 따라서 writeLong()/writeInt()와 같이 기록해 주면 됩니다.
그 다음에 나오는 readGameInfo() 메소드 역시 마찬가지 입니다.
/**
* Record store에서 데이터를 읽어옴.
*/ public void readGameInfo()
{
try {
re = recordStore.enumerateRecords(null, this, true);
while(re.hasNextElement()) {
int id = re.nextRecordId();
bais = new ByteArrayInputStream(recordStore.getRecord(id));
dis = new DataInputStream(bais);
try {
// 돈.
money[0] = readLong();
money[1] = readLong();
// 사운드/진동.
sound = readBoolean();
vibration = readBoolean();
} catch(Exception e) {}
}
}
catch (RecordStoreException e){}
endRead();
} |
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.*;
public class DataManager implements RecordComparator
{
protected DataInputStream dis = null;
protected DataOutputStream dos = null;
protected RecordEnumeration re = null;
protected RecordStore recordStore = null;
protected ByteArrayInputStream bais = null;
protected ByteArrayOutputStream baos = null;
/**
* 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);
}
/**
* 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();
}
/**
* RS에서 필요한 데이터를 읽어왔다면, RS를 닫는다.
*/
protected void endRead()
{
closeRS();
re = null;
dis = null;
bais = null;
recordStore = null;
System.gc();
}
protected void writeBoolean(boolean value) throws Exception
{
dos.writeBoolean(value);
}
protected void writeLong(long value) throws Exception
{
dos.writeLong(value);
}
protected void writeInt(int value) throws Exception
{
dos.writeInt(value);
}
protected boolean readBoolean() throws Exception
{
return dis.readBoolean();
}
protected long readLong() throws Exception
{
return dis.readLong();
}
protected int readInt() throws Exception
{
return dis.readInt();
}
/**
* 현재 열려있는 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) {}
}
public int compare(byte[] arg1, byte[] arg2)
{
return 1;
}
}
|
먼저, 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.도 최대한 쉽게 설명하기 위해 최대한 간단히 코딩한 면이 없지 않기 때문에, 컴퓨터가 그렇게 똑독하지 않습니다.
나머지 부분들은 이제 여러분들이 지금 까지 배운 내용을 바탕으로 여러가지 요소들을 추가해 보시고,
이미지가 마음에 들지 않으시는 분들은 직접 웹에서 구한 이미지로 변경해 자신만의 맞고를 만들어 보시는 것도 좋겠네요.
강좌는 끝났지만, 질문/답변 게시판은 열려 있으니, 언제든 궁금 하신 사항이 있다면 질문 올려 주시길 바랍니다.
그럼 모두들 수고하셨습니다.