Python을 기반으로 개발을 진행하였습니다.
1. TELEMETRY API 불러오기
TELEMETRY API에 대한 정보는 아래의 링크에서 확인할 수 있습니다.
https://documentation.pubg.com/en/telemetry.html
Telemetry — pubg 1.0 documentation
Telemetry Telemetry provides further insight into a match. This is where you will find the Telemetry Events that occur throughout the match. In order to get the telemetry data, you will first need to get the link for it from within the match object. Detail
documentation.pubg.com
TELEMETRY API를 불러오기 위해서는 MATCHES API를 불러와야 합니다. MATCHES API에서 원격 분석 파일에 대한 링크를 제공합니다.
# match api를 불러오는 함수
def get_match_data(api_key: str, match_id: str) -> dict:
url = f'https://api.pubg.com/shards/steam/matches/{match_id}'
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/vnd.api+json"
}
response = requests.get(url, headers=headers)
return response.json()
# 매치 데이터에서 텔레메트리 URL을 가져오는 함수
def get_telemetry_url(match_data: dict) -> str:
for item in match_data.get("included", []):
if item["type"] == "asset":
return item["attributes"]["URL"]
return None
# 텔레메트리 데이터를 가져오기 위한 함수
def get_telemetry_data(telemetry_url: str) -> list:
response = requests.get(telemetry_url)
return response.json()
위의 코드를 실행하여 response.json()을 출력하게 되면 아래와 같이 나타납니다.
[
{
'MatchId': 'match.bro.competitive.pc-2018-24.kakao.squad.kakao.2023.06.17.02.e51170a3-1f3f-4660-a924-b9f41dae04d5',
'PingQuality': '',
'_D': '2023-06-17T02:36:05.4435945Z',
'_T': 'LogMatchDefinition'
},
{
'accountId': 'account.9391f40fd3f64b6c86f4da43907509cf',
'common': {'isGame': 0},
'_D': '2023-06-17T02:34:56.349Z',
'_T': 'LogPlayerLogin'
},
{
'character': {'name': 'BamBi_Cherry', 'teamId': 2, 'health': 100, 'location': {'x': 796655.8125, 'y': 20385.26171875, 'z': 547.231201171875}, 'ranking': 0, 'individualRanking': 0, 'accountId': 'account.9391f40fd3f64b6c86f4da43907509cf', 'isInBlueZone': False, 'isInRedZone': False, 'zone': []}, 'common': {'isGame': 0}, '_D': '2023-06-17T02:34:56.365Z', '_T': 'LogPlayerCreate'
},
{
'accountId': 'account.eeaaabdc18f243cb84d96547e7fee
결과에서 좌표 데이터 x, y, z를 확인할 수 있습니다.
데이터에서 필요한 정보만 추출하여 보기 좋게 정리하기 위해 데이터 프레임으로 만들면 다음과 같습니다.
# 보기 좋게 데이터 프레임으로 출력 코드
def get_player_positions(telemetry_data: list) -> list:
player_positions = []
for event in telemetry_data:
if event["_T"] == "LogPlayerPosition":
position_data = {
"timestamp": event["_D"],
"player_name": event["character"]["name"],
"location_x": event["character"]["location"]["x"],
"location_y": event["character"]["location"]["y"],
"location_z": event["character"]["location"]["z"],
}
player_positions.append(position_data)
player_positions_df = pd.DataFrame(player_positions)
player_positions_player_sort = player_positions_df.sort_values(by = 'player_name')
return player_positions_player_sort
player_positions_player_sort 데이터 프레임을 출력하게 되면 다음과 같습니다.
timestamp player_name location_x location_y location_z
328 2023-06-17T02:35:51.708Z 4050bosa2 191367.421875 514403.187500 1322.589966
1192 2023-06-17T02:38:11.742Z 4050bosa2 418571.250000 324442.875000 1035.089966
1132 2023-06-17T02:38:01.721Z 4050bosa2 418573.468750 324442.000000 1035.089966
638 2023-06-17T02:36:41.720Z 4050bosa2 443475.000000 253126.406250 112098.476562
142 2023-06-17T02:35:21.713Z 4050bosa2 189149.000000 518973.593750 886.299988
... ... ... ... ... ...
1501 2023-06-17T02:39:04.248Z woong070409 582429.062500 484496.468750 1331.060059
3803 2023-06-17T02:46:34.241Z woong070409 591804.125000 474479.375000 328.529999
279 2023-06-17T02:35:44.225Z woong070409 797820.187500 18834.300781 538.830017
4390 2023-06-17T02:48:54.245Z woong070409 565797.375000 387782.875000 1454.969971
1324 2023-06-17T02:38:34.221Z woong070409 582098.125000 484318.250000 982.059998
[6966 rows x 5 columns]
player_positions_player_sort의 값들을 보았을 때 6966 rows인 것을 보아 한 경기에 모든 플레이어의 위치를 분 단위로 기록한 것을 알 수 있습니다.
하지만 검색한 유저의 위치 정보를 출력하는 것이 목표이기 때문에 아래와 같이 player_name 필터링을 하면서 이름, x좌표, y좌표를 positions 리스트에 추가해 줍니다. 이때 이미지 위에 출력할 것이기 때문에 x, y좌표만 필요함으로 z 좌표는 추출하지 않습니다.
# 텔레메트리 데이터에서 플레이어 위치 데이터를 추출하는 함수
def get_player_positions(telemetry_data: list) -> list:
positions = []
for event in telemetry_data:
if event["_T"] in ("LogPlayerPosition", "LogParachuteLanding"):
if event["character"]["name"] == 'breakthebalance':
positions.append({
"player_name": event["character"]["name"],
"location_x": event["character"]["location"]["x"],
"location_y": event["character"]["location"]["y"]
})
return positions
추출에 성공했다면 좌표대로 이미지 위에 선을 그리는 코드를 작성합니다.
# 선으로 플레이어 위치를 연결하여 맵 이미지에 그리는 함수
def draw_positions_on_map(map_image_path, positions, scale_factor=0.001):
map_image = cv2.imread(map_image_path)
for index in range(len(positions)-1):
x = int(positions[index]["location_x"] * scale_factor)
y = int(positions[index]["location_y"] * scale_factor)
x1 = int(positions[index+1]["location_x"] * scale_factor)
y1 = int(positions[index+1]["location_y"] * scale_factor)
cv2.line(map_image, (x, y), (x1, y1), (255, 0, 0), 5)
plt.figure(figsize=(8, 8))
plt.imshow(cv2.cvtColor(map_image, cv2.COLOR_BGR2RGB))
plt.show()
return 0
위의 함수들을 실행하기 위해서는 아래와 같이 api_key 값과 match_id 값을 가지고 있어야 합니다.
또한 맵 이미지는 공식 홈페이지에서 다운로드해서 사용해야 합니다. 이때 맵의 정보를 미리 알고 있어야 하기 때문에 맵 정보를 match 데이터를 불러올 때 확인해 줍니다.
# 사용자 설정 값
api_key = "your_api_key"
# PLAYERS API에서 추출한 "breakthebalance"님의 matches id중 하나를 대입
match_id = 'e51170a3-1f3f-4660-a924-b9f41dae04d5'
map_image_path = "C:/Users/Admin/Desktop/pubg_map.jpg"
# 코드 실행 부분
match_data = get_match_data(api_key, match_id)
telemetry_url = get_telemetry_url(match_data)
telemetry_data = get_telemetry_data(telemetry_url)
positions = get_player_positions(telemetry_data)
draw_positions_on_map(map_image_path, positions)
이때 map_name에 대한 정보는 아래와 같습니다.
{
"Baltic_Main": "Erangel (Remastered)",
"Chimera_Main": "Paramo",
"Desert_Main": "Miramar",
"DihorOtok_Main": "Vikendi",
"Erangel_Main": "Erangel",
"Heaven_Main": "Haven",
"Kiki_Main": "Deston",
"Range_Main": "Camp Jackal",
"Savage_Main": "Sanhok",
"Summerland_Main": "Karakin",
"Tiger_Main": "Taego"
}
2. 코드 실행
위의 전체 코드를 실행시키면 결과는 다음과 같이 출력됩니다.
위의 실행 결과에서 다소 이상한 부분이 있습니다.
이는 x, y좌표의 값의 맵의 설정값에 비해 매우 큰 값으로 나타나 임의로 줄여서 출력했기 때문입니다.
맵의 좌표 최댓값은 아래와 같습니다.
위치 값은 센티미터 단위로 측정됩니다.
(0,0)은 각 맵의 왼쪽 상단에 있습니다.
X 및 Y 축의 범위는 Erangel, Miramar, Taego 및 Deston의 경우 0 - 816,000입니다.
X 및 Y 축의 범위는 Vikendi의 경우 0 - 612,000입니다.
X 및 Y 축의 범위는 Sanhok의 경우 0 - 408,000입니다.
X 및 Y 축의 범위는 Paramo의 경우 0 - 306,000입니다.
X축과 Y축의 범위는 Karakin과 Range에 대해 0 - 204,000입니다.
X 및 Y축의 범위는 Haven의 경우 0 - 102,000입니다.
해당 맵은 Paramo이기 때문에 최댓값이 408,000이어야 합니다. 하지만 x, y좌표를 추출해서 max 값을 출력해 보면 x좌표가 677,761까지도 출력이 되는 것을 볼 수 있습니다.
그렇기 때문에 맵과 수의 크기를 맞추기 위해 임의로 0.001을 곱해서 출력했기 때문에 문제가 발생했습니다.
3. 수정 및 보안 할점
- match_id를 수정으로 입력하여 하나의 게임만 출력 되는 것을 자동으로 match_id를 가져와 대입시켜 여러 판의 게임을 가져오게 구현해야 합니다. -> 자동화
- 맵 이름을 수동으로 파악한 뒤 이미지를 준비해야 하는 과정을 if 문을 통해 자동으로 맵 이름에 맞는 이미지를 불러오고 해당 맵 위에 알맞은 이동 경로를 출력할 수 있게끔 구현해야 합니다.
- 공식 사이트에서 제공하는 맵의 최대 범위와 api로 불러와 출력한 최대 범위가 맞지 않는 문제의 원인을 찾아 해결해야 합니다.
- 비행기의 이동 루트 및 자기장의 위치, top 10 팀의 이동경로, 죽은 지점 등을 맵에 그림으로 그려지게 구현하는 방법을 찾아 구현할 계획입니다.
'게임 데이터 분석' 카테고리의 다른 글
[게임 데이터 분석 #5] MongoDB를 사용한 데이터 저장 (1) | 2023.06.30 |
---|---|
[게임 데이터 분석 #4-1] TELEMETRY API를 활용해 판별 MAP 이미지 위에 이동 경로 그리기 (1) | 2023.06.25 |
[게임 데이터 분석 #3] chicken-dinner를 활용한 리플레이 애니메이션 출력하기 (0) | 2023.06.14 |
[게임 데이터 분석 #2] PUBG API를 활용한 판별, 팀별 통계 (0) | 2023.06.11 |
[게임 데이터 분석 #1] PUBG API를 활용한 간단한 통계 (0) | 2023.06.10 |