[Korean] HaxBall Headless Host - dapucita/haxbotron GitHub Wiki
HaxBall Headless Host by dapucita (2021.01.24.)
이 문서는 헥스볼
게임에서 Headless Host
기능을 활용하는 방법을 설명합니다.
dapucita
가 작성하였으며, Headless Host
의 API 명세
는
헥스볼
의 제작자인 basro
가 공개한 영어 문서(2019년 3월 20일자)를 한국어로 번역하고 보충하였습니다.
이 문서의 내용을 따라하고 활용하기 위해서는 프로그래밍
과 Javascript
에 대한 배경지식이 있어야 합니다.
당장 이해하기 어렵거나 필요하지 않은 내용은 일단 넘어가는 것이 좋을 수도 있습니다.
공식 명세가 변경되는 경우 내용 상의 차이가 발생할 수 있으며 최신 명세를 준수하기 위해서는 원본 문서를 따라야 합니다.
들어가며
헥스볼은 Haxe 언어
로 개발되었으며 다양한 커스텀 맵을 지원하는 온라인 게임입니다.
과거 Adobe Flash
상에서 실행되었지만 HTML5
로 전환되었기에 웹표준을 준수하는 크롬, 파이어폭스
등의 웹브라우저만 있으면 쉽게 즐길 수 있습니다.
헥스볼은 WebRTC
를 이용해 P2P
로 연결되는 멀티 플레이어 게임이기에 다른 사람과 같이 즐기기 위해서는 방을 개설해야 하며, 이때 웹브라우저를 종료하면 방도 같이 종료된다는 문제가 있습니다.
따라서 Headless Host
를 활용하여 방을 개설하면 방장이 상주하지 않더라도 방을 유지하고 게임을 즐길 수 있기에 유용합니다.
Javascript
로 Headless Host
를 구현할 수 있으며, 그래픽과 음향을 요구하지 않기 때문에 서버 환경에서도 실행할 수 있습니다.
실행하기
API
를 활용하여 구현체를 완성했다면 기본적으로는 웹브라우저를 통해 이를 실행할 수 있습니다.
이 페이지에 접속하여 개발자 도구를 열고, 소스코드를 붙여넣으면 됩니다.
크롬의 경우 F12
키를 누르면 개발자 도구를 열 수 있습니다.
소스코드가 웹브라우저에 의해 실행되고 로봇 방지 검증 과정을 통과한 이후에 정상적으로 작동할 것입니다.
자동화하기
방을 개설하기 위해 웹브라우저를 켜고 소스코드를 붙여넣는 것을 반복하는 일은 무척이나 따분하고 원래의 취지에도 맞지 않습니다.
다행히도 Headless Host
를 구현한 소스코드와 웹브라우저를 자동으로 실행하는 소스코드를 결합한다면 방을 개설하고 운영하는 과정을 편리하게 자동화할 수 있습니다.
따라서 웹브라우저를 자동으로 실행하고 제어하기 위해서는 Headless WebBrowser
를 활용해야 합니다.
예를 들어 Puppeteer
를 통해 Node.js
환경에서 Javascript
로 Chromium WebBrowser
를 조작하고 제어할 수 있습니다.
Node.js
환경으로 전환할 때의 장점은 Javascript API
가 제한된 웹브라우저 환경에서 벗어나 보다 다양한 기능을 활용할 수 있다는 것입니다.
이 문서의 작성자인 dapucita
가 개발한 Haxbotron
역시 이런 과정을 통해 프로그램을 자동으로 실행하고 관리하며 게임을 즐길 수 있게 해줍니다. 참고하세요.
접속 문제 해결하기
VPS 등의 서버 환경에서 Headless Host
를 구동하는 경우 일부 플레이어들이 접속하지 못하는 문제가 발생할 수도 있습니다.
이러한 문제는 크롬 웹브라우저 버전 78 이상을 활용하여 구동할 때 발생할 수 있으며, WebRTC
에 관한 보안 설정에 기인하는 문제입니다.
따라서 로컬IP에 대한 WebRTC 익명화 옵션을 비활성화해야 하며 크롬 웹브라우저를 실행할 때 커맨드 옵션으로 --disable-features=WebRtcHideLocalIpsWithMdns
를 붙여주면 됩니다.
보안 설정하기
Headless Host
은 분명 편리한 기능이지만 불행히도 헥스볼
의 태생적인 취약점에 노출되어 있습니다.
헥스볼은 온라인 연결을 지원하기 위해 WebRTC
를 활용하며, 따라서 악의적인 시도에 의해 IP가 유출되고 네트워크 공격을 받을 수 있습니다.
소위 핑폭
현상은 주로 서비스거부공격(DoS), 디도스(DDoS, 분산서비스거부공격)
에 의해 발생하는 것으로, 방을 개설한 컴퓨터에 과도한 부하를 주어 게임 이용을 방해하는 악의적인 공격에 의해 발생합니다.
따라서 이러한 공격을 방어하기 위해서는 서버 운용 시에 활용되는 여러 보안 정책을 적용할 필요가 있습니다.
일부 서버 제공 업체에서는 자체적인 방어 체계를 갖추고 있기 때문에 이러한 네트워크 공격을 원천적으로 방지하는데 도움을 받을 수 있습니다.
서버 환경에서는 모든 네트워크 접근을 기록할 수 있으며 따라서 민/형사법상 대응을 위한 증빙 자료로 제출할 수 있습니다.
다른 프로그래밍 언어 사용하기
Headless Host
를 구현하기 위해서 반드시 Javascript
만을 사용해야 하는 것은 아닙니다.
기본적인 원리는 같으므로 Javascript
로 컴파일되거나, 혹은 웹 컨테이너 래퍼를 제공하는 다른 프로그래밍 언어를 사용할 수 있습니다.
가령 Typescript
와 같은 언어는 Javascript
로 트랜스파일링되기 때문에 Javascript
를 대체할 수 있습니다.
혹은 C# 혹은 Python 등의 다른 언어를 사용해 웹 객체를 조작할 수 있으므로 Javascript
의 대안이 될 수 있습니다.
API 명세
Headless Host
를 활용하기 위해서는 먼저 방(Room)
을 개설하여야 하며, 이를 위한 HBInit
메서드는 window
전역 객체에 포함되어 있습니다.
따라서 window.HBInit
메서드를 호출하여 Room
객체를 초기화할 수 있으며 이후에 window.onHBLoaded
메서드가 호출됩니다.
한편 게임 상에서의 상태(state)를 변화시키는 모든 API 기능은 비동기
로 실행됩니다.
가령 roomObject.getPlayerList
메서드를 통해 플레이어 목록을 가져온 이후에 roomObject.setPlayerTeam
메서드를 사용하여 플레이어를 다른 팀으로 옮기는 경우, 이미 받아온 플레이어 목록에는 이러한 변화가 반영되어 있지 않을 것입니다.
이러한 동기
와 비동기
에 대한 차이를 숙지하여야 합니다.
Typescript
와 같은 강타입 언어로 개발하려는 경우, Headless Host
에 대한 타입 선언을 활용할 수 있습니다. Haxbotron
에는 이러한 타입을 구현해놓은 인터페이스가 포함되어 있습니다. 참고하세요.
Window
전역 객체
📦 HBInit
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
HBInit | 방(Room) 객체를 초기화하여 반환합니다. |
RoomObject |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
roomConfig | RoomConfigObject | 방(Room) 의 세부 사항을 설정합니다. |
방(Room)
를 처음 만들기 위해 사용하며 반환된 객체를 통해 조작하고 제어할 수 있습니다.
이 메서드를 호출하면 Headless Host
페이지 상에 로봇 방지 검증 과정이 시작되며, 이를 통과해야 정상적으로 기능을 수행할 수 있습니다.
let room = HBInit({
roomName: "My room", // 방의 이름입니다.
maxPlayers: 16, // 들어올 수 있는 최대 인원수입니다.
noPlayer: true // 호스트(봇 플레이어) 상주 여부입니다. (true로 설정할 것을 추천합니다)
});
room.setDefaultStadium("Huge"); // Huge 맵을 선택합니다.
room.setScoreLimit(3); // 점수 제한을 5점으로 설정합니다.
room.setTimeLimit(3); // 시간 제한을 3분으로 설정합니다.
// 어드민(방장) 권한을 가진 플레이어가 아무도 없다면 갱신하여 새로 부여합니다.
function updateAdmins() {
let players = room.getPlayerList(); // 모든 플레이어 목록을 가져옵니다.
if ( players.length == 0 ) return; // 아무도 접속중이지 않다면 작동하지 않습니다.
if ( players.find((player) => player.admin) != null ) return; // 어드민이 있다면 작동하지 않습니다.
room.setPlayerAdmin(players[0].id, true); // 목록 상의 첫 번째 플레이어에게 어드민 권한을 부여합니다.
}
room.onPlayerJoin = function(player) { // onPlayerJoin는 플레이어가 접속할 때 실행되는 이벤트 메서드입니다.
updateAdmins(); // 어드민(방장) 권한을 가진 플레이어가 아무도 없다면 갱신하여 새로 부여합니다.
}
room.onPlayerLeave = function(player) { // onPlayerLeave는 플레이어가 나갈 때 실행되는 이벤트 메서드입니다.
updateAdmins(); // 어드민(방장) 권한을 가진 플레이어가 아무도 없다면 갱신하여 새로 부여합니다.
}
RoomConfigObject
객체
📦 Window
전역객체의 HBInit
메서드는 RoomConfigObject
객체를 바탕으로 방(Room)
객체를 초기화합니다.
RoomConfigObject
객체의 모든 멤버 변수는 선택적으로 필요한 값만 설정할 수 있습니다.
멤버변수 | 멤버변수 타입 | 역할 |
---|---|---|
roomName | string | 방의 이름(제목) |
playerName | string | 호스트의 이름 |
password | string | 방의 비밀번호 |
maxPlayers | int | 최대 접속 가능 인원수 |
public | bool | 방 목록에의 노출 여부 |
geo | object literal | 지역 정보 덮어쓰기 |
token | string | 인증 토큰 |
noPlayer | bool | 호스트 표시 여부 |
geo
멤버변수
📇 프로퍼티 | 프로퍼티 타입 | 역할 |
---|---|---|
code | string | 국가 코드 |
lat | float | 위도 |
lon | float | 경도 |
token
멤버변수
📇 Headless Host
를 여는 경우 reCaptcha 인증
이 요구될 수 있습니다.
이 때 token
인증 토큰을 함께 보내면 이 과정을 생략할 수 있으며, 여기에서 발급받을 수 있습니다.
인증 토큰은 수 분 후에 만료됩니다.
noPlayer
멤버변수
📇 이 멤버변수의 값을 true
로 설정하는 경우, 방(Room)
을 열 때 봇 호스트(방장)가 표시되지 않습니다.
기본값은 하위 호환성을 위해 false
로 지정되어 있지만, true
로 설정할 것을 권장합니다.
봇 호스트에 의해 게임 이벤트가 발생하는 경우 byPlayer
매개변수의 값은 null
이 되므로 타입 검사가 필요할 수 있습니다.
RoomObject
객체
📦 RoomObject
객체를 사용하여 방(Room)
을 조작하고 게임 이벤트를 처리할 수 있습니다.
sendChat
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
sendChat | 봇 호스트를 통해 채팅 메시지를 보냅니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
message | string | 채팅 메시지의 본문 |
targetId? | int | 채팅 메시지를 받을 플레이어의 아이디(id) |
targetId
매개변수가 null
값을 가지는 경우 모든 플레이어에게 채팅 메시지가 전송됩니다.
room.sendChat("안녕하세요!", null); // 모든 플레이어에게 보냅니다.
room.sendChat("반갑습니다!", 1); // 아이디가 1인 플레이어에게 보냅니다.
setPlayerAdmin
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setPlayerAdmin | 해당 플레이어의 방장(어드민) 권한을 통제합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerID | int | 대상 플레이어의 아이디 |
admin | bool | 권한 부여 혹은 박탈 |
admin
매개변수가 true
값을 가지는 경우 해당 플레이어에게 권한을 부여하며, false
값을 가지는 경우 반대로 권한을 박탈합니다.
setPlayerTeam
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setPlayerTeam | 해당 플레이어의 팀을 이동합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerID | int | 대상 플레이어의 아이디 |
team | TeamID | 팀 아이디 |
kickPlayer
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
kickPlayer | 해당 플레이어를 추방합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerID | int | 대상 플레이어의 아이디 |
reason | string | 사유 |
ban | bool | 영구추방 여부 |
ban
매개변수가 true
값을 가지는 경우 영구추방(밴)하며, 반대로 false
값을 가지는 경우 일반 추방(킥)을 합니다.
영구추방은 일반적으로 호스트가 인게임 명령어(/clear_bans)
를 사용하거나, 방(Room)
을 재시작해야 해제할 수 있습니다. 다만 Headless Host
에서 호스트는 봇이므로 인게임 명령어(/clear_bans)
를 사용하여 해제하는 것은 불가합니다. 혹은 clearBan
이나 clearBans
와 같은 Headless Host 메서드
를 사용하면 봇을 통해 해제할 수 있습니다.
clearBan
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
clearBan | 해당 플레이어의 영구추방을 해제합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerID | int | 대상 플레이어의 아이디 |
clearBans
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
clearBans | 모든 영구추방을 해제합니다. | void |
setScoreLimit
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setScoreLimit | 게임의 점수 제한을 설정합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
limit | int | 제한 점수 |
이 메서드는 게임이 진행 중인 경우에는 동작하지 않습니다.
setTimeLimit
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setTimeLimit | 게임의 시간 제한 (분 단위)을 설정합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
limitInMinutes | int | 제한 시간 (분 단위) |
이 메서드는 게임이 진행 중인 경우에는 동작하지 않습니다.
setCustomStadium
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setCustomStadium | 사용자 정의(커스텀) 맵을 불러옵니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
stadiumFileContents | string | 맵 파일 정의 |
.hbs
맵 파일 정의를 해석하여 맵으로 불러옵니다.
.hbs
맵 파일은 JSON
규격으로 정의된 텍스트 파일이며, 따라서 Headless Host
에서 사용할 때는 텍스트 내용을 지정해주어야 합니다.
이 메서드는 게임이 진행 중인 경우에는 동작하지 않습니다.
setCustomStadium(stadiumFileContents : string) : void
let stadiumFileText = `...` // 여기에 맵 파일 정의를 삽입하세요.
let room = HBInit({}); // 방을 엽니다.
room.setCustomStadium(stadiumFileText); // 사용자 정의 맵을 불러옵니다.
setDefaultStadium
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setDefaultStadium | 기본 맵을 불러옵니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
stadiumName | string | 맵 이름 |
Haxball
기본 맵을 불러옵니다. 이름은 대소문자를 구분합니다.
이 메서드는 게임이 진행 중인 경우에는 동작하지 않습니다.
setTeamsLock
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setTeamsLock | 팀 이동을 제한합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
locked | bool | 제한 여부 |
locked
매개변수가 true
값을 가지면 일반 플레이어의 팀 이동이 제한되며, 반대로 false
값을 가지면 팀 이동이 허용됩니다. 방장(어드민)은 다른 플레이어를 수동으로 옮길 수 있습니다.
setTeamColors
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setTeamColors | 팀 무늬(색상)를 설정합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
team | TeamID | 팀 아이디 |
angle | float | 줄무늬 각도 |
textColor | int | 플레이어 아바타 색상 |
colors | int[] | 팀 색상 |
textColor
와 colors
매개변수는 0xFF0000
(빨강)처럼 정수 값으로 색상을 지정합니다.
colors
매개변수는 정수 값의 배열을 지정하며 3개의 요소를 갖습니다. 첫 번째 요소는 팀의 기본 색상, 두 번째 요소와 세 번째 요소는 보조 색상입니다.
두 번째와 세 번째 요소는 생략할 수 있으며, 이 때 줄무늬도 사라집니다.
startGame
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
startGame | 게임을 시작합니다. | void |
이 메서드는 게임이 진행 중인 경우에는 동작하지 않습니다.
stopGame
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
stopGame | 게임을 종료합니다. | void |
이 메서드는 게임이 진행 중이지 않은 경우에는 동작하지 않습니다.
pauseGame
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
pauseGame | 게임을 일시정지합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
pauseState | bool | 일시정지 여부 |
pauseState
매개변수가 true
값을 가지면 일시정지하며, false
값을 가지면 다시 재개합니다.
게임을 재개하는 경우 약간의 대기 시간을 거친 후에 경기가 이어집니다.
getPlayer
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getPlayer | 특정 플레이어의 PlayerObject 객체를 가져옵니다. |
PlayerObject |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerId | int | 대상 플레이어의 아이디 |
해당 플레이어가 존재하지 않는(접속 중이지 않은) 경우 null
값을 반환합니다.
getPlayerList
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getPlayer | 모든 플레이어의 PlayerObject 객체 배열을 가져옵니다. |
PlayerObject[] |
방(Room)
에 접속 중인 모든 플레이어의 객체 배열을 반환합니다.
getScores
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getScores | 경기의 점수와 시간 정보를 담은 ScoresObject 객체를 가져옵니다. |
ScoresObject |
이 메서드는 게임이 진행 중이지 않은 경우에는 null
을 반환합니다.
getBallPosition
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getBallPosition | 공의 좌표를 담은PositionObject 객체를 가져옵니다. |
PositionObject |
이 메서드는 게임이 진행 중이지 않은 경우에는 null
을 반환합니다.
startRecording
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
startRecording | 경기 녹화를 시작합니다. | void |
이 메서드를 사용한 후에는 반드시 녹화를 종료하는 stopRecording
메서드를 사용해야 불필요한 낭비를 방지할 수 있습니다.
stopRecording
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
stopRecording | 경기 녹화를 종료합니다. | Uint8Array |
이 메서드는 경기 녹화가 진행 중이지 않은 경우에는 null
을 반환합니다.
녹화 데이터는 일반적으로 .hbr2
파일로 저장합니다. 여기에서 활용법을 참고하세요.
Uint8Array
타입에 대한 자세한 정보는 여기에서 JavaScript 형식화 배열
과 ArrayBuffer
를 참고하세요.
setPassword
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setPassword | 방의 비밀번호를 설정하거나 해제합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
pass? | string | 비밀번호 |
pass
매개변수가 null
값을 가지는 경우 방의 비밀번호를 해제합니다.
setRequireRecaptcha
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setRequireRecaptcha | 방에 접속할 때 reCaptcha 인증을 요구하거나 면제합니다. |
void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
required | bool | 인증 요구 여부 |
required
매개변수가 true
값을 가지면 인증을 요구하며, 반대로 false
값을 가지면 인증을 요구하지 않습니다.
reorderPlayers
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
reorderPlayers | 방에 접속할 때 reCaptcha 인증을 요구하거나 면제합니다. |
void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerIdList | int[] | 대상 플레이어들의 아이디 |
moveToTop | bool | 최상단 이동 여부 |
playerIDList
매개변수는 대상 플레이어들의 아이디 배열을 지정합니다. 배열로 지정된 플레이어들은 우선 플레이어 목록에서 지워지며, 배열 요소의 순서에 따라 다시 표시됩니다.
moveToTop
매개변수가 true
값을 가지면 플레이어 목록의 위에서부터 표시하며, 반대로 false
값을 가지면 아래에서부터 표시합니다.
sendAnnouncement
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
sendAnnouncement | 시스템 메시지를 보냅니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
msg | string | 메시지 본문 |
targetId? | int | 대상 플레이어의 아이디 |
color? | int | 메시지 색상 |
style? | string | 메시지 서식 |
sound? | int | 알람 소리 단계 |
이 메서드는 sendChat
메서드보다 많은 양의 채팅 메시지를 한 번에 전송할 수 있으며, 이 때 봇 호스트는 보낸 사람으로 표시되지 않습니다.
targetID
매개변수가 null
값을 가지면 모든 플레이어에게 메시지를 전송합니다.
color
매개변수는 0xFF0000
(빨강)처럼 정수 값으로 색상을 지정하며, null
값을 가지면 일반 채팅 메시지와 같은 기본 색상으로 표시됩니다.
color
매개변수는 "normal"
, "bold"
, "italic"
, "small"
, "small-bold"
, "small-italic"
중 하나의 값을 가질 수 있으며, null
값을 가지면 "normal"
값을 가지는 것과 동일한 서식을 가집니다.
sound
매개변수는 0
, 1
, 2
중 하나의 값을 가질 수 있습니다. 0
으로 지정하면 알람 소리를 내지 않으며 1
로 지정해야 일반 채팅 메시지와 같은 알람 소리를 냅니다. 2
로 지정하는 경우 경고 알람 소리를 냅니다.
color
매개변수의 가능한 각 값은 워드 프로세서오와 마찬가지로 일반
, 강조
, 기울인 글씨
, 작은 글씨
, 작고 강조한 글씨
, 작고 기울인 글씨
를 의미합니다.
setKickRateLimit
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setKickRateLimit | kick rate 를 제한합니다. |
void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
min | int | kick 사이 허용되는 프레임 수 |
rate | int | kick 제한 수 |
burst | int | kick 비축 수 |
이 메서드는 kick
(킥, 공차기)과 관련된 kick rate
를 설정하고 제한합니다.
min
매개변수는 두 kick
사이의 최소 논리프레임 수를 지정합니다. 이 매개변수의 값보다 빠르게 공을 찰 수는 없으며 기본값은 2
입니다.
rate
매개변수는 min
매개변수와 비슷하지만, burst
매개변수의 값에 따라 플레이어로 하여금 나중에 쓸 여분의 kick
을 모을 수 있게 해주며 기본값은 0
입니다.
burst
매개변수는 플레이어가 여분의 kick
을 얼마만큼 모아둘 수 있는 지를 지정하며, 기본값은 0
입니다.
// 만약 처음 4번의 킥을 2프레임동안 했으면, 이후에는 15프레임마다 1번의 킥만이 허용됩니다. (1초에 60프레임이므로 4번 가능합니다.)
// 1초 동안 킥을 하지 않으면 킥을 비축하여 4번 가능하게 됩니다.
room.setKickRateLimit(2, 15, 3);
setPlayerAvatar
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setPlayerAvatar | 플레이어의 아바타를 덮어씁니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerId | int | 대상 플레이어의 아이디 |
avatar | string | 아바타 |
avatar
매개변수가 null
값을 가지면 덮어쓰기가 취소되고, 플레이어가 스스로 아바타를 지정하는 것이 다시 허용됩니다.
setDiscProperties
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setDiscProperties | 해당 디스크(원반)의 속성을 설정합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
discIndex | int | 대상 디스크(원반)의 아이디 |
properties | DiscPropertiesObject | 디스크 속성 |
properties
매개변수는 DiscPropertiesObject
객체를 값으로 가지며 해당 객체의 null
값을 가지는 각 프로퍼티에 대해서는 기존의 설정이 유지됩니다.
room.setDiscProperties(0, {x: 0, y: 0}); // 0번 디스크의 좌표계를 (0,0)으로 지정하지만 다른 설정은 그대로 유지합니다.
getDiscProperties
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getDiscProperties | 해당 디스크(원반)의 DiscPropertiesObject 객체를 가져옵니다. |
DiscPropertiesObject |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
discIndex | int | 대상 디스크(원반)의 아이디 |
discIndex
매개변수의 값이 유효하지 않은 경우 null
을 반환합니다.
setPlayerDiscProperties
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
setPlayerDiscProperties | 해당 플레이어의 디스크(원반) 속성을 설정합니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerId | int | 대상 플레이어의 아이디 |
properties | DiscPropertiesObject | 디스크 속성 |
이 메서드는 setDiscProperties
메서드와 달리 플레이어를 대상으로 합니다.
getPlayerDiscProperties
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getPlayerDiscProperties | 해당 플레이어의 DiscPropertiesObject 디스크(원반) 객체를 가져옵니다. |
DiscPropertiesObject |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
playerId | int | 대상 플레이어의 아이디 |
이 메서드는 getDiscProperties
메서드와 달리 플레이어를 대상으로 합니다.
getDiscCount
메서드
🗃️ 이름 | 역할 | 반환 타입 |
---|---|---|
getDiscCount | 게임 내 공과 플레이어를 포함한 모든 디스크(원반)의 개수를 구합니다. | int |
CollisionFlags
멤버객체
📦 방(Room)
을 제어하기 위해 사용하는 RoomObject
객체에는 CollisionFlags
멤버객체가 포함되어 있습니다.
CollisionFlags
객체는 CollisionFlagsObject
객체 타입이며, cMask
와 cGroup
디스크(원반) 프로퍼티(속성)로 구성된 충돌 플래그 상수
(collision flags constants)를 담고 있습니다.
사용할 수 있는 충돌 플래그 상수
는 ball
, red
, blue
, redKO
, blueKO
, wall
, all
, kick
, score
, c0
, c1
, c2
, c3
과 같습니다.
자세한 정보는 여기에서 볼 수 있습니다.
// 4번 디스크가 'ball' cGroup 속성을 갖는지 확인합니다.
let discProps = room.getDiscProperties(4);
let hasBallFlag = (discProps.cGroup & room.CollisionFlags.ball) != 0;
// 5번 디스크에 'wall' cMask 속성을 부여하며 다른 속성은 그대로 유지합니다.
let discProps = room.getDiscProperties(5);
room.setDiscProperties(5, {cMask: discProps.cMask | room.CollisionFlags.wall});
📡 게임 이벤트
RoomObject
객체를 사용하여 방(Room)
을 조작하고 게임 이벤트를 처리할 수 있습니다.
이벤트 메서드를 만들고 각 이벤트에 이벤트 리스너로 연결하여 다양한 상황에 대응할 수 있습니다.
room.onPlayerJoin = function(player) { // onPlayerJoin는 플레이어가 접속할 때 실행되는 이벤트 메서드입니다.
// 여기에 이벤트 처리를 구현합니다.
// 이 이벤트에는 player 매개변수가 주어지므로 이를 활용할 수 있습니다.
room.sendChat(`${player.name}님을 환영합니다.`, player.id);
}
onPlayerJoin
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerJoin | 새 플레이어가 접속하였을 때 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
player | PlayerObject | 대상 플레이어의 PlayerObject 객체 |
onPlayerLeave
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerLeave | 해당 플레이어가 나갔을 때 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
player | PlayerObject | 대상 플레이어의 PlayerObject 객체 |
onTeamVictory
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onTeamVictory | 경기 결과가 정해진 후 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
scores | ScoresObject | 경기가 끝난 직후의 결과를 담은 ScoresObject 객체 |
Haxball
게임의 각 경기는 점수 차에 의해 승리 메시지가 표시된 후에야 완전히 종료됩니다. 따라서 onTeamVictory
이벤트가 호출된 후 onGameStop
이벤트가 연이어 호출됩니다.
onPlayerChat
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerChat | 플레이어가 채팅 메시지를 보낼 때 호출됩니다. | bool |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
player | PlayerObject | 해당 플레이어의 PlayerObject 객체 |
message | string | 채팅 메시지 본문 |
이 이벤트는 true
를 반환할 때 채팅 창에 채팅 메시지를 표시하며, 반대로 false
를 반환하면 채팅 메시지를 실제로는 표시하지 않습니다.
onPlayerBallKick
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayonPlayerBallKick | 플레이어가 공을 차면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
player | PlayerObject | 해당 플레이어의 PlayerObject 객체 |
플레이어가 키(슛 혹은 패스)를 눌러 실제로 공을 차는 경우에만 이 이벤트가 호출되며, 단지 몸으로 밀기만 하는 경우에는 호출되지 않습니다.
onTeamGoal
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onTeamGoal | 골이 들어가면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
team | TeamID | 골을 넣은 팀의 아이디 |
자책골을 넣는 경우에도 이 이벤트가 호출됩니다.
onGameStart
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onGameStart | 경기를 시작하면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
byPlayer? | PlayerObject | 경기를 시작한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 경기가 시작되는 경우 byPlayer
매개변수가 null
값을 가집니다.
onGameStop
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onGameStop | 경기가 종료되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
byPlayer? | PlayerObject | 경기를 종료한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 경기가 종료된 경우 byPlayer
매개변수가 null
값을 가집니다.
onPlayerAdminChange
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerAdminChange | 대상 플레이어의 방장(어드민) 권한이 변경되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
changedPlayer | PlayerObject | 권한이 변경된 해당 플레이어의 PlayerObject 객체 |
byPlayer? | PlayerObject | 해당 플레이어에 대해 권한을 변경한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 권한이 변경된 경우 byPlayer
매개변수가 null
값을 가집니다.
onPlayerTeamChange
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerTeamChange | 대상 플레이어의 팀이 변경되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
changedPlayer | PlayerObject | 팀이 변경된 해당 플레이어의 PlayerObject 객체 |
byPlayer? | PlayerObject | 해당 플레이어에 대해 팀을 변경한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 팀이 변경된 경우 byPlayer
매개변수가 null
값을 가집니다.
onPlayerKicked
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerKicked | 플레이어가 추방되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
kickedPlayer | PlayerObject | 추방당한 해당 플레이어의 PlayerObject 객체 |
reason | string | 추방 사유 |
ban | bool | 영구추방 여부 |
byPlayer? | PlayerObject | 해당 플레이어를 추방한 플레이어의 PlayerObject 객체 |
영구추방인 경우 ban
매개변수가 true
값을 가집니다.
플레이어가 아닌 봇에 의해 추방된 경우 byPlayer
매개변수가 null
값을 가집니다.
onPlayerLeave
이벤트가 호출된 후에야 onPlayerKicked
이벤트가 호출됩니다.
onGameTick
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onGameTick | 경기 중에 매 틱(Tick)마다 호출됩니다. | void |
경기 중인 경우 1초에 60회씩 틱(Tick)이 발생합니다.
경기 중이 아니거나 일시정지한 경우에는 이 이벤트가 호출되지 않습니다.
이 이벤트는 플레이어와 공의 위치를 감독하는 데 유용합니다.
onGamePause
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onGamePause | 경기가 일시정지되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
byPlayer? | PlayerObject | 경기를 일시정지한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 경기가 일시정지된 경우 byPlayer
매개변수가 null
값을 가집니다.
onGameUnpause
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onGameUnpause | 경기가 재개되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
byPlayer? | PlayerObject | 경기를 재개한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 경기가 재개된 경우 byPlayer
매개변수가 null
값을 가집니다.
경기는 약간의 대기 시간을 거친 후에야 완전히 재개되므로, 실질적으로 재개된 시점을 감지하기 위해서는 onGameUnpause
이벤트가 호출된 후 첫 번째로 발생하는 onGameTick
이벤트를 활용할 수 있습니다.
onPositionsReset
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPositionsReset | 골 이후 플레이어와 공의 좌표가 초기화(킥오프)되면 호출됩니다. | void |
onPlayerActivity
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onPlayerActivity | 플레이어가 활동할 때마다 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
player | PlayerObject | 대상 플레이어의 PlayerObject 객체 |
플레이어가 키를 누르는 것과 같은 활동을 의미하므로 이 이벤트는 부재 중인 플레이어를 감지할 때 유용합니다.
onStadiumChange
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onStadiumChange | 맵이 열리면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
newStadiumName | string | 열린 맵의 이름 |
byPlayer? | PlayerObject | 맵을 불러온 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 맵이 열린 경우 byPlayer
매개변수가 null
값을 가집니다.
onRoomLink
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onRoomLink | 방 주소(링크)가 발급되면 호출됩니다. | void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
url | string | 방의 URL 주소(링크) |
onKickRateLimitSet
이벤트
📡 이름 | 역할 | 반환 타입 |
---|---|---|
onKickRateLimitSet | kick rate 가 바뀔 때 호출됩니다. |
void |
매개변수 | 매개변수 타입 | 역할 |
---|---|---|
min | int | kick 사이 허용되는 프레임 수 |
rate | int | kick 제한 수 |
burst | int | kick 비축 수 |
byPlayer? | PlayerObject | 변경한 플레이어의 PlayerObject 객체 |
플레이어가 아닌 봇에 의해 바뀐 경우 byPlayer
매개변수가 null
값을 가집니다.
PlayerObject
객체
📦 PlayerObject
객체는 플레이어의 정보를 담고 있습니다.
멤버변수 | 멤버변수 타입 | 역할 |
---|---|---|
id | int | 해당 플레이어의 아이디 |
name | string | 해당 플레이어의 이름 |
team | TeamID | 해당 플레이어의 팀 |
admin | bool | 방장(어드민) 권한의 여부 |
position | PositionObject | 해당 플레이어의 좌표(위치) |
auth | string | 해당 플레이어의 고유 인증값 |
conn | string | 해당 플레이어의 고유 연결값 |
id
멤버변수는 개별 플레이어의 아이디로 고유한 양의 정수 값을 가집니다. 이 값은 변경될 수 없으며, 플레이어가 새로 접속하는 경우 최근에 접속한 플레이어에게 발급된 아이디보다 1
만큼 증가한 새 아이디를 갖게 됩니다.
position
멤버변수는 해당 플레이어가 경기장 내에 위치하지 않는 경우 null
값을 가집니다.
auth
멤버변수는 해당 플레이어에게 주어지는 고유의 문자열 인증값을 가집니다. 플레이어가 동일한 컴퓨터를 사용하더라도 다른 브라우저를 사용하면 이 값은 달라집니다. 따라서 이 멤버변수는 다른 일반적인 게임에서 사용자 계정을 구분하는 것과 마찬가지의 용도로 사용할 수 있습니다. 다만 자동 추방 시스템을 구현하기에는 적절하지 않습니다. onPlayerJoin
이벤트를 통해서만 이 멤버변수의 값이 설정되며, 플레이어 인증의 유효성 검사에 실패하는 경우에는 null
값을 가질 수 있습니다.
플레이어가 인증값을 직접 알고 싶다면 여기에서 Public ID
를 확인하면 됩니다.
conn
멤버변수는 해당 플레이어에게 주어지는 고유의 문자열 연결값을 가집니다. 연결값은 각 네트워크 연결마다 고유한 값을 갖게 됩니다. 따라서 플레이어가 동일한 네트워크를 사용하는 경우 이 값이 같게 되므로 중복 접속을 감지할 때 유용합니다. 이 멤버변수는 onPlayerJoin
이벤트를 통해서만 값이 설정됩니다.
이중으로 접속하는 것을 방지하기 위해서는 conn
멤버변수가 같은 플레이어가 이미 접속 중인지 검사하면 됩니다. 전적(스탯) 저장 시스템을 구현한 경우, 이전의 전적 기록을 불러오고 싶다면 auth
멤버변수가 같은 기존 기록을 불러오면 됩니다.
ScoresObject
객체
📦 ScoresObject
객체는 경기의 점수와 시간 정보를 담고 있습니다.
멤버변수 | 멤버변수 타입 | 역할 |
---|---|---|
red | int | 빨강 팀(레드)의 점수 |
blue | int | 파랑 팀(블루)의 점수 |
time | float | 경과한 시간 (초 단위) |
scoreLimit | int | 점수 제한 |
timeLimit | float | 시간 제한 |
time
매개변수의 시간 값은 게임이 멈춘 상태에서는 변화하지 않습니다.
TeamID
열거형
📦 TeamID
열거형은 정수로 표현한 팀 아이디의 값을 의미합니다.
소스코드의 가독성을 위해 열거형을 사용하며, 혹은 간편하게 정수 값만을 사용해도 됩니다.
팀 이름 | 정수 값 |
---|---|
대기 팀(스펙) | 0 |
레드 팀(빨강) | 1 |
블루 팀(블루) | 2 |
PositionObject
객체
📦 PositionObject
객체는 플레이어 혹은 공의 위치 좌표를 담고 있습니다.
멤버변수 | 멤버변수 타입 | 역할 |
---|---|---|
x | float | X축 좌표 |
y | float | Y축 좌표 |
DiscPropertiesObject
객체
📦 DiscPropertiesObject
객체는 플레이어나 공과 같은 게임 내의 물리 디스크(원반)의 정보를 담고 있습니다.
멤버변수 | 멤버변수 타입 | 역할 |
---|---|---|
x | float | X축 좌표 |
y | float | Y축 좌표 |
xspeed | float | 속력벡터(speed vector)의 X축 좌표 |
yspeed | float | 속력벡터(speed vector)의 Y축 좌표 |
xgravity | float | 중력벡터(gravity vector)의 X축 좌표 |
ygravity | float | 중력벡터(gravity vector)의 Y축 좌표 |
radius | float | 디스크(원반) 반경(반지름) |
bCoeff | float | 반발계수(bouncing coefficient) |
invMass | float | 디스크(원반) 질량의 역수 |
damping | float | 감쇠상수(damping factor) |
color | int | 디스크(원반) 색상 |
cMask | int | 디스크(원반)의 충돌마스크(collision mask) |
cGroup | int | 디스크(원반)의 충돌그룹(collision groups) |
color
매개변수는 0xFF0000
(빨강)처럼 정수 값으로 색상을 지정하며, -1
값을 가지는 경우 투명 속성을 가집니다.
cMask
매개변수는 해당 디스크(원반)이 충돌할 수 있는 그룹을 나타냅니다.
활용하기
Headless Host
를 개발하기 위해서는 프로그래밍 언어
를 사용해 아이디어를 구현해야 합니다.
다양한 기능을 구현할 수 있으며 이 문서에서는 몇 가지의 핵심적인 예시를 설명합니다.
자동으로 방장(어드민) 임명하기
방장(어드민) 권한을 자동으로 부여하여 관리 상의 공백을 막아야 할 수 있습니다.
getPlayerList
메서드를 통해 현재 접속 중인 플레이어 목록을 모두 받아온 후, admin
값을 검사하여 한 명이라도 true
값을 갖지 않는다면 이 때 임명하면 됩니다.
admin
값을 검사하는 시점은 onPlayerJoin
, onPlayerLeave
, onPlayerAdminChange
이벤트 등이 호출되었을 때가 적절합니다.
중복 접속 방지하기
플레이어가 이중으로 접속하여 적절하지 못한 행태를 보이는 것을 막아야 할 수 있습니다.
PlayerObject
객체의 conn
값을 비교하면 이미 접속한 플레이어가 있는지 검사할 수 있습니다.
onPlayerJoin
이벤트 내에서 getPlayerList
메서드를 통해 현재 접속 중인 플레이어 목록을 모두 받아온 후, conn
값을 비교합니다.
같은 conn
값을 가지는 플레이어가 존재한다면, 새로 접속한 플레이어를 추방합니다.
맵 변경 방지하기
방장(어드민) 권한을 가진 플레이어가 적절하지 않은 맵을 불러와 정상적인 게임 진행을 방해하는 것을 막아야 할 수 있습니다.
onStadiumChange
이벤트가 발생할 때마다 다시 맵을 불러와 사실상 맵을 고정시킬 수 있습니다.
onStadiumChange
내에서 setCustomStadium
메서드 혹은 setDefaultStadium
메서드를 통해 원하는 맵을 불러오도록 합니다.
플레이어 정보 확장하기
여러 기능을 지원하기 위해 플레이어 정보를 다양하게 관리해야 할 수 있습니다.
별도의 자료 구조를 활용하여 PlayerObject
객체보다 많은 정보를 담을 수 있습니다.
배열 혹은 Map 객체를 활용하여 접속 시간, 종료 시간, 공격 포인트 등을 기록합니다.
또는 외부의 DB 서버를 활용하도록 합니다.
플레이어 정보 보존하기
플레이어 정보를 유실 없이 보존해야 할 수 있습니다.
HTML5 localStorage API
나 외부의 DB 서버를 활용하여 정보를 보관할 수 있습니다.
휘발성 메모리에 정보를 담은 이후 보존하고자 하는 내용을 선별하여 외부에 기록합니다.
플레이어가 다시 접속하였을 때는 PlayerObject
객체의 auth
값을 비교한 후 해당하는 플레이어의 정보를 불러오도록 합니다.
명령어 구현하기
기능을 확장하기 위해 명령어를 지원해야 할 수 있습니다.
플레이어가 채팅 메시지를 보내면 그것을 처리하여 명령어로 인식할 수 있습니다.
채팅 메시지의 맨 앞에 올 명령어 접두사를 정합니다.
명령어 접두사가 인식되면 해당 명령어를 해석(파싱)하고 기능을 불러오도록 합니다.
마치며
Headless Host
를 구현하며 발생하는 문제는 Haxball GitHub
에 문의하면 됩니다. 여기에서 제보하면 되며 해당 사이트는 전 세계 사람들이 이용하는 곳이므로 의사소통을 위해 반드시 영어
로 내용을 작성하여야 합니다.
또한 Headless Host
의 명세는 예고 없이 변경될 수 있으므로 적절히 변경점을 확인하여야 합니다. Haxball
게임의 News
혹은 Haxball GitHub
를 통해 새 소식을 주시하면 됩니다.
악의적인 네트워크 공격에 대응하기 위해서는 기존에 공개된 다양한 네트워크 보안 자료를 참고하는 것이 좋습니다. 네트워크 보안은 번거롭고 쉽지 않은 일이며 심지어는 자금의 소모를 야기합니다.
지속적으로 Headless Host
를 운영하고자 하는 경우 서버 호스팅
을 이용하는 것이 유용할 수 있습니다. 아마존
, 구글
, 마이크로소프트
등 다양한 기업에서 유료 서비스를 제공하고 있으므로 관련 자료를 참고하면 됩니다.
일부 서비스 제공 업체에서는 무료로 체험할 수 있는 크레딧을 지급하기도 합니다.
주변에 Headless Host
개발을 경험하거나 프로그래밍이 가능한 사람이 있다면 조언을 구할 수 있습니다. 학습 방향과 아이디어에 대한 지도를 부탁할 수 있지만 이것은 당연한 권리가 아닌 호의임을 잊어서는 안됩니다. 직접 구현을 요구하는 것은 그다지 바람직하지 못합니다. 다른 사람에게 부담을 지우기 보다는 스스로 부딪혀보며 문제 해결 능력
을 기르는 것이 장기적으로도 옳은 일입니다.
프로그래밍은 시간과 노력의 싸움입니다. 정진하세요!
참고자료
- HaxBall Headless Host
- HaxBall Connection Issues
- HaxBall Collision Flags
- HaxBall Stadium File
- Haxball Replay File
- Haxbotron Headless Host Server
라이센스
Haxball
게임과 Haxball Headless Host
의 본질적인 내용의 저작권은 원저작자인 마리오 카르바할
(Mario Carbajal; Basro)에게 있습니다.
이 문서의 고유한 내용의 저작권은 dapucita
에게 있으며 사용권은 MIT Licence
에 준합니다.
기타 내용물의 저작권은 해당 원저작자에게 있습니다.