유니티로 리듬게임을 만들어보려고 한다.
시장에는 DJMAX와 EZ2ON이라는 아주 훌륭한 참고자료들이 있기 때문에,
이 게임들을 내멋대로 해석해 카피해보고자 한다. 주로 카피할 게임은 DJMAX이다.
그래서 이름도 DJadeMAX RESPECT V로 지었다.
구조 설계
BMS
이미 게임이든 음악이든 아마추어 제작자들이 BMS 데이터를 활용하고 있다.
BMSE와 BMHelper 등 아주 훌륭한 툴들이 있지만, 사용하지 않을 것이다.
MIDI
BMHelper에는 MIDI가 들어간다고 들었다.
MIDI의 구조를 공부한 김에, 노트가 연주되면 키음이 들릴 수 있도록 MIDI와 노트가 하나로 이뤄진 파일을 만들어보고자 한다.
구조 작성
내가 구상한 채보 데이터의 구조는 아래와 같다.
public enum KeyDefine
{
Key4 = 0,
Key5 = 1,
Key6 = 2,
Key8 = 3
}
public enum KeyIndex
{
LeftShift = 0,
KEY_A = 1,
KEY_S = 2,
KEY_D = 3,
KEY_C = 4,
Comma = 5,
KEY_L = 6,
SemiColon = 7,
Quote = 8,
RightShift = 9
}
[Serializable]
public struct Project
{
public List<Head> heads; // 헤드 정보
public string title; // 곡 제목
public string artist; // 아티스트
public string genre; // 장르
public string maker; // 제작자
public string comment; // 비고
public string background; // 배경 이미지, 로딩화면에 사용
public string music; // 음악 파일
public uint deltaLength; // 음악 길이, ms
public byte BPM; // Beat Per Minute, 음악 속도
}
[Serializable]
public struct Head
{
public List<Track> tracks; // 트랙 정보
public KeyDefine keyDefine; // 키 설정
public string levelCover; // 커버 이미지, 곡 선택창에 사용
public byte level; // 난이도
}
[Serializable]
public struct Track
{
// MIDI 설정
public uint channel; // 채널
public uint note; // 음계
public uint velocity; // 음량
public uint duration; // 음 길이
public uint instrument; // 악기 종류
// 노트 설정
public uint deltaTime; // 노트 출현 시간
public uint noteLength; // 노트 길이, 롱 노트일 경우에만 사용, 기본값 0
public KeyIndex keyIndex; // 키 Index
}
enum
으로 들어간 것들은 원래 byte형으로 넣으려고 했는데, 아무리 봐도 직관적이지 않아 수정했다.
level
과 BPM
은 255를 넘지 않을 것이기 때문에 byte형으로 넣긴 했는데........
수만개의 데이터가 들어갈 Track의 미디 데이터가 아니면 솔직히 의미 없을 것 같긴 하다.
변수들은 구조체 padding을 고려해 (C#에서도 의미가 있는지는 모르겠지만) 크기가 큰 순서대로 정렬해주었다.
string의 길이가 50Bytes를 넘지 않는다는 가정 하 모든 변수의 자료형에 따라 대략적인 크기를 계산해보았을 때
약 1.2MB의 메모리를 사용하는 것으로 계산됐다.
물론 곡의 난이도와 음악의 길이에 따라 더 많이 쓰긴 하겠지만, 파일 크기가 커봐야 3MB 남짓 될 것이라고 예상하고 데이터를 이진화 후 저장하기로 했다.
복합형 암,복호화
ARMA3의 애드온파일들은 bikey
와 biprivatekey
확장자를 가진 키 페어로 서버에서 애드온의 무결성을 검사한다.
파일을 열어보면 이진화되어있긴 하지만, 공개 키와 개인 키를 사용하는 점,
그리고 파일 안에 키 이름과 RSA1
, RSA2
라는 문구가 있는 것으로 보아 RSA 비대칭 암호화를 사용하는 것으로 보인다.
RSA는 대용량 데이터 처리에 불리하므로 내용물은 암호화하지 않고 애드온 패키지 파일인 pbo
파일 헤더의 서명데이터를 체크해 키 무결성 검사를 하는 것으로 보인다. (그냥 내 추측이지만)
하지만 채보 데이터는 내용물이 공개되면 게임플레이에 지대한 영향을 미칠 수 있기 때문에 내용물도 보호해야 하니
파일 내용을 AES 암호화하고 openssl
로 생성한 비대칭 키 페어를 이용해 AES 암호키와 초기화벡터를 한번 더 암호화한다.
다만, 채보 데이터를 제작하는 과정에서는 내용물을 열어볼 수 있어야 하므로 RSA는 생략하고 그대로 저장한다.
이렇게 되면 프로젝트 파일이 아닌 이상 에디터에서는 다시 열 수 없고, 인게임에서만 데이터 확인이 가능할 것이다.
public class FileIO
{
private struct AESKeyData
{
public byte[] key;
public byte[] iv;
}
public static async Task SaveToFile(Project project, string filePath)
{
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream())
{
byte[] key, iv;
formatter.Serialize(memoryStream, project);
byte[] serializedData = memoryStream.ToArray();
byte[] encryptedData = Crypto.EncryptAES(serializedData, out key, out iv);
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
{
// write to file header
fileStream.Write(key, 0, key.Length);
fileStream.Write(iv, 0, iv.Length);
await fileStream.WriteAsync(encryptedData, 0, encryptedData.Length);
}
}
}
public static async Task<Project> LoadFromFile(string filePath)
{
// Load to file and decrypt
byte[] encryptedData;
byte[] key, iv;
using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
{
key = new byte[16];
iv = new byte[16];
await fileStream.ReadAsync(key, 0, key.Length);
await fileStream.ReadAsync(iv, 0, iv.Length);
encryptedData = new byte[fileStream.Length - key.Length - iv.Length];
await fileStream.ReadAsync(encryptedData, 0, encryptedData.Length);
byte[] serializedData = Crypto.DecryptAES(encryptedData, key, iv);
// Deserialize
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream(serializedData))
{
return (Project)formatter.Deserialize(memoryStream);
}
}
}
public static async Task Export(Project project, string filePath)
{
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream())
{
byte[] key, iv;
formatter.Serialize(memoryStream, project);
byte[] serializedData = memoryStream.ToArray();
byte[] encryptedData = Crypto.EncryptAES(serializedData, out key, out iv);
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
{
// write to file header
// Encrypt key and iv with RSA
RSAParameters rsaParams = Crypto.ReadRSAKey("msePub.pem");
byte[] encryptedKey, encryptedIV;
Crypto.EncryptAESData(key, iv, rsaParams, out encryptedKey, out encryptedIV);
fileStream.Write(encryptedKey, 0, encryptedKey.Length);
fileStream.Write(encryptedIV, 0, encryptedIV.Length);
await fileStream.WriteAsync(encryptedData, 0, encryptedData.Length);
}
}
}
}
하지만 이렇게 하면 인증서 키 파일이 그대로 보여 의미가 없으므로 나중에 HTTPS를 통해 RSA 암,복호화 또는 다른 방식의 암, 복호화를 하는 것으로 수정해야겠다.
'Projects > DJade MAX Respect V' 카테고리의 다른 글
리듬게임 클론코딩 프로젝트 - UI (0) | 2024.01.15 |
---|---|
리듬게임 클론코딩 프로젝트 - 폰트 (0) | 2024.01.02 |