[轉載]熱血傳奇之資源文件與地圖的讀取分析
Mr.Johness
阿何的程序人生
JMir——Java版熱血傳奇2之資源文件與地圖
我雖然是90後,但是也很喜歡熱血傳奇2(以下簡稱“傳奇”)這款遊戲。
進入程序員行業後自己也對傳奇客戶端實現有所研究,現在將我的一些研究結果展示出來,如果大家有興趣的話不妨與我交流。
項目我托管到codeplex上了,使用GPLv2開源協議。大家可以checkout代碼出來看。
我現在將地圖加載出來了,算是達到了裏程碑1吧。
如果要將傳奇的地圖和資源文件詳細解析可能我得寫上幾萬字,不過我現在越來越懶了,就只將讀取wix、wil、map文件的方法和它們的解析貼出來吧。
準備工作:
熱血傳奇十周年客戶端
JDK7
Eclipse
註意:
閱讀此篇文章後您將不需要再到網絡上搜索傳奇資源文件和地圖文件解析,因為我的隨筆絕對是最全最完整最詳細的!但這可能需要您花費一些耐心。
第一部分——地圖:
第一節——描述:
Q: Tile是什麽?
A: Tile在中文是“瓷磚”、“塊”的意思,具體到傳奇地圖中就是48*32屏幕像素大小的矩形區域。單個傳奇地圖就是由多個Tile構成的。
Q: map格式文件究竟存放了哪些信息?
A: map格式文件保存了一個完成地圖的所有信息,但是對於當前Tile的圖片只是保存了一個索引而不是把圖片色彩數據保存下來。
Q: map格式文件怎樣讀取?
A: 對於文件讀取以及對應到Java語言中的數據類型和數據結構我們要從兩方面考慮。
一是map的數據內容:
map文件分為兩部分。一個文件頭標識了當前地圖的高度、寬度等重要信息;剩余部分則是多個Tile的詳細信息
二是map格式文件是由Object-Pascal(以下簡稱Delphi)語言序列化而成的,我們首先需要了解從Delphi序列化的數據到Java反序列化需要進行的操作。
以上內容表明了地圖的信息,熱血傳奇中地圖由Tile構成,每個Tile對應48*32屏幕像素大小。
.map文件則保存了地圖的寬度、高度以及每個Tile的詳細信息。
第二節——對應:
.map文件如果對應到編程語言中數據結構的話在Delphi中如下(文件頭):
1 TMapHeader = packed record
2 ?wWidth:?Word;
3 ?wHeight?:Word;
4 ?sTitle?:String[16];
5 ?UpdateDate?:TDateTime;
6 ?Reserved?:array[0..22] of Char;
(Tile,兩種都可以):
復制代碼
1 type
2 TMapInfo = packed record
3 ?wBkImg?:Word;
4 ?wMidImg?:Word;
5 ?wFrImg?:Word;
6 ?btDoorIndex?:Byte;
7 ?btDoorOffset?:Byte;
8 ?btAniFrame?:Byte;
9 ?btAniTick?:Byte;
10 ?btArea?:Byte;
11 ?btLight?:Byte;
12
13 type
14 TMapInfo = packed record
15 ?wBigTileImg?:Word;
16 ?wSmTileImg?:Word;
17 ?wObjImg?:Word;
18 ?btDoorIndex?:Byte;
19 ?btDoorOffset?:Byte;
20 ?btAniFrame?:Byte;
21 ?btAniTick?:Byte;
22 ?btObjFile?:Byte;
23 ?btLight?:Byte;
復制代碼
每個.map文件如果在Delphi中就成了一個TMapHeader加wWidth*wHeight個MapTile。
(對於每個字段占用的字節數請查看下面Java代碼中註釋)
由於我們是使用Java語言描述熱血傳奇地圖,所以我針對上述兩個數據結構使用Java語言進行了描述:
復制代碼
1 package org.coderecord.jmir.entt.internal;
2
3 import java.util.Date;
4
5 /**
6 * 熱血傳奇2地圖文件頭
7 * <p>
8 * 針對*.map文件的數據結構使用Java語言描述
9 * <br>
10 * 地圖文件頭為52字節,在Pascal中定義為
11 * <br>
12 * TMapHeader = packed record
13 * <br>
14 *  wWidth: Word;
15 * <br>
16 *  wHeight :Word;
17 * <br>
18 *  sTitle :String[16];
19 * <br>
20 *  UpdateDate :TDateTime;
21 * <br>
22 *  Reserved :array[0..22] of Char;
23 * </p>
24 * <p>
25 * <b>wWidth</b> 表示地圖寬度(占用兩個字節,相當於Java語言short;一般不超過1000)
26 * <br>
27 * <b>wHeight</b> 表示地圖高度(占用兩個字節,相當於Java於洋short;一般不超過1000)
28 * <br>
29 * <b>sTitle</b> 標題,靜態單字符串(占用17個字節,首字節為字符串已使用的長度即已存放的字符數,一般為“Legend of mir”)
30 * <br>
31 * <b>UpdateDate</b> 地圖最後更新時間(占用8個字節,為TDateTime類型,[email protected]
32 * <br>
33 * <b>Reserved</b> 保留字符,固定為23字節
34 * </p>
35 *
36 * @author ShawRyan
37 *
38 */
39 public class MapHeader {
40
41 /** 地圖寬度(橫向長度) */
42 private short width;
43 /** 地圖高度(縱向長度) */
44 private short height;
45 /** 標題 */
46 private String title;
47 /** 更新日期 */
48 private Date updateDate;
49 /** 保留字符 */
50 private char[] reserved;
51
52 /** 默認構造函數 */
53 public MapHeader() {}
54 /** 帶全部參數的構造函數 */
55 public MapHeader(short width, short height, String title, Date updateDate, char[] reserved) {
56 this.width = width;
57 this.height = height;
58 this.title = title;
59 this.updateDate = updateDate;
60 this.reserved = reserved;
61 }
62 /** 使用已有對象構造實例 */
63 public MapHeader(MapHeader mapHeader) {
64 this.width = mapHeader.getWidth();
65 this.height = mapHeader.getHeight();
66 this.title = mapHeader.getTitle();
67 this.updateDate = mapHeader.getUpdateDate();
68 this.reserved = mapHeader.getReserved();
69 }
70
71 /** 獲取地圖寬度(橫向長度) */
72 public short getWidth() {
73 return width;
74 }
75 /** 設置地圖寬度(橫向長度) */
76 public void setWidth(short width) {
77 this.width = width;
78 }
79 /** 獲取地圖高度(縱向長度) */
80 public short getHeight() {
81 return height;
82 }
83 /** 設置地圖高度(縱向長度) */
84 public void setHeight(short height) {
85 this.height = height;
86 }
87 /** 獲取標題 */
88 public String getTitle() {
89 return title;
90 }
91 /** 設置標題 */
92 public void setTitle(String title) {
93 this.title = title;
94 }
95 /** 獲取更新時間 */
96 public Date getUpdateDate() {
97 return updateDate;
98 }
99 /** 設置更新時間 */
100 public void setUpdateDate(Date updateDate) {
101 this.updateDate = updateDate;
102 }
103 /** 獲取保留字符 */
104 public char[] getReserved() {
105 return reserved;
106 }
107 /** 設置保留字符 */
108 public void setReserved(char[] reserved) {
109 this.reserved = reserved;
110 }
111 }
復制代碼
(Tile我使用了兩種描述方式,後一種用於生產環境更加優秀):
復制代碼
1 package org.coderecord.jmir.entt.internal;
2
3 /**
4 * 熱血傳奇2地圖“塊”
5 * <br>
6 * 即 “<b>邏輯坐標</b>”點(人物/NPC等放置需要占用一個邏輯坐標點)
7 * <br>
8 * 需要註意的是邏輯坐標和屏幕坐標是不一樣的,屏幕坐標一般為像素值,根據顯示器分辨率設置而有所不同
9 * <br>
10 * 熱血傳奇2中一個邏輯坐標點(地圖塊)需要占用 48 * 32 屏幕坐標大小
11 * <br>
12 * 每個地圖塊為2層結構,包括‘地’和‘空’
13 * 例如樹葉投影下的地圖塊就是2層,包括地表及物體(如有突起石頭的地面或有水流的地面)和樹葉
14 * <p>
15 * 在Pascal語言中使用以下數據結構對地圖塊進行描述和存儲(兩種)
16 * <br>
17 * type
18 * <br>
19 * TMapInfo = packed record
20 * <br>
21 *  wBkImg :Word;
22 * <br>
23 *  wMidImg :Word;
24 * <br>
25 *  wFrImg :Word;
26 * <br>
27 *  btDoorIndex :Byte;
28 * <br>
29 *  btDoorOffset :Byte;
30 * <br>
31 *  btAniFrame :Byte;
32 * <br>
33 *  btAniTick :Byte;
34 * <br>
35 *  btArea :Byte;
36 * <br>
37 *  btLight :Byte;
38 * </p>
39 * <p>
40 * type
41 * <br>
42 * TMapInfo = packed record
43 * <br>
44 *  wBigTileImg :Word;
45 * <br>
46 *  wSmTileImg :Word;
47 * <br>
48 *  wObjImg :Word;
49 * <br>
50 *  btDoorIndex :Byte;
51 * <br>
52 *  btDoorOffset :Byte;
53 * <br>
54 *  btAniFrame :Byte;
55 * <br>
56 *  btAniTick :Byte;
57 * <br>
58 *  btObjFile :Byte;
59 * <br>
60 *  btLight :Byte;
61 * </p>
62 * <p>
63 * <b>wBkImg</b>或<b>wBigTileImg</b> 表示地圖地表圖片,如果最高位為1則表示不能通過(或站立),如河水型地表等。在判斷是否可以飛過(從空中通過)時則不需要考慮
64 * <br>
65 * <b>wMidImg</b>或<b>wSmTileImg</b> 表示地圖可視物體圖片(有時被稱為可視數據/中間層/小地圖塊/地圖補充背景等等),如果wBkImg(或wBigTileImg)沒有鋪滿則使用此地圖塊進行鋪墊。最高位不作為判斷依據,不過圖片索引一般小於0x8000,即最高位一般為0。例如在某地圖中第一個地圖塊的wBkImg(或wBigTileImg)大小為96 * 64,則代表該地圖左上角4個塊兒的地表都不為空,此時緊鄰的三個地圖塊都可以不用設置wBkImg(或wBigTileImg)和wMidImg(或wSmTileImg);如果某個地圖塊的沒有被其他塊兒的wBkImg(或wBigTileImg)鋪滿,自己也沒有wBkImg(或wBigTileImg),那麽它就需要一個wMidImg(或wSmTileImg)進行鋪墊。值得一提的是並不一定在有了wMidImg(或wBigTileImg)後就不需要繪制此層圖片了
66 * <br>
67 * <b>wFrImg</b>或<b>wObjImg</b> 表示表層圖片(對象),即空中遮擋物,如植物或建築物,如果最高位為1則表示不能通過(或站立)。在判斷是否可飛過(從空中通過)時需要作為唯一條件判斷,在判斷是否可以徒步通過或站立時需要聯合wBkImg進行判斷
68 * <br>
69 * <b>總的來說,地圖一般為兩層(只是針對上面的三個屬性,下方的也屬於地圖部分,不過先不納入考慮),包括背景層與對象層,背景層為wBkImg(或wBigTileImg)和wMidImg(或wSmTileImg)的集合,一般來說wBkImg就能搞定,也有時候需要兩者都有;Spirit(人物/怪物/NPC/掉落物品等)在兩層中間;索引從1開始,所以在從資源中真正取圖片時應該減1(適用於所有資源索引);索引一般最高位為0,為1一般表示特殊情況(在Java語言中可以理解為大於0,因為首位為1表示負數)</b>
70 * <br>
71 * <b>btDoorIndex</b> 門索引,最高位為1表示有門,為0表示沒有門。
72 * <br>
73 * <b>btDoorOffset</b> 門偏移,最高位為1表示門打開了,為0表示門為關閉狀態
74 * <br>
75 * <b>btAniFrame</b> 幀數,指示當前地圖塊動態內容由多少張靜態圖片輪詢播放,需要和btAniTick一起起作用;如果最高位為1(即值大於0x80,或者在Java中為小於0的數值)則表示有動態內容
76 * <br>
77 * <b>btAniTick</b> 跳幀數,指示當前地圖塊動態內容應該每隔多少幀變換當前顯示的靜態圖片,需要和btAniFrame一起作用
78 * <br>
79 * <b>btAniFrame和btAniTick作用時表達式如下index = (gAniCount % (btAniFrame * (1 + btAniTick))) / (1 + btAniTick)
80 * <br>
81 *  其中gAniCount是當前畫面幀是第幾幀,它會在每次繪制遊戲界面時累加,它可以有最大值,超過可以置0;index是相對當前objImgIdx的偏移量,比如當前對象層圖片索引為1,而AniFrame為10,則表示從1到11這10副圖片應該作為一動態內容播放(有待考證)
82 * </b>
83 * <br>
84 * <b>btArea</b>或<b>btObjFile</b> 表示當前wFrImg(或wObjImg)和動態內容構成圖片來自哪個Object資源文件,具體為Object{btArea}.wil中,如果btArea為0則是Objects.wil
85 * <br>
86 * <b>btLight</b> 亮度,一般為0/1/4
87 * </p>
88 * @author ShawRyan
89 *
90 */
91 public class MapTile {
92
93 /** 背景圖索引 */
94 private short bngImgIdx;
95 /** 補充背景圖索引 */
96 private short midImgIdx;
97 /** 對象圖索引 */
98 private short objImgIdx;
99 /** 門索引 */
100 private byte doorIdx;
101 /** 門偏移 */
102 private byte doorOffset;
103 /** 動畫幀數 */
104 private byte aniFrame;
105 /** 動畫跳幀數 */
106 private byte aniTick;
107 /** 資源文件索引 */
108 private byte objFileIdx;
109 /** 亮度 */
110 private byte light;
111
112 /** 默認構造函數 */
113 public MapTile() { }
114 /** 使用已有對象構造實例 */
115 public MapTile(MapTile mapTile) {
116 this.bngImgIdx = mapTile.bngImgIdx;
117 this.midImgIdx = mapTile.midImgIdx;
118 this.objImgIdx = mapTile.objImgIdx;
119 this.doorIdx = mapTile.doorIdx;
120 this.doorOffset = mapTile.doorOffset;
121 this.aniFrame = mapTile.aniFrame;
122 this.aniTick = mapTile.aniTick;
123 this.objFileIdx = mapTile.objFileIdx;
124 this.light = mapTile.light;
125 }
126 /** 帶全部參數的構造函數 */
127 public MapTile(short bngImgIdx, short midImgIdx, short objImgIdx, byte doorIdx, byte doorOffset, byte aniFrame, byte aniTick, byte objFileIdx, byte light) {
128 this.bngImgIdx = bngImgIdx;
129 this.midImgIdx = midImgIdx;
130 this.objImgIdx = objImgIdx;
131 this.doorIdx = doorIdx;
132 this.doorOffset = doorOffset;
133 this.aniFrame = aniFrame;
134 this.aniTick = aniTick;
135 this.objFileIdx = objFileIdx;
136 this.light = light;
137 }
138
139 /** 獲取背景圖索引 */
140 public short getBngImgIdx() {
141 return bngImgIdx;
142 }
143 /** 設置背景圖索引 */
144 public void setBngImgIdx(short bngImgIdx) {
145 this.bngImgIdx = bngImgIdx;
146 }
147 /** 獲取補充圖索引 */
148 public short getMidImgIdx() {
149 return midImgIdx;
150 }
151 /** 設置補充圖索引 */
152 public void setMidImgIdx(short midImgIdx) {
153 this.midImgIdx = midImgIdx;
154 }
155 /** 獲取對象圖索引 */
156 public short getObjImgIdx() {
157 return objImgIdx;
158 }
159 /** 設置對象圖索引 */
160 public void setObjImgIdx(short objImgIdx) {
161 this.objImgIdx = objImgIdx;
162 }
163 /** 獲取門索引 */
164 public byte getDoorIdx() {
165 return doorIdx;
166 }
167 /** 設置門索引 */
168 public void setDoorIdx(byte doorIdx) {
169 this.doorIdx = doorIdx;
170 }
171 /** 獲取門偏移 */
172 public byte getDoorOffset() {
173 return doorOffset;
174 }
175 /** 設置門偏移 */
176 public void setDoorOffset(byte doorOffset) {
177 this.doorOffset = doorOffset;
178 }
179 /** 獲取動畫幀數 */
180 public byte getAniFrame() {
181 return aniFrame;
182 }
183 /** 設置動畫幀數 */
184 public void setAniFrame(byte aniFrame) {
185 this.aniFrame = aniFrame;
186 }
187 /** 獲取動畫跳幀數 */
188 public byte getAniTick() {
189 return aniTick;
190 }
191 /** 設置動畫跳幀數 */
192 public void setAniTick(byte aniTick) {
193 this.aniTick = aniTick;
194 }
195 /** 獲取資源文件索引 */
196 public byte getObjFileIdx() {
197 return objFileIdx;
198 }
199 /** 設置資源文件索引 */
200 public void setObjFileIdx(byte objFileIdx) {
201 this.objFileIdx = objFileIdx;
202 }
203 /** 獲取亮度 */
204 public byte getLight() {
205 return light;
206 }
207 /** 設置亮度 */
208 public void setLight(byte light) {
209 this.light = light;
210 }
211 }
復制代碼
復制代碼
1 package org.coderecord.jmir.entt.internal;
2
3 import org.coderecord.jmir.scn.DrawSupport;
4
5 /**
6 * MapTile方便程序邏輯的另類解讀方式
7 *
8 * @author ShawRyan
9 *
10 */
11 public class MapTileInfo {
12 /** 背景圖索引 */
13 private short bngImgIdx;
14 /** 是否有背景圖(在熱血傳奇2地圖中,背景圖大小為4個地圖塊,具體到繪制地圖時則表現在只有橫縱坐標都為雙數時才繪制),[email protected]