멀티플레이 게임 서버 개발을 빠르게 시작하세요. 향후 Google Agones 기반으로 K8S 운영 및 대규모 신속 확장 전용 게임 서버 관련 글을 업데이트할 예정입니다. ☁️ 네이티브🤗 Cloud-Native를 받아들여 보세요!
WebSocket 서버
서버
Server는 WebSocket 서버를 제공하여 서버와 클라이언트 간의 통신을 구현합니다.
constructor (options)
options.server
WebSocket 서버를 바인딩할 HTTP 서버. 서버에서 express를 사용할 수도 있습니다.
// Colyseus + Express
import { Server } from "colyseus";
import { createServer } from "http";
import express from "express";
const port = Number(process.env.port) || 3000;
const app = express();
app.use(express.json());
const gameServer = new Server({
server: createServer(app)
});
gameServer.listen(port);
// Colyseus (기본)
import { Server } from "colyseus";
const port = process.env.port || 3000;
const gameServer = new Server();
gameServer.listen(port);
options.pingInterval
서버가 클라이언트에게 "ping"을 보내는 밀리초. 기본값: 3000
클라이언트가 pingMaxRetries 재시도 후에도 응답하지 않으면 연결이 강제로 끊어집니다.
options.pingMaxRetries
허용되는 최대 응답 없는 ping 수. 기본값: 2.
options.verifyClient
이 메서드는 WebSocket 핸드셰이크 전에 발생합니다. verifyClient가 설정되지 않으면 핸드셰이크가 자동으로 수락됩니다.
info(Object)origin(String) 클라이언트가 지정한Origin header의 값.req(http.IncomingMessage) 클라이언트의HTTP GET요청.secure(Boolean)req.connection.authorized또는req.connection.encrypted가 설정된 경우true.next(Function) 사용자는info필드를 확인한 후 이 콜백을 호출해야 합니다. 이 콜백의 매개변수는:result(Boolean) 핸드셰이크를 수락할지 여부.code(Number)result가false인 경우, 이 필드는 클라이언트에 전송될 HTTP 오류 상태 코드를 결정합니다.name(String)result가false인 경우, 이 필드는 HTTP 이유 구문을 결정합니다.
import { Server } from "colyseus";
const gameServer = new Server({
// ...
verifyClient: function (info, next) {
// 'info' 유효성 검사
//
// - next(false)는 WebSocket 핸드셰이크를 거부합니다
// - next(true)는 WebSocket 핸드셰이크를 수락합니다
}
});
options.presence
여러 프로세스/머신으로 Colyseus를 확장할 때 상태 서버를 제공해야 합니다.
import { Server, RedisPresence } from "colyseus";
const gameServer = new Server({
// ...
presence: new RedisPresence()
});
현재 사용 가능한 상태 서버는 다음과 같습니다:
RedisPresence(단일 서버 및 다중 서버 확장)
options.gracefullyShutdown
자동으로 shutdown routine을 등록합니다. 기본값은 true입니다. 비활성화된 경우, 종료 프로세스에서 수동으로 gracefullyShutdown() 메서드를 호출해야 합니다.
define (name: string, handler: Room, options?: any)
새로운 room handler를 정의합니다.
매개변수:
name: string-room의 공개 이름. 클라이언트가room에 참여할 때 이 이름을 사용합니다.handler: Room-Roomhandler 클래스에 대한 참조.options?: any-room초기화에 대한 사용자 지정 옵션.
// "chat" 룸 정의
gameServer.define("chat", ChatRoom);
// "battle" 룸 정의
gameServer.define("battle", BattleRoom);
// 사용자 지정 옵션으로 "battle" 룸 정의
gameServer.define("battle_woods", BattleRoom, { map: "woods" });
"동일한 room handler 여러 번 정의":
- 다른
options를 사용하여 동일한room handler를 여러 번 정의할 수 있습니다.Room#onCreate()가 호출될 때options에는Server#define()에서 지정한 병합 값과 룸을 생성할 때 제공된 옵션이 포함됩니다.
매칹메이킹 필터: filterBy(options)
매개변수
options: string[]- 옵션 이름 목록
create() 또는 joinOrCreate() 메서드로 룸이 생성될 때, filterBy() 메서드로 정의된 options만 내부에 저장되며, join() 또는 joinOrCreate() 호출에서 관련 rooms를 필터링하는 데 사용됩니다.
예시: 다른 "게임 모드" 허용.
gameServer
.define("battle", BattleRoom)
.filterBy(['mode']);
루임을 생성할 때마다 mode 옵션이 내부에 저장됩니다.
client.joinOrCreate("battle", { mode: "duo" }).then(room => {/* ... */});
onCreate() 및/또는 onJoin()에서 제공된 옵션을 처리하여 룸 구현에서 요청된 기능을 구현할 수 있습니다.
class BattleRoom extends Room {
onCreate(options) {
if (options.mode === "duo") {
// 무언가를 수행합니다!
}
}
onJoin(client, options) {
if (options.mode === "duo") {
// 이 플레이어를 팀에 넣습니다!
}
}
}
예시: 내장된 maxClients로 필터링
maxClients는 matchmaking을 위한 내부 변수이며 필터링에도 사용할 수 있습니다.
gameServer
.define("battle", BattleRoom)
.filterBy(['maxClients']);
그런 다음 클라이언트는 특정 수의 플레이어를 수용할 수 있는 루임에 참여하도록 요청할 수 있습니다.
client.joinOrCreate("battle", { maxClients: 10 }).then(room => {/* ... */});
client.joinOrCreate("battle", { maxClients: 20 }).then(room => {/* ... */});
매칹메이킹 우선순위: sortBy(options)
또한 생성 시 룸에 참여하는 정보에 따라 룸 참여에 다른 우선순위를 부여할 수 있습니다.
options 매개변수는 왼쪽에 필드 이름, 오른쪽에 정렬 방향이 있는 키-값 객체입니다. 정렬 방향은 -1, "desc", "descending", 1, "asc" 또는 "ascending" 중 하나일 수 있습니다.
예시: 내장된 clients로 정렬
clients는 현재 연결된 클라이언트 수를 포함하는 matchmaking을 위해 저장되는 내부 변수입니다. 다음 예시에서 가장 많은 클라이언트가 연결된 룸이 우선권을 갖습니다. -1, "desc" 또는 "descending"을 사용하여 내림차순으로 정렬합니다:
gameServer
.define("battle", BattleRoom)
.sortBy({ clients: -1 });
플레이어 수가 가장 적은 순으로 정렬하려면 반대로 수행할 수 있습니다. 오름차순으로 1, "asc" 또는 "ascending"을 사용합니다:
gameServer
.define("battle", BattleRoom)
.sortBy({ clients: 1 });
로비의 실시간 룸 목록 활성화
LobbyRoom이 특정 룸 유형의 업데이트를 수신하도록 허용하려면 실시간 목록을 활성화하여 정의해야 합니다:
gameServer
.define("battle", BattleRoom)
.enableRealtimeListing();
룸 인스턴스 이벤트 수신
define 메서드는 등록된 handler 인스턴스를 반환하며, 룸 인스턴스 범위 외부에서 match-making 이벤트를 수신할 수 있습니다. 다음과 같은 이벤트가 있습니다:
"create"- 룸이 생성될 때"dispose"- 루이가 삭제될 때"join"- 클라이언트가 룸에 참여할 때"leave"- 클라이언트가 룸을 떠날 때"lock"- 룸이 잠겨 있을 때"unlock"- 룸이 잠금 해제되었을 때
사용법:
gameServer
.define("chat", ChatRoom)
.on("create", (room) => console.log("room created:", room.roomId))
.on("dispose", (room) => console.log("room disposed:", room.roomId))
.on("join", (room, client) => console.log(client.id, "joined", room.roomId))
.on("leave", (room, client) => console.log(client.id, "left", room.roomId));
이러한 이벤트를 통해 룸의 state를 조작하는 것은 권장되지 않습니다. 대신 abstract methods를 룸 핸들러에서 사용하세요.
simulateLatency (milliseconds: number)
이는 로컬 테스트에서 서버를 원격 클라우드에 배포하지 않고도 "laggy(지연된)" 클라이언트의 동작을 테스트하고 싶을 때 사용하는 편리한 메서드입니다.
// 프로덕션 환경에서는 절대 `simulateLatency()` 메서드를 호출하지 마세요.
if (process.env.NODE_ENV !== "production") {
// 서버와 클라이언트 간에 200ms 지연 시뮬레이션.
gameServer.simulateLatency(200);
}
attach (options: any)
일반적으로 호출할 필요가 없습니다. 매우 명확한 이유가 있는 경우에만 사용하세요.
WebSocket 서버에 연결하거나 생성합니다.
options.server: WebSocket 서버를 바인딩할 HTTP 서버.options.ws: 기존의 재사용 가능한 WebSocket 서버.
Express
import express from "express";
import { Server } from "colyseus";
const app = new express();
const gameServer = new Server();
gameServer.attach({ server: app });
http.createServer
import http from "http";
import { Server } from "colyseus";
const httpServer = http.createServer();
const gameServer = new Server();
gameServer.attach({ server: httpServer });
WebSocket.Server
import http from "http";
import express from "express";
import ws from "ws";
import { Server } from "colyseus";
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({
// 사용자 지정 WebSocket.Server 설정.
});
const gameServer = new Server();
gameServer.attach({ ws: wss });
listen (port: number)
WebSocket 서버를 지정된 포트에 바인딩합니다.
onShutdown (callback: Function)
프로세스가 종료되기 전에 호출될 콜백를 등록합니다. graceful shutdown 참조.
gracefullyShutdown (exit: boolean)
모든 룸을 닫고 캐시 데이터를 정리합니다. 정리가 완료되면 promise를 반환합니다.
Server 생성자에서 gracefullyShutdown: false가 제공되지 않은 경우 이 메서드가 자동으로 호출됩니다.
룸 API (서버 측)
서버를 설정했다면, 이제 room handlers를 등록하고 사용자 연결을 수락할 시간입니다.
Room에서 확장되는 클래스를 생성하여 room handlers를 정의할 것입니다.
import http from "http";
import { Room, Client } from "colyseus";
export class MyRoom extends Room {
// 룸이 초기화될 때
onCreate (options: any) { }
// WebSocket 핸드셰이크가 완료되기 전에 제공된 옵션을 기반으로 클라이언트를 인증
onAuth (client: Client, options: any, request: http.IncomingMessage) { }
// 클라이언트가 룸에 성공적으로 참여했을 때
onJoin (client: Client, options: any, auth: any) { }
// 클라이언트가 룸을 떠날 때
onLeave (client: Client, consented: boolean) { }
// 더 이상 클라이언트가 없을 때 호출되는 정리 콜백. (`autoDispose` 참조)
onDispose () { }
}
룸 라이프사이클
이러한 메서드는 룸의 라이프사이클에 해당합니다.
onCreate (options)
루이가 초기화된 후 한 번 호출됩니다. 룸 처리 프로그램을 등록할 때 사용자 지정 초기화 옵션을 지정할 수 있습니다.
options에는 Server#define()에 지정한 병합 값과 client.joinOrCreate() 또는 client.create()에서 제공된 옵션이 포함됩니다.
onAuth (client, options, request)
onAuth() 메서드는 onJoin() 전에 실행됩니다. 룸에 참여하는 클라이언트의 인증에 사용할 수 있습니다.
onAuth()가 참 값을 반환하면onJoin()이 호출되고 반환 값이 세 번째 매개변수로 전달됩니다.onAuth()가 거짓 값을 반환하면 클라이언트가 즉시 거부되어 클라이언트matchmaking함수 호출이 실패합니다.- 또한
ServerError를 발생시켜 클라이언트에서 처리할 사용자 지정 오류를 공개할 수 있습니다.
구현하지 않으면 항상 true를 반환합니다 - 모든 클라이언트 연결을 허용합니다.
"플레이어 IP 주소 가져오기": request 변수를 사용하여 사용자의 IP 주소, http 헤더 등을 검색할 수 있습니다. 예를 들어: request.headers['x-forwarded-for'] || request.connection.remoteAddress
구현 예시
async / await
import { Room, ServerError } from "colyseus";
class MyRoom extends Room {
async onAuth (client, options, request) {
/**
* 대안으로 `async` / `await`를 사용할 수 있습니다,
* 이는 내부적으로 `Promise`를 반환합니다.
*/
const userData = await validateToken(options.accessToken);
if (userData) {
return userData;
} else {
throw new ServerError(400, "bad access token");
}
}
}
동기식
import { Room } from "colyseus";
class MyRoom extends Room {
onAuth (client, options, request): boolean {
/**
* 즉시 `boolean` 값을 반환할 수 있습니다.
*/
if (options.password === "secret") {
return true;
} else {
throw new ServerError(400, "bad access token");
}
}
}
프로미스
import { Room } from "colyseus";
class MyRoom extends Room {
onAuth (client, options, request): Promise<any> {
/**
* `Promise`를 반환하고 클라이언트를 유효성 검증하기 위해 비동기 작업을 수행할 수 있습니다.
*/
return new Promise((resolve, reject) => {
validateToken(options.accessToken, (err, userData) => {
if (!err) {
resolve(userData);
} else {
reject(new ServerError(400, "bad access token"));
}
});
});
}
}
클라이언트 예시
클라이언트에서는 Facebook과 같은 인증 서비스에서 token을 사용하여 matchmaking 메서드(join, joinOrCreate 등)를 호출할 수 있습니다:
client.joinOrCreate("world", {
accessToken: yourFacebookAccessToken
}).then((room) => {
// 성공
}).catch((err) => {
// 오류 처리...
err.code // 400
err.message // "bad access token"
});
onJoin (client, options, auth?)
매개변수:
client:클라이언트인스턴스.options:Server#define()에 지정한 값과client.join()에서 제공된 옵션을 병합한 값.auth: (선택 사항)auth데이터는onAuth메서드에서 반환됩니다.
클라이언트가 룸에 성공적으로 참여했을 때, requestJoin 및 onAuth가 성공한 후 호출됩니다.
onLeave (client, consented)
클라이언트가 룸을 떠날 때 호출됩니다. 연결이 클라이언트에서 시작된 경우 consented 매개변수는 true이고, 그렇지 않으면 false입니다.
이 함수를 async로 정의할 수 있습니다. graceful shutdown 참조.
동기식
onLeave(client, consented) {
if (this.state.players.has(client.sessionId)) {
this.state.players.delete(client.sessionId);
}
}
비동기식
async onLeave(client, consented) {
const player = this.state.players.get(client.sessionId);
await persistUserOnDatabase(player);
}
onDispose ()
다음과 같은 경우 룸이 삭제되기 전에 onDispose() 메서드가 호출됩니다:
- 룸에 더 이상 클라이언트가 없고
autoDispose가true(기본값)로 설정된 경우 - 수동으로
.disconnect()를 호출한 경우
async onDispose() 비동기 메서드를 정의하여 데이터베이스에 일부 데이터를 지속화할 수 있습니다. 실제로는 경기 후 플레이어 데이터를 데이터베이스에 유지하기에 좋은 장소입니다.
예시 룸
이 예시는 onCreate, onJoin 및 onMessage 메서드를 구현하는 room을 보여줍니다.
import { Room, Client } from "colyseus";
import { Schema, MapSchema, type } from "@colyseus/schema";
// 잠재적인 2D 월드 위치를 보여주는 추상적인 플레이어 객체
export class Player extends Schema {
@type("number")
x: number = 0.11;
@type("number")
y: number = 2.22;
}
// 사용자 지정 게임 상태, 현재는 Player 유형의 ArraySchema만
export class State extends Schema {
@type({ map: Player })
players = new MapSchema<Player>();
}
export class GameRoom extends Room<State> {
// Colyseus는 룸 인스턴스를 생성할 때 호출됩니다
onCreate(options: any) {
// 빈 룸 상태 초기화
this.setState(new State());
// 이 룸이 "move" 메시지를 받을 때마다 호출됩니다
this.onMessage("move", (client, data) => {
const player = this.state.players.get(client.sessionId);
player.x += data.x;
player.y += data.y;
console.log(client.sessionId + " at, x: " + player.x, "y: " + player.y);
});
}
// 클라이언트가 참여할 때마다 호출됩니다
onJoin(client: Client, options: any) {
this.state.players.set(client.sessionId, new Player());
}
}
공개 메서드
Room handlers에는 다음 메서드를 사용할 수 있습니다.
onMessage (type, callback)
클라이언트가 보낸 메시지 유형을 처리하기 위한 콜백을 등록합니다.
type 매개변수는 string 또는 number일 수 있습니다.
특정 메시지 유형에 대한 콜백
onCreate () {
this.onMessage("action", (client, message) => {
console.log(client.sessionId, "'action' 메시지를 보냄: ", message);
});
}
모든 메시지에 대한 콜백
다른 모든 유형의 메시지를 처리하기 위해 콜백을 등록할 수 있습니다.
onCreate () {
this.onMessage("action", (client, message) => {
//
// 'action' 메시지가 전송될 때 트리거됩니다.
//
});
this.onMessage("*", (client, type, message) => {
//
// 다른 유형의 메시지가 전송될 때 트리거됩니다,
// 위에 정의된 'action'의 특정 핸들러는 제외합니다.
//
console.log(client.sessionId, "sent", type, message);
});
}
setState (object)
새로운 room state 인스턴스를 설정합니다. state object에 대한 자세한 내용은 State Handling을 참조하세요. state를 처리하기 위해 새로운 Schema Serializer를 사용하는 것이 강력히 권장됩니다.
room state 업데이트를 위해 이 메서드를 호출하지 마세요. 이진 패치 알고리즘(binary patch algorithm)이 호출될 때마다 다시 설정됩니다.
일반적으로 room handler의 onCreate() 기간 동안 이 메서드를 한 번만 호출합니다.
setSimulationInterval (callback[, milliseconds=16.6])
(선택 사항) 게임 상태를 변경할 수 있는 시뮬레이션 간격을 설정합니다. 시댜레이션 간격은 게임 루프입니다. 기본 시뮬레이션 간격: 16.6ms (60fps)
onCreate () {
this.setSimulationInterval((deltaTime) => this.update(deltaTime));
}
update (deltaTime) {
// 여기에 물리 또는 월드 업데이트를 구현하세요!
// 룸 상태를 업데이트하는 좋은 장소입니다
}
setPatchRate (milliseconds)
모든 클라이언트에게 패치 상태를 보내는 빈도를 설정합니다. 기본값은 50ms (20fps)
setPrivate (bool)
방 목록을 비공개로 설정합니다(false를 제공하면 공개로 복원됩니다).
Private rooms는 getAvailableRooms() 메서드에 나열되지 않습니다.
setMetadata (metadata)
이 루에 메타데이터를 설정합니다. 각 룸 인스턴스에 메타데이터를 첨부할 수 있습니다 - 메타데이터를 첨부하는 유일한 목적은 client.getAvailableRooms()를 사용하여 사용 가능한 룸 목록을 가져올 때 룸을 다른 룸과 구분하기 위함입니다. roomId로 연결합니다.
// 서버 측
this.setMetadata({ friendlyFire: true });
이제 룸에 첨부된 메타데이터가 있습니다. 예를 들어, 클라이언트는 friendlyFire가 있는 룸을 확인하고 roomId로 직접 연결할 수 있습니다:
// 클라이언트 측
client.getAvailableRooms("battle").then(rooms => {
for (var i=0; i<rooms.length; i++) {
if (room.metadata && room.metadata.friendlyFire) {
//
// 'friendlyFire'가 있는 루에 `roomId`로 참여합니다:
//
var room = client.join(room.roomId);
return;
}
}
});
setSeatReservationTime (seconds)
클라이언트가 룸에 유효하게 참여할 수 있도록 기다릴 수 있는 시간(초)을 설정합니다. onAuth()가 얼마나 오랫동안 기다려야 하는지 고려하여 다른 좌석 예약 시간을 설정해야 합니다. 기본값은 15초입니다.
전역적으로 좌석 예약 시간을 변경하려면 COLYSEUS_SEAT_RESERVATION_TIME 환경 변수를 설정할 수 있습니다.
send (client, message)
this.send()는 더 이상 사용되지 않습니다. 대신 client.send()를 사용하세요.
broadcast (type, message, options?)
연결된 모든 클라이언트에게 메시지를 보냅니다.
사용 가능한 옵션은 다음과 같습니다:
except: 메시지를 보내지 않을Client인스턴스afterNextPatch: 다음 패치 후 메시지를 브로드캐스트
브로드캐스트 예시
모든 클라이언트에게 메시지를 브로드캐스트합니다:
onCreate() {
this.onMessage("action", (client, message) => {
// 모든 클라이언트에게 메시지를 브로드캐스트합니다
this.broadcast("action-taken", "an action has been taken!");
});
}
발신자를 제외한 모든 클라이언트에게 메시지를 브로드캐스트합니다.
onCreate() {
this.onMessage("fire", (client, message) => {
// "fire" 이벤트를 트리거한 클라이언트를 제외한 모든 클라이언트에게 "fire" 이벤트를 보냅니다.
this.broadcast("fire", message, { except: client });
});
}
state가 변경된 후에만 모든 클라이언트에게 메시지를 브로드캐스트합니다:
onCreate() {
this.onMessage("destroy", (client, message) => {
// 상태에서 변경 사항을 수행합니다!
this.state.destroySomething();
// 이 메시지는 새 상태가 적용된 후에만 도착합니다
this.broadcast("destroy", "something has been destroyed", { afterNextPatch: true });
});
}
schema-encoded 메시지를 브로드캐스트합니다:
class MyMessage extends Schema {
@type("string") message: string;
}
// ...
onCreate() {
this.onMessage("action", (client, message) => {
const data = new MyMessage();
data.message = "an action has been taken!";
this.broadcast(data);
});
}
lock ()
룸을 잠그면 새 클라이언트가 연결할 수 있는 사용 가능한 룸 풀에서 제거됩니다.
unlock ()
룸의 잠금을 해제하면 새 클라이언트가 연결할 수 있도록 사용 가능한 룸 풀로 반환됩니다.
allowReconnection (client, seconds?)
지정된 클라이언트가 룸에 다시 연결하도록 허용합니다. onLeave() 메서드에서 사용해야 합니다.
**seconds**를 제공하면 제공된 시간 후에 재연결이 취소됩니다.
async onLeave (client: Client, consented: boolean) {
// 다른 사용자에게 클라이언트를 비활성화로 표시
this.state.players[client.sessionId].connected = false;
try {
if (consented) {
throw new Error("consented leave");
}
// 연결이 끊긴 클라이언트가 20초 동안 이 룸에 다시 연결하도록 허용
await this.allowReconnection(client, 20);
// 클라이언트가 돌아왔습니다! 다시 활성화합시다.
this.state.players[client.sessionId].connected = true;
} catch (e) {
// 20초가 만료되었습니다. 클라이언트를 제거합시다.
delete this.state.players[client.sessionId];
}
}
또는, **seconds**의 수를 제공하지 않고 자체 로직을 사용하여 재연결을 거부할 수 있습니다.
async onLeave (client: Client, consented: boolean) {
// 다른 사용자에게 클라이언트를 비활성화로 표시
this.state.players[client.sessionId].connected = false;
try {
if (consented) {
throw new Error("consented leave");
}
// 재연결 토큰 가져오기
const reconnection = this.allowReconnection(client);
//
// 여기서 재연결을 거부하는 사용자 지정 로직이 있습니다.
// API 데모를 위해, 플레이어가 2라운드를 놓친 경우 재연결을 거부하는 간격이 생성됩니다,
// (턴 기반 게임을 한다고 가정)
//
// 실제 시나리오에서는 `reconnection`을 Player 인스턴스에 저장하고,
// 예를 들어 게임 루프 로직 동안 이 검사를 수행합니다
//
const currentRound = this.state.currentRound;
const interval = setInterval(() => {
if ((this.state.currentRound - currentRound) > 2) {
// 수동으로 클라이언트 재연결 거부
reconnection.reject();
clearInterval(interval);
}
}, 1000);
// 연결이 끊긴 클라이언트가 다시 연결하도록 허용
await reconnection;
// 클라이언트가 돌아왔습니다! 다시 활성화합시다.
this.state.players[client.sessionId].connected = true;
} catch (e) {
// 20초가 만료되었습니다. 클라이언트를 제거합시다.
delete this.state.players[client.sessionId];
}
}
disconnect ()
모든 클라이언트의 연결을 끊은 후 삭제합니다.
broadcastPatch ()
"이것은 필요하지 않을 수 있습니다!" 이 메서드는 프레임워크에 의해 자동으로 호출됩니다.
이 메서드는 state에서 변이가 발생했는지 확인하고 모든 연결된 클라이언트에게 브로드캐스트합니다.
패치를 브로드캐스트하는 시기를 제어하려면 기본 패치 간격을 비활성화할 수 있습니다:
onCreate() {
// 자동 패치 비활성화
this.setPatchRate(null);
// 시계 타이머가 활성화되어 있는지 확인
this.setSimulationInterval(() => {/* */});
this.clock.setInterval(() => {
// 사용자 지정 조건이 충족되는 경우에만 패치를 브로드캐스트합니다.
if (yourCondition) {
this.broadcastPatch();
}
}, 2000);
}
공개 속성
roomId: string
고유하게 생성된 9자리 길이의 room id.
onCreate() 기간 동안 this.roomId를 대체할 수 있습니다. roomId가 고유한지 확인해야 합니다.
roomName: string
gameServer.define()의 첫 번째 매개변수로 제공한 room의 이름입니다.
state: T
setState()에 제공된 state 인스턴스
clients: Client[]
연결된 클라이언트 array. Web-Socket Client 참조.
maxClients: number
룸에 연결할 수 있는 최대 클라이언트 수. 룸이 이 한계에 도달하면 자동으로 잠깁니다. lock() 메서드를 명시적으로 잠그지 않은 한 클라이언트가 연결을 끊으면 룸이 잠금 해제됩니다.
patchRate: number
연결된 클라이언트에게 룸 상태를 보내는 빈도(밀리초). 기본값은 50ms (20fps)
autoDispose: boolean
마지막 클라이언트가 연결을 끊을 때 룸을 자동으로 삭제합니다. 기본값은 true
locked: boolean (읽기 전용)
다음 경우에 이 속성이 변경됩니다:
- 허용된 최대 클라이언트 수에 도달했습니다(
maxClients) lock()또는unlock()메서드를 사용하여 룸을 수동으로 잠그거나 잠금 해제했습니다
clock: ClockTimer
timing events를 위한 ClockTimer 인스턴스.
presence: Presence
presence 인스턴스. 자세한 내용은 Presence API를 참조하세요.
WebSocket 클라이언트
client 인스턴스는 다음에 존재합니다:
Room#clientsRoom#onJoin()Room#onLeave()Room#onMessage()
이것은 ws 패키지의 원본 WebSocket 연결입니다. 더 많은 메서드를 사용할 수 있지만 Colyseus와 함께 사용하지 않는 것이 좋습니다.
속성
sessionId: string
각 세션에 고유한 ID.
클라이언트에서는 room 인스턴스에서 sessionId를 찾을 수 있습니다.
auth: any
onAuth() 기간 동안 반환된 사용자 지정 데이터.
메서드
send(type, message)
클라이언트에게 message 유형의 메시지를 보냅니다. 메시지는 MsgPack으로 인코딩되며 모든 JSON-seriazeable 데이터 구조를 저장할 수 있습니다.
type은 string 또는 number일 수 있습니다.
메시지 보내기:
//
// 문자열 유형("powerup")으로 메시지 보내기
//
client.send("powerup", { kind: "ammo" });
//
// 숫자 유형(1)으로 메시지 보내기
//
client.send(1, { kind: "ammo"});
leave(code?: number)
클라이언트와 room 간의 연결을 강제로 끊습니다.
이것은 클라이언트에서 room.onLeave 이벤트를 트리거합니다.
error(code, message)
code와 message가 포함된 error를 클라이언트에게 보냅니다. 클라이언트는 onError에서 이를 처리할 수 있습니다.
timing events의 경우, Room 인스턴스에서 this.clock 메서드를 사용하는 것이 좋습니다.
모든 간격과 시간 초과는 this.clock에 등록됩니다.
Room이 정리될 때 자동으로 지워집니다.
내장된 setTimeout 및
setInterval 메서드는 CPU 로드에 의존하므로 예상치 못한 실행 시간으로 지연될 수 있습니다.
시계(Clock)
clock는 상태 시뮬레이션 외부의 이벤트를 위한 유용한 메커니즘입니다. 예를 들어, 플레이어가 아이템을 수집할 때 시간을 측정할 수 있습니다. clock.setTimeout을 사용하여 새로 수집 가능한 객체를 생성할 수 있습니다. clock.를 사용하는 장점은 room 업데이트와 증분에 대해 걱정할 필요가 없으며, 룸 상태와 독립적으로 이벤트 타이밍에 집중할 수 있다는 것입니다.
공개 메서드
참고: time 매개변수의 단위는 밀리초입니다
clock.setInterval(callback, time, ...args): Delayed
setInterval() 메서드는 고정된 지연 시간 사이에 함수를 반복적으로 호출하거나 코드 조각을 실행합니다.
이것은 간격을 식별하는 Delayed 인스턴스를 반환하므로 나중에 조작할 수 있습니다.
clock.setTimeout(callback, time, ...args): Delayed
setTimeout() 메서드는 지정된 시간이 지나면 함수나 코드 조각을 실행하는 timer를 설정합니다. 이것은 간격을 식별하는 Delayed 인스턴스를 반환하므로 나중에 조작할 수 있습니다.
예시
이 MVP 예시는 Room: setInterval(), setTimeout 및 이전에 저장된 Delayed 유형의 인스턴스를 지우는 것을 보여줍니다; 그리고 Room's clock 인스턴스에서 currentTime을 표시합니다. 1초 후 'Time now ' + this.clock.currentTime가 console.log된 후, 10초 후 간격을 지웁니다: this.delayedInterval.clear();.
// Delayed 가져오기
import { Room, Client, Delayed } from "colyseus";
export class MyRoom extends Room {
// 이 예시를 위해
public delayedInterval!: Delayed;
// 룸이 초기화될 때
onCreate(options: any) {
// 시계 시작
this.clock.start();
// 간격을 설정하고 나중에 지우기 위해 참조를 저장합니다
this.delayedInterval = this.clock.setInterval(() => {
console.log("Time now " + this.clock.currentTime);
}, 1000);
// 10초 후 간격을 지웁니다.
// 이것은 완전히 *중지 및 파괴*됩니다
this.clock.setTimeout(() => {
this.delayedInterval.clear();
}, 10_000);
}
}
clock.clear()
clock.setInterval() 및 clock.setTimeout()에 등록된 모든 간격과 시간 초과를 지웁니다.
clock.start()
타이머를 시작합니다.
clock.stop()
타이머를 중지합니다.
clock.tick()
각 시뮬레이션 간격 단계에서 자동으로 이 메서드가 호출됩니다. tick 기간 동안 모든 Delayed 인스턴스를 확인합니다.
Room#setSimiulationInterval()에 대한 자세한 내용은 참조하세요.
공개 속성
clock.elapsedTime
clock.start() 메서드가 호출된 후 경과된 시간(밀리초). 읽기 전용.
clock.currentTime
현재 시간(밀리초). 읽기 전용.
clock.deltaTime
이전 및 현재 clock.tick() 호출 간의 밀리초 차이. 읽기 전용.
Delayed
clock.setInterval() 또는 clock.setTimeout()로 지연된 인스턴스 생성
공개 메서드
delayed.pause()
특정 Delayed 인스턴스의 시간을 일시 중지합니다. (.resume()이 호출될 때까지 elapsedTime은 증가하지 않습니다.)
delayed.resume()
특정 Delayed 인스턴스의 시간을 다시 시작합니다. (elapsedTime은 계속 정상적으로 증가합니다)
delayed.clear()
시간 초과 또는 간격을 지웁니다.
delayed.reset()
경과 시간(elapsed time)을 재설정합니다.
공개 속성
delayed.elapsedTime: number
Delayed 인스턴스의 실행 시간(밀리초).
delayed.active: boolean
timer가 여전히 실행 중인 경우 true를 반환합니다.
delayed.paused: boolean
타이머가 .pause()로 일시 중지된 경우 true를 반환합니다.
매칭메이커 API
"이것은 필요하지 않을 수 있습니다!"
이 섹션은 고급 용도입니다. 일반적으로 client-side methods를 사용하는 것이 더 좋습니다. 클라이언트 방법으로 목표를 달성할 수 없다고 생각하면 이 페이지에 설명된 방법을 고려해야 합니다.
아래에 설명된 메서드는 matchMaker 싱글톤에서 제공되며, "colyseus" 패키지에서 가져올 수 있습니다:
import { matchMaker } from "colyseus";
const matchMaker = require("colyseus").matchMaker;
.createRoom(roomName, options)
새로운 룸을 생성합니다
매개변수:
roomName:gameServer.define()에 정의된 식별자.options:onCreate의 옵션.
const room = await matchMaker.createRoom("battle", { mode: "duo" });
console.log(room);
/*
{ "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
*/
.joinOrCreate(roomName, options)
룸에 참여하거나 생성하고 클라이언트 좌석 예약을 반환합니다.
매개변수:
roomName:gameServer.define()에 정의된 식별자.options: 클라이언트 좌석 예약의 옵션(예:onJoin/onAuth).
const reservation = await matchMaker.joinOrCreate("battle", { mode: "duo" });
console.log(reservation);
/*
{
"sessionId": "zzzzzzzzz",
"room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
}
*/
"좌석 예약 소비": consumeSeatReservation()을 사용하여 클라이언트에서 예약된 좌석을 사용하여 룸에 참여할 수 있습니다.
.reserveSeatFor(room, options)
클라이언트(client)를 위한 룸(room)에 좌석을 예약합니다.
"좌석 예약 소비": consumeSeatReservation()을 사용하여 클라이언트에서 예약된 좌석을 사용하여 룸에 참여할 수 있습니다.
매개변수:
room: 룸 데이터(createRoom()등에서 결과)options:onCreate옵션
const reservation = await matchMaker.reserveSeatFor("battle", { mode: "duo" });
console.log(reservation);
/*
{
"sessionId": "zzzzzzzzz",
"room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
}
*/
.join(roomName, options)
룸에 참여하고 좌석 예약을 반환합니다. roomName에 사용 가능한 룸이 없으면 예외를 발생시킵니다.
매개변수:
roomName:gameServer.define()에 정의된 식별자.options: 클라이언트 좌석 예약의 옵션(예:onJoin/onAuth)
const reservation = await matchMaker.join("battle", { mode: "duo" });
console.log(reservation);
/*
{
"sessionId": "zzzzzzzzz",
"room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
}
*/
"좌석 예약 소비": consumeSeatReservation()을 사용하여 클라이언트에서 예약된 좌석을 사용하여 룸에 참여할 수 있습니다.
.joinById(roomId, options)
id로 룸에 참여하고 클라이언트 좌석 예약을 반환합니다. roomId에 대한 room을 찾을 수 없으면 예외가 발생합니다.
매개변수:
roomId: 특정room인스턴스의ID.options: 클라이언트 좌석 예약의 옵션(예:onJoin/onAuth)
const reservation = await matchMaker.joinById("xxxxxxxxx", {});
console.log(reservation);
/*
{
"sessionId": "zzzzzzzzz",
"room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
}
*/
"좌석 예약 소비": consumeSeatReservation()을 사용하여 클라이언트에서 예약된 좌석을 사용하여 룸에 참여할 수 있습니다.
.create(roomName, options)
새로운 룸을 생성하고 클라이언트 좌석 예약을 반환합니다.
매개변수:
roomName:gameServer.define()에 정의된 식별자.options: 클라이언트 좌석 예약의 옵션(예:onJoin/onAuth)
const reservation = await matchMaker.create("battle", { mode: "duo" });
console.log(reservation);
/*
{
"sessionId": "zzzzzzzzz",
"room": { "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
}
*/
"좌석 예약 소비": consumeSeatReservation()을 사용하여 클라이언트에서 예약된 좌석을 사용하여 룸에 참여할 수 있습니다.
.query(conditions)
캐시된 룸에 대한 쿼리를 실행합니다.
const rooms = await matchMaker.query({ name: "battle", mode: "duo" });
console.log(rooms);
/*
[
{ "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false },
{ "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false },
{ "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
]
*/
.findOneRoomAvailable(roomName, options)
사용 가능한 공개적이고 잠기지 않은 룸을 찾습니다.
매개변수:
roomId: 특정room인스턴스의ID.options: 클라이언트 좌석 예약의 옵션(예:onJoin/onAuth)
const room = await matchMaker.findOneRoomAvailable("battle", { mode: "duo" });
console.log(room);
/*
{ "roomId": "xxxxxxxxx", "processId": "yyyyyyyyy", "name": "battle", "locked": false }
*/
.remoteRoomCall(roomId, method, args)
원격 room에서 메서드를 호출하거나 속성을 반환합니다.
매개변수:
roomId: 특정room인스턴스의ID.method: 호출하거나 검색할 메서드 또는 속성.args: 매개변수 배열.
// id로 원격 룸에서 lock() 호출
await matchMaker.remoteRoomCall("xxxxxxxxx", "lock");
Presence
여러 프로세스 및/또는 머신에서 서버를 확장할 때 Server에 Presence 옵션을 제공해야 합니다. Presence의 목적은 특히 매칭(match-making) 과정에서 다른 프로세스 간에 통신하고 데이터를 공유하는 것입니다.
LocalPresence(기본값)RedisPresence
각 Room 처리 프로그램에서도 presence 인스턴스를 사용할 수 있습니다. 해당 API를 사용하여 데이터를 지속화하고 PUB/SUB를 통해 룸 간에 통신할 수 있습니다.
LocalPresence
이것은 기본 옵션입니다. 단일 프로세스에서 Colyseus를 실행할 때 사용됩니다.
RedisPresence (clientOpts?)
여러 프로세스 및/또는 머신에서 Colyseus를 실행할 때 이 옵션을 사용하세요.
매개변수:
clientOpts: Redis 클라이언트 옵션(호스트/자격 증명). 전체 옵션 목록은 참조하세요.
import { Server, RedisPresence } from "colyseus";
// 이것은 slave 프로세스에서 발생합니다.
const gameServer = new Server({
// ...
presence: new RedisPresence()
});
gameServer.listen(2567);
const colyseus = require('colyseus');
// 이것은 slave 프로세스에서 발생합니다.
const gameServer = new colyseus.Server({
// ...
presence: new colyseus.RedisPresence()
});
gameServer.listen(2567);
API
Presence API는 Redis API를 기반으로 하며, 이것은 키-값 데이터베이스입니다.
각 Room 인스턴스에는 다음 메서드를 구현하는 presence 속성이 있습니다:
subscribe(topic: string, callback: Function)
주어진 topic을 구독합니다. topic에 메시지가 게시될 때마다 callback이 트리거됩니다.
unsubscribe(topic: string)
주어진 topic의 구독을 취소합니다.
publish(topic: string, data: any)
메시지를 주어진 topic에 게시합니다.
exists(key: string): Promise<boolean>
key가 존재하는지 여부를 불리언 값으로 반환합니다.
setex(key: string, value: string, seconds: number)
key를 string 값으로 보유하도록 설정하고, key가 주어진 초 후에 만료되도록 설정합니다.
get(key: string)
key의 값을 가져옵니다.
del(key: string): void
지정된 key를 삭제합니다.
sadd(key: string, value: any)
key에 저장된 set에 지정된 멤버를 추가합니다. 이미 해당 set의 멤버인 지정 멤버는 무시됩니다. key가 존재하지 않으면 지정된 멤버를 추가하기 전에 새로운 set을 생성합니다.
smembers(key: string)
key에 저장된 set 값의 모든 멤버를 반환합니다.
sismember(member: string)
member가 key에 저장된 set의 멤버인 경우 반환
반환 값
1요소가set의 요소인 경우.0요소가set의 멤버가 아니거나key가 존재하지 않는 경우.
srem(key: string, value: any)
key에 저장된 set에서 지정된 멤버를 삭제합니다. 해당 set의 멤버가 아닌 지정 멤버는 무시됩니다. key가 존재하지 않으면 빈 set으로 간주하며 이 명령은 0을 반환합니다.
scard(key: string)
key에 저장된 set의 set 기수(요소 수)를 반환합니다.
sinter(...keys: string[])
모든 주어진 set의 교집합으로 얻은 set 멤버를 반환합니다.
hset(key: string, field: string, value: string)
key에 저장된 hash의 필드를 value로 설정합니다. key가 존재하지 않으면 hash를 포함하는 새로운 key를 생성합니다. 필드가 이미 hash에 존재하면 해당 필드가 덮어쓰기됩니다.
hincrby(key: string, field: string, value: number)
증분 방식으로 key에 저장된 hash의 필드에 저장된 숫자를 증가시킵니다. key가 존재하지 않으면 hash를 포함하는 새로운 key를 생성합니다. 필드가 존재하지 않으면 작업을 수행하기 전에 값을 0으로 설정합니다.
hget(key: string, field: string): Promise<string>
key에 저장된 hash의 field와 연관된 값을 반환합니다.
hgetall(key: string): Promise<{[field: string]: string}>
key에 저장된 hash의 모든 필드와 값을 반환합니다.
hdel(key: string, field: string)
key에 저장된 hash에서 지정된 필드를 삭제합니다. hash에 존재하지 않는 지정된 필드는 무시됩니다. key가 존재하지 않으면 빈 hash로 간주하며 이 명령은 0을 반환합니다.
hlen(key: string): Promise<number>
key에 저장된 hash에 포함된 필드 수를 반환합니다.
incr(key: string)
key 값에 저장된 숫자를 1 증가시킵니다. key가 존재하지 않으면 0으로 설정한 후 작업을 수행합니다. key에 잘못된 유형의 값이 포함되거나 정수로 표현할 수 없는 문자열이 포함된 경우 오류를 반환합니다. 이 작업은 64비트 부호 정수로 제한됩니다.
decr(key: string)
key에 저장된 숫자를 1 감소시킵니다. key가 존재하지 않으면 0으로 설정한 후 작업을 수행합니다. key에 잘못된 유형의 값이 포함되거나 정수로 표현할 수 없는 문자열이 포함된 경우 오류를 반환합니다. 이 작업은 64비트 부호 정수로 제한됩니다.
Graceful Shutdown
Colyseus는 기본적으로 우아한 종료 메커니즘을 제공합니다. 다음 작업은 프로세스가 자신을 종료하기 전에 수행됩니다:
- 모든 연결된 클라이언트를 비동기적으로 끊습니다 (
Room#onLeave) - 모든 생성된 룸을 비동기적으로 삭제합니다 (
Room#onDispose) - 종료 프로세스
Server#onShutdown전에 선택적 비동기 콜백을 실행합니다
onLeave / onDispose에서 비동기 작업을 수행해야 하는 경우 Promise를 반환하고 작업이 준비되면 resolve해야 합니다. onShutdown(callback)도 마찬가지입니다.
Promise 반환
Promise를 반환함으로써 서버는 워커 프로세스를 종료하기 전에 이들을 완료할 때까지 기다립니다.
import { Room } from "colyseus";
class MyRoom extends Room {
onLeave (client) {
return new Promise((resolve, reject) => {
doDatabaseOperation((err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
onDispose () {
return new Promise((resolve, reject) => {
doDatabaseOperation((err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
async 사용
async 키워드는 함수가 내부적으로 Promise를 반환하도록 합니다. Async / Await에 대해 더 읽어보세요.
import { Room } from "colyseus";
class MyRoom extends Room {
async onLeave (client) {
await doDatabaseOperation(client);
}
async onDispose () {
await removeRoomFromDatabase();
}
}
프로세스 종료 콜백
onShutdown 콜백을 설정하여 프로세스 종료를 수신할 수도 있습니다.
import { Server } from "colyseus";
let server = new Server();
server.onShutdown(function () {
console.log("마스터 프로세스가 종료되고 있습니다!");
});
참조
한국어 매뉴얼은 다음에서 동기화 업데이트됩니다:
- https:/colyseus.hacker-linner.com