在游戏进行中,玩家会进行各种操作,例如编队、移动、技能、造建筑等,这些操作就是Action。APM(Actions Per Minute),表示每分钟的操作次数,APM可以很好的反映玩家的手速和实力,当然也有高APM的菜鸟和低APM的高手。
在魔兽录像文件中,需要记录下玩家的操作,这些操作是记录在游戏时间段(TimeSlot)数据块中的,这在上一篇博文中有提到。
结构:
在TimeSlot中从第6字节开始到数据块结尾的部分,包含多个玩家数据块(CommandData Block):
1字节:玩家ID;
2~3字节:数据块剩余字节数n;
4~n+4字节:包含该玩家对应的多个操作数据块(ActionBlock)。
ActionBlock结构:
1字节:ActionID,表示操作类型,例如暂停游戏操作的ActionID是0x01;
剩余字节:Action参数,该部分结构需要根据ActionID来确定,有些Action没有这部分。
由于Action类型很多,每种ActionID对应的ActionBlock结构这里不一一列出,下面列出一小部分:
1.暂停游戏
ActionID:0x01
字节数:1
计算APM:否
2.继续游戏
ActionID:0x02
字节数:1
计算APM:否
3.编队
ActionID:0x17
字节数:4+n*8
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3~4字节:选择单位的数量n;
5~4+n*8:选择单位
4.选择编队
ActionID:0x18
字节数:3
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3字节:未知0x03
其他Action请参考文档:http://w3g.deepnode.de/files/w3g_actions.txt
APM计算:
由于暴雪官方并没有提供APM的计算方式,所以APM计算的方式都是前辈牛人们总结出来的,不同的录像分析软件算出来的APM可能会有一些误差。
APM的值等于玩家的有效Action数量除以玩家游戏时间的分钟数。
一个ActionBlock一般表示玩家的一次操作,例如一次编队、暂停游戏。其中部分操作要算入APM中,例如编队,而有些操作不计算APM,例如暂停游戏。另外,还有的ActionBlock是自动生成的,也不算入APM。Action是否算入APM可以查看文档w3g_actions.txt。
其中比较特殊的有ActionID为0x16的Action。这个Action表示选择或取消选择。ActionBlock的第二个字节为0x01表示选择,0x02表示取消选择。一般来说这个Action是算入APM的,但是如果两个相邻的ActionID为0x16的ActionBlock,前一个为取消选择,后一个为选择,那么这两个ActionBlock只算一次有效的Action,因为前一个是自动生成的。
在游戏进行过程中,可能会有玩家暂停游戏的情况,也有玩家在游戏结束前提前退出游戏的情况。在计算APM的时候一定要去掉这部分的时间,这样算出来的APM才准确。
下面的截图就是RepKing录像分析软件没有考虑游戏暂停导致的问题,导致玩家游戏时间大于录像的时长,APM计算不准确。
Java解析Action和APM:
在Player.java中加入action表示玩家的有效操作数:
/** * 操作次数 */ private int action; public int getAction() { return action; } public void setAction(int action){ this.action = action; }在ReplayData.java中,加入对Action的解析,在ReplayData中加入isPause表示游戏是否暂停,在游戏暂停时游戏时间不能增加:
/** * 是否暂停 */ private boolean isPause; /** * 解析一个时间块 */ private void analysisTimeSlot() { offset++; int bytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; // 游戏时间在非暂停状态下增加 int timeIncrement = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); if(!isPause) { time += timeIncrement; } offset += 2; // 解析Action analysisAction(offset + bytes - 2); } /** * 解析TimeSlot中的Action * @param end TimeSlot的结束位置 */ private void analysisAction(int timeSlotEnd) { while(offset != timeSlotEnd) { byte playerId = uncompressedDataBytes[offset]; Player player = getPlayById(playerId); int action = player.getAction(); offset++; int commandDataBlockbytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; int commandDataBlockEnd = offset + commandDataBlockbytes; boolean lastActionWasDeselect = false; while(offset != commandDataBlockEnd) { byte actionId = uncompressedDataBytes[offset]; boolean thisActionIsDeselect = false; if(actionId == 0x16 && uncompressedDataBytes[offset + 1] == 0x02) { thisActionIsDeselect = true; } switch (actionId) { // 暂停游戏 case 0x01: isPause = true; offset++; break; // 继续游戏 case 0x02: isPause = false; offset++; break; case 0x03: offset += 2; break; case 0x04: case 0x05: offset++; break; case 0x06: offset++; while(uncompressedDataBytes[offset] != 0) { offset++; } offset++; break; case 0x07: offset += 5; break; case 0x10: offset += 15; action++; break; case 0x11: offset += 23; action++; break; case 0x12: offset += 31; action++; break; case 0x13: offset += 39; action++; break; case 0x14: offset += 44; action++; break; case 0x16: offset++; byte selectMode = uncompressedDataBytes[offset]; offset++; if(selectMode == 0x02) { action++; } else { if(!lastActionWasDeselect) { action++; } } int number = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; offset += number * 8; break; case 0X17: offset += 2; int n = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; offset += n * 8; action++; break; case 0x18: offset += 3; action++; break; case 0x19: offset += 13; break; case 0x1a: offset++; break; case 0x1b: offset += 10; break; case 0x1c: offset += 10; action++; break; case 0x1d: offset += 9; action++; break; case 0x1e: offset += 6; action++; break; case 0x21: offset += 9; break; case 0x20: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x29: case 0x2a: case 0x2b: case 0x2c: case 0x2f: case 0x30: case 0x31: case 0x32: offset++; break; case 0x27: case 0x28: case 0x2d: offset += 6; break; case 0x2e: offset += 5; break; case 0x50: offset += 6; break; case 0x51: offset += 10; break; case 0x60: offset += 9; while(uncompressedDataBytes[offset] != 0) { offset++; } offset++; break; case 0x61: offset++; action++; break; case 0x62: offset += 13; break; case 0x66: case 0x67: offset++; action++; break; case 0x68: offset += 13; break; case 0x69: case 0x6a: offset += 17; break; case 0x75: offset += 2; break; } lastActionWasDeselect = thisActionIsDeselect; } player.setAction(action); } }在Test.java中,输出计算得到的玩家APM值:
package com.xxg.w3gparser; import java.io.File; import java.io.IOException; import java.util.List; import java.util.zip.DataFormatException; public class Test { public static void main(String[] args) throws IOException, W3GException, DataFormatException { Replay replay = new Replay(new File("C:/Documents and Settings/Administrator/桌面/131224_[UD]crabby_VS_[ORC]LuciferLVZ_LostTemple_RN.w3g")); Header header = replay.getHeader(); System.out.println("版本:1." + header.getVersionNumber() + "." + header.getBuildNumber()); long duration = header.getDuration(); System.out.println("时长:" + convertMillisecondToString(duration)); UncompressedData uncompressedData = replay.getUncompressedData(); System.out.println("游戏名称:" + uncompressedData.getGameName()); System.out.println("游戏创建者:" + uncompressedData.getCreaterName()); System.out.println("游戏地图:" + uncompressedData.getMap()); List<Player> list = uncompressedData.getPlayerList(); for(Player player : list) { System.out.println("---玩家" + player.getPlayerId() + "---"); System.out.println("玩家名称:" + player.getPlayerName()); if(player.isHost()) { System.out.println("是否主机:主机"); } else { System.out.println("是否主机:否"); } System.out.println("游戏时间:" + convertMillisecondToString(player.getPlayTime())); System.out.println("操作次数:" + player.getAction()); System.out.println("APM:" + player.getAction() * 60000 / player.getPlayTime()); if(player.getTeamNumber() != 12) { System.out.println("玩家队伍:" + (player.getTeamNumber() + 1)); switch(player.getRace()) { case 0x01: case 0x41: System.out.println("玩家种族:人族"); break; case 0x02: case 0x42: System.out.println("玩家种族:兽族"); break; case 0x04: case 0x44: System.out.println("玩家种族:暗夜精灵"); break; case 0x08: case 0x48: System.out.println("玩家种族:不死族"); break; case 0x20: case 0x60: System.out.println("玩家种族:随机"); break; } switch(player.getColor()) { case 0: System.out.println("玩家颜色:红"); break; case 1: System.out.println("玩家颜色:蓝"); break; case 2: System.out.println("玩家颜色:青"); break; case 3: System.out.println("玩家颜色:紫"); break; case 4: System.out.println("玩家颜色:黄"); break; case 5: System.out.println("玩家颜色:橘"); break; case 6: System.out.println("玩家颜色:绿"); break; case 7: System.out.println("玩家颜色:粉"); break; case 8: System.out.println("玩家颜色:灰"); break; case 9: System.out.println("玩家颜色:浅蓝"); break; case 10: System.out.println("玩家颜色:深绿"); break; case 11: System.out.println("玩家颜色:棕"); break; } System.out.println("障碍(血量):" + player.getHandicap() + "%"); if(player.isComputer()) { System.out.println("是否电脑玩家:电脑玩家"); switch (player.getAiStrength()) { case 0: System.out.println("电脑难度:简单的"); break; case 1: System.out.println("电脑难度:中等难度的"); break; case 2: System.out.println("电脑难度:令人发狂的"); break; } } else { System.out.println("是否电脑玩家:否"); } } else { System.out.println("玩家队伍:裁判或观看者"); } } List<ChatMessage> chatList = uncompressedData.getReplayData().getChatList(); for(ChatMessage chatMessage : chatList) { String chatString = "[" + convertMillisecondToString(chatMessage.getTime()) + "]"; chatString += chatMessage.getFrom().getPlayerName() + " 对 "; switch ((int)chatMessage.getMode()) { case 0: chatString += "所有人"; break; case 1: chatString += "队伍"; break; case 2: chatString += "裁判或观看者"; break; default: chatString += chatMessage.getTo().getPlayerName(); } chatString += " 说:" + chatMessage.getMessage(); System.out.println(chatString); } } private static String convertMillisecondToString(long millisecond) { long second = (millisecond / 1000) % 60; long minite = (millisecond / 1000) / 60; if (second < 10) { return minite + ":0" + second; } else { return minite + ":" + second; } } }输出:
版本:1.26.6059
时长:15:39
游戏名称:当地局域网内的游戏 (96
游戏创建者:962030958
游戏地图:Maps\E-WCLMAP\(2)AncientIsles.w3x
---玩家1---
玩家名称:962030958
是否主机:主机
游戏时间:15:38
操作次数:2635
APM:168
玩家队伍:1
玩家种族:不死族
玩家颜色:黄
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:flygogogo
是否主机:否
游戏时间:15:37
操作次数:3483
APM:222
玩家队伍:2
玩家种族:兽族
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否
[0:12]962030958 对 所有人 说:glgl
[4:53]962030958 对 所有人 说:==
[4:53]962030958 对 所有人 说:猫咪爬到键盘上了
[4:53]962030958 对 所有人 说:g?
[4:53]flygogogo 对 所有人 说:g
[15:32]flygogogo 对 所有人 说:
[15:35]flygogogo 对 所有人 说:gg
[15:36]flygogogo 对 所有人 说:
参考文档:http://w3g.deepnode.de/files/w3g_actions.txt
结束语:
《Java解析魔兽争霸3录像W3G文件》系列博文就写到这里了,当然还有很多可以继续写的东西,例如判断玩家胜负,判断玩家的英雄、单位等。需要源码的同学可以在评论中留下E-mail。
Github地址:https://github.com/wucao/jw3gparser 欢迎star!
作者:叉叉哥 转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/19326555