java解析魔兽争霸3录像_Java解析魔兽争霸3录像W3G文件(五):Action和APM计算
在游戲進行中,玩家會進行各種操作,例如編隊、移動、技能、造建筑等,這些操作就是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
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 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 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 對 所有人 說:
結束語:
《Java解析魔獸爭霸3錄像W3G文件》系列博文就寫到這里了,當然還有很多可以繼續寫的東西,例如判斷玩家勝負,判斷玩家的英雄、單位等。需要源碼的同學可以在評論中留下E-mail。
總結
以上是生活随笔為你收集整理的java解析魔兽争霸3录像_Java解析魔兽争霸3录像W3G文件(五):Action和APM计算的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android抓trace工具,Andr
- 下一篇: 国外优秀Windows7桌面插件RAIN