설정, 클라이언트 이동 동기화.
소개
제목에서 알 수 있듯이 이 튜토리얼 시리즈에서는 기본 멀티 플레이어 게임 아키텍처를 소개하고 Node.js를 사용하여 기존 io 게임 (agar.io 및 slither.io)과 유사한 브라우저 기반 멀티 플레이어 게임을 만드는 방법에 대한 전체 자습서를 실행합니다. 서버 쪽은 Node.js, 클라이언트 쪽은 Phaser.js입니다. Node.js 서버 측은 클라이언트와 서버 간의 통신을 위해 Socket.io와 Express.js를 사용합니다. 클라이언트 측은 더 나은 충돌 감지를 위해 특별히 p2 물리를 사용합니다. 이 시리즈의 첫 번째 파트에서는 개발 환경을 설정하고 여러 클라이언트를 서버에 연결하며 마우스 포인터를 사용하여 플레이어 동작을 구현합니다. 이 튜토리얼의 마지막에는 모든 플레이어의 움직임이 동기화됩니다.설정
Node.Js가 설치되어 있지 않은 경우 여기에서 다운로드 할 수 있습니다 (https://nodejs.org/en/). Node를 설치 한 후, Node.js 명령 프롬프트를 실행하고 프로젝트 폴더로 이동하십시오. 그런 다음 npm init을 입력하여 package.json을 작성하십시오. 내 프로젝트 폴더는 멀티 플레이어 게임이라고합니다.우선 socket.io가 필요합니다. Socket.io를 사용하면 클라이언트와 서버 측 사이에 실시간 통신을 구현할 수 있습니다. Socket.io는 Node.js와 같이 이벤트 중심적입니다. 예를 들어, 클라이언트가 "공격"이라는 메시지를 보낸다면 서버는 "공격"메시지를 듣고 그에 대한 조치를 취합니다.
npm install socket.io --save를 입력하십시오. --save는 패키지를 package.json에 종속성으로 포함시킬 것입니다. package.json은 나중에 배포 할 때 필요합니다.
그 다음으로 Express.js가 필요합니다. Express.js는 Node.js 프레임 워크로서 Node.js http 모듈을 사용할 필요없이 웹 응용 프로그램을보다 쉽게 만들 수 있습니다. 이 모듈에서는 Express.js가 이미 가지고 있는 많은 것을 다시 구현해야합니다. . 멀티 플레이어 게임을 보다 쉽게 개발할 수 있습니다.
Socket.io와 마찬가지로 Express.js를 설치하려면 npm install express --save를 입력하십시오.
이제 코드를 작성할 준비가되었습니다.
index.html
<body>
<div id="gameDiv">
</div>
</body>
<script src="client/lib/phaser.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="client/player.js"></script>
<script src="client/main.js"></script>
index.html에서 페이저 게임을 위한 컨테이너를 설정하십시오. 여기, "gameDiv"입니다. 페이저 게임 프레임 워크, 클라이언트 용 socket.io 라이브러리 및 게임 파일을 참조하십시오.
1 단계:
클라이언트 측 : main.js
var socket; // socket이라는 전역 변수를 정의한다.
socket = io.connect(); // 서버에 연결 요청 보내기
// 게임을 브라우저에 적절히 맞출수 있도록 화면 크기를 구성
canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;
// 페이저 게임을 만든다.
game = new Phaser.Game(canvas_width,canvas_height, Phaser.CANVAS,
'gameDiv');
var gameProperties = {
// 이것은 세계의 경계를 결정하는 실제 게임 크기입니다.
gameWidth: 4000,
gameHeight: 4000,
};
// 이것은 주 게임 상태입니다.
var main = function(game){
};
// add the
main.prototype = {
preload: function() {
},
// 이 함수는 게임을로드 할 때 한 번 실행됩니다.
create: function () {
console.log("client started");
// 서버에서 "연결"메시지를 듣습니다.
// 클린트가 연결되면 서버는 자동으로 "연결"메시지를 내 보냅니다.
// 클라이언트가 연결되면 onsocketConnected를 호출하십시오.
socket.on("connect", onsocketConnected);
}
}
// 이 함수는 우리가 연결할 때 시작된다.
function onsocketConnected () {
console.log("connected to server");
}
// 게임 상태를 감싼다.
var gameBootstrapper = {
init: function(gameContainerElementId){
game.state.add('main', main);
game.state.start('main');
}
};;
// 래퍼에서 init 함수를 호출하고 division ID를 지정합니다.
gameBootstrapper.init("gameDiv");
코멘트는 그것이 어떻게 작동하는지 간단히 설명합니다. 중요한 라인은 io.connect ()입니다. 기본적으로 클라이언트는 서버에 대한 연결을 요청할 수 있습니다. 서버는 이 연결 요청을 수신하고 성공적으로 연결할 때 "연결"메시지를 다시 클라이언트로 내 보냅니다. 그래서 우리는 socket.on ( "connect", onsocketConnected)이라는 줄을 가지고 있습니다. 클라이언트가 연결 메시지를 받으면 나중에 게임을 초기화 할 수있는 onsocketConnected 함수를 호출합니다.서버 : app.js :
// import express.js
var express = require('express');
// 변수 app에 할당
var app = express();
// 서버를 만들고 요청 처리기로 응용 프로그램에 전달.
var serv = require('http').Server(app); //Server-11
// 주어진 경로에 get 요청이 발생하면 index.html
파일을 보내십시오.이 경우 '/'입니다.
app.get('/',function(req, res) {
res.sendFile(__dirname + '/client/index.html');
});
// 이것은 get 요청이 '/client'에 도달 할 때 모든
정적 파일을 클라이언트 폴더에 넣는 것을 의미합니다
app.use('/client',express.static(__dirname + '/client'));
// listen on port 2000
serv.listen(process.env.PORT || 2000);
console.log("Server started.");
// 우리가 만든 serv 객체를 socket.io에 바인드한다.
var io = require('socket.io')(serv,{});
// 모든 클라이언트의 연결 요청 수신 대기
io.sockets.on('connection', function(socket){
console.log("socket connected");
// 고유 한 socket.id를 출력하십시오.
console.log(socket.id);
});
서버 측 코드에서 먼저 익스프레스 모듈을 가져 와서 변수 앱에 할당합니다. 우리는 다음이 응용 프로그램을 사용하여 서버를 만듭니다.Server11 : 이것이 무엇인지 궁금 할 것입니다. Node Http 모듈에서 require ( 'http'). Server (function (requestListener))는 서버에 요청할 때마다 requestListener를 실행하여 응답과 요청을 처리합니다. app)를 requestListener로 사용하여 응답 및 요청을 처리합니다.
그런 다음 client라는 정적 파일을 설정합니다. app.use ( '/ client', express.static (__ dirname + '/ client'))가 없으면 기본적으로 Node.js는 index.html의 클라이언트 폴더에서 파일을 참조하려고 할 때 수행 할 작업을 알 수 없습니다. Express.js는 express.static이라는 편리한 함수를 제공하여 정적 파일에 쉽게 액세스 할 수있게 해줍니다.
예를 들어 index.html에서 <script src = "client/main"></script>를 호출하여 javascript 게임 파일에 액세스했습니다. 이 파일은 localhost:2000/client/main.js에 있습니다. app.use( '/ client', express.static(__ dirname + '/ client'))는 클라이언트 폴더에있는 모든 정적 파일을 localhost : 2000/client에 저장하여 액세스 할 수 있도록합니다.
우리의 게임을 실제로 볼 시간입니다 !! 노드 app.js를 입력하여 서버를 실행하십시오. 브라우저로 이동하여 localhost:2000을 입력하십시오. 콘솔에 연결된 소켓 메시지와 긴 임의의 문자 (소켓 ID)가 표시되면 정상적으로 작동합니다.
와우, 몇 가지 연결 메시지를 출력 할 수 있지만 실제 게임을 만들고 싶습니다! 이제는 socket.io에 들어가기에 좋은시기입니다. 이 클라이언트와 서버 코드에서는 클라이언트가 서버에 연결할 때마다 플레이어 객체를 생성합니다
2 단계:
클라이언트 main.js :
var socket;
socket = io.connect();
canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;
game = new Phaser.Game(canvas_width,canvas_height,
Phaser.CANVAS, 'gameDiv');
var gameProperties = {
gameWidth: 4000,
gameHeight: 4000,
game_elemnt: "gameDiv",
in_game: false,
};
var main = function(game){
};
// 플레이어가 서버에 연결할 때 이 함수를 호출하십시오.
function onsocketConnected () {
// 연결된 사용자가 제어 할 수 있도록 기본 플레이어 개체를 만듭니다.
createPlayer();
gameProperties.in_game = true;
// 서버에 새로운 플레이어 객체가 생성되었음을 알리는
// "new_player"메시지를 보내십시오.
socket.emit('new_player', {x: 0, y: 0, angle: 0});
}
// CLIENT의 "main"플레이어 클래스.
// 이 플레이어는 사용자가 제어하는 플레이어입니다.
// 이 예제를 사용하여 그래픽을 사용하여 그림을 그릴 수 있습니다.
function createPlayer () {
// Phaser의 그래픽을 사용하여 원을 그립니다.
player = game.add.graphics(0, 0);
player.radius = 100;
// 채우기 및 선 스타일 설정
player.beginFill(0xffd900);
player.lineStyle(2, 0xffd900, 1);
player.drawCircle(0, 0, player.radius * 2);
player.endFill();
player.anchor.setTo(0.5,0.5);
player.body_size = player.radius;
// 모양을 그리다
game.physics.p2.enableBody(player, true);
player.body.addCircle(player.body_size, 0 , 0);
}
main.prototype = {
preload: function() {
game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
game.world.setBounds(0, 0, gameProperties.gameWidth,
gameProperties.gameHeight, false, false, false, false);
// 저는 물리 시스템에 P2JS를 사용하고 있습니다.
game.physics.startSystem(Phaser.Physics.P2JS);
game.physics.p2.setBoundsToWorld(false, false, false, false, false)
// y 중력을 0으로 설정합니다.
// 이것은 플레이어가 중력에 의해 떨어지지 않는다는 것을 의미합니다.
game.physics.p2.gravity.y = 0;
// 중력을 끄다
game.physics.p2.applyGravity = false;
game.physics.p2.enableBody(game.physics.p2.walls, false);
// 충돌 감지 켜기
game.physics.p2.setImpactEvents(true);
},
create: function () {
game.stage.backgroundColor = 0xE1A193;;
console.log("client started");
// 클라이언트가 서버에 성공적으로 연결하고 onsocketConnected를
// 호출하면 수신 대기합니다.
socket.on("connect", onsocketConnected);
},
update: function () {
// 플레이어 입력을 내 보낸다.
// 그가 게임에 있을 때 플레이어를 움직이십시오.
if (gameProperties.in_game) {
// phaser의 마우스 포인터를 사용하여 사용자의 마우스 위치를
// 추적합니다.
var pointer = game.input.mousePointer;
// distanceToPointer를 사용하면 마우스 포인터와 플레이어
// 개체 사이의 거리를 측정 할 수 있습니다.
if (distanceToPointer(player, pointer) <= 50) {
// 플레이어는 특정 속도로 마우스 포인터로 이동할 수 있습니다.
// 이것이 어떻게 구현되는지 player.js를보십시오.
movetoPointer(player, 0, pointer, 100);
} else {
movetoPointer(player, 500, pointer);
}
}
}
}
var gameBootstrapper = {
init: function(gameContainerElementId){
game.state.add('main', main);
game.state.start('main');
}
};;
gameBootstrapper.init("gameDiv");
중요한 줄은 socket.emit ( 'new_player', {x : 0, y : 0, angle : 0}) ;입니다. 이것이 socket.io를 사용하여 서버에 메시지를 보내는 방법입니다. 첫 번째 매개 변수는 메시지 이름이고 두 번째 매개 변수는 보내려는 데이터입니다. 클라이언트가 서버에 연결할 때 플레이어가 제어 할 원형 개체를 추가하기만 하면됩니다. movetoPointer를 사용하면 특정 속도로 마우스 포인터를 향해 이동할 수 있습니다. 구현을 위해 player.js를 볼 수 있습니다.서버 : app.js
var express = require('express');
var app = express();
var serv = require('http').Server(app);
app.get('/',function(req, res) {
res.sendFile(__dirname + '/client/index.html');
});
app.use('/client',express.static(__dirname + '/client'));
serv.listen(process.env.PORT || 2000);
console.log("Server started.");
// 여기에 우리는 서버에 연결된 클라이언트의
// 모든 플레이어를 저장합니다.
var player_lst = [];
// 플레이어 목록에 저장되는 플레이어 "클래스"
var Player = function (startX, startY, startAngle) {
var x = startX
var y = startY
var angle = startAngle
}
// 서버가 클라이언트로부터 "new_player"라는 메시지를받을 때마다
// onNewplayer 함수가 호출됩니다.
function onNewplayer (data) {
// 새로운 플레이어 오브젝트를 형성하다
var newPlayer = new Player(data.x, data.y, data.angle);
console.log("created new player with id " + this.id);
player_lst.push(newPlayer);
}
// io connection
var io = require('socket.io')(serv,{});
io.sockets.on('connection', function(socket){
console.log("socket connected");
// 클라이언트에서 "new_player"메시지 청취
socket.on("new_player", onNewplayer);
});
새 서버 측 코드에서 추가 한 중요한 기능은 socket.on ( "new_player", onNewplayer)입니다. onNewPlayer 함수 내에서 "this.id" 줄을 확인하십시오. 함수에서 "this"는 io.sockets.on의 "socket"을 참조합니다. 콜백의 "this.id"는 io.sockets.on의 "socket.id"와 같습니다. Socket.id는 고유하므로 모든 연결마다 다른 ID가 있습니다.게임을 시작하면 이제 서클을 제어 할 수 있습니다! 하지만 ... 다른 플레이어를 보지 못하면 멀티 플레이어 게임이 아닙니다. 실시간으로 적을 추가 할 시간입니다!
3 단계
클라이언트 : main.js
var socket;
socket = io.connect();
canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;
game = new Phaser.Game(canvas_width,canvas_height, Phaser.CANVAS,
'gameDiv');
// 적 플레이어 목록
var enemies = [];
var gameProperties = {
gameWidth: 4000,
gameHeight: 4000,
game_elemnt: "gameDiv",
in_game: false,
};
var main = function(game){
};
function onsocketConnected () {
console.log("connected to server");
createPlayer();
gameProperties.in_game = true;
// 서버에 초기 위치에 보내고 우리에게 연결되었음을 알려줍니다.
socket.emit('new_player', {x: 0, y: 0, angle: 0});
}
// 서버가 클라이언트 연결 끊김을 알리면 연결되지 않은 적을 발견하고
// 게임에서 제거합니다.
function onRemovePlayer (data) {
var removePlayer = findplayerbyid(data.id);
// Player not found
if (!removePlayer) {
console.log('Player not found: ', data.id)
return;
}
removePlayer.player.destroy();
enemies.splice(enemies.indexOf(removePlayer), 1);
}
function createPlayer () {
player = game.add.graphics(0, 0);
player.radius = 100;
// set a fill and line style
player.beginFill(0xffd900);
player.lineStyle(2, 0xffd900, 1);
player.drawCircle(0, 0, player.radius * 2);
player.endFill();
player.anchor.setTo(0.5,0.5);
player.body_size = player.radius;
// draw a shape
game.physics.p2.enableBody(player, true);
player.body.clearShapes();
player.body.addCircle(player.body_size, 0 , 0);
player.body.data.shapes[0].sensor = true;
}
// 이것이 적 클래스 입니다.
var remote_player = function (id, startx, starty, start_angle) {
this.x = startx;
this.y = starty;
// 이것이 유일한 소켓 ID입니다.
// 우리는 그것을 적의 유일한 이름으로 사용한다.
this.id = id;
this.angle = start_angle;
this.player = game.add.graphics(this.x , this.y);
this.player.radius = 100;
// set a fill and line style
this.player.beginFill(0xffd900);
this.player.lineStyle(2, 0xffd900, 1);
this.player.drawCircle(0, 0, this.player.radius * 2);
this.player.endFill();
this.player.anchor.setTo(0.5,0.5);
this.player.body_size = this.player.radius;
// draw a shape
game.physics.p2.enableBody(this.player, true);
this.player.body.clearShapes();
this.player.body.addCircle(this.player.body_size, 0 , 0);
this.player.body.data.shapes[0].sensor = true;
}
// 서버는 새로운 적 플레이어가 서버에 연결할 때 알려줍니다.
// 우리는 우리 게임에서 새로운 적을 창조합니다.
function onNewPlayer (data) {
console.log(data);
// 적 개체
var new_enemy = new remote_player(data.id, data.x,
data.y, data.angle);
enemies.push(new_enemy);
}
// 서버는 새로운 적의 움직임이 있음을 알려줍니다.
// 이동 된 적을 찾고 서버와 적의 움직임을 동기화합니다.
function onEnemyMove (data) {
console.log(data.id);
console.log(enemies);
var movePlayer = findplayerbyid (data.id);
if (!movePlayer) {
return;
}
movePlayer.player.body.x = data.x;
movePlayer.player.body.y = data.y;
movePlayer.player.angle = data.angle;
}
// 여기서 우리는 소켓 ID를 사용합니다.
// 적의 목록을 검색하여 적을 찾습니다.
function findplayerbyid (id) {
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].id == id) {
return enemies[i];
}
}
}
main.prototype = {
preload: function() {
game.stage.disableVisibilityChange = true;
game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
game.world.setBounds(0, 0, gameProperties.gameWidth,
gameProperties.gameHeight,
false, false, false, false);
game.physics.startSystem(Phaser.Physics.P2JS);
game.physics.p2.setBoundsToWorld(false, false, false, false, false)
game.physics.p2.gravity.y = 0;
game.physics.p2.applyGravity = false;
game.physics.p2.enableBody(game.physics.p2.walls, false);
// physics start system
//game.physics.p2.setImpactEvents(true);
},
create: function () {
game.stage.backgroundColor = 0xE1A193;;
console.log("client started");
socket.on("connect", onsocketConnected);
// listen to new enemy connections
socket.on("new_enemyPlayer", onNewPlayer);
// listen to enemy movement
socket.on("enemy_move", onEnemyMove);
// remove_player를 받으면 플레이어를 제거합니다.
socket.on('remove_player', onRemovePlayer);
},
update: function () {
// 플레이어 입력을 내 보낸다.
// 플레이어가 만들어지면 플레이어를 움직인다.
if (gameProperties.in_game) {
var pointer = game.input.mousePointer;
if (distanceToPointer(player, pointer) <= 50) {
movetoPointer(player, 0, pointer, 100);
} else {
movetoPointer(player, 500, pointer);
}
//Send a new position data to the server
socket.emit('move_player', {x: player.x, y: player.y,
angle: player.angle});
}
}
}
var gameBootstrapper = {
init: function(gameContainerElementId){
game.state.add('main', main);
game.state.start('main');
}
};;
gameBootstrapper.init("gameDiv");
클라이언트 측 구조는 매우 복잡하지 않습니다. 우리는 서버로부터 새로운 플레이어 메시지를 기다리고 적 대상의 새로운 인스턴스를 생성하고, 움직일 때 적 개체의 위치를 올바른 위치 (서버에서 전송)로 이동시킵니다. findPlayerbyId 함수는 id에 의해 corrent 적을 찾는 데 사용됩니다. 플레이어가 연결을 끊으면 onRemovePlayer 함수를 사용하여 적 개체를 찾아 게임에서 제거합니다. game.stage.disableVisibilityChange = true 행을 추가하십시오. 즉, 커서가 브라우저를 떠날 때 브라우저를 잠자기 하지 않습니다. 즉, 개발을 위해 두 개의 브라우저를 동시에 모니터링 할 수 있습니다.var express = require('express');
var app = express();
var serv = require('http').Server(app);
app.get('/',function(req, res) {
res.sendFile(__dirname + '/client/index.html');
});
app.use('/client',express.static(__dirname + '/client'));
serv.listen(process.env.PORT || 2000);
console.log("Server started.");
var player_lst = [];
// 서버의 플레이어 클래스
var Player = function (startX, startY, startAngle) {
this.x = startX
this.y = startY
this.angle = startAngle
}
// 새 플레이어가 연결되면 플레이어 개체의 새 인스턴스를 만들고
// 새 플레이어 메시지를 클라이언트에 보냅니다.
function onNewplayer (data) {
console.log(data);
//new player instance
var newPlayer = new Player(data.x, data.y, data.angle);
console.log(newPlayer);
console.log("created new player with id " + this.id);
newPlayer.id = this.id;
// 발신자를 제외한 모든 클라이언트에게 보낼 정보
var current_info = {
id: newPlayer.id,
x: newPlayer.x,
y: newPlayer.y,
angle: newPlayer.angle,
};
// 이미 연결되어있는 모든 사람에 대해 새 플레이어에게 보냅니다.
for (i = 0; i < player_lst.length; i++) {
existingPlayer = player_lst[i];
var player_info = {
id: existingPlayer.id,
x: existingPlayer.x,
y: existingPlayer.y,
angle: existingPlayer.angle,
};
console.log("pushing player");
// 보낸 사람 - 클라이언트에게만 메시지 보내기
this.emit("new_enemyPlayer", player_info);
}
// 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
this.broadcast.emit('new_enemyPlayer', current_info);
player_lst.push(newPlayer);
}
// 플레이어 위치를 업데이트하고 보낸 사람을
// 제외한 모든 클라이언트에게 정보를 보냅니다.
function onMovePlayer (data) {
var movePlayer = find_playerid(this.id);
movePlayer.x = data.x;
movePlayer.y = data.y;
movePlayer.angle = data.angle;
var moveplayerData = {
id: movePlayer.id,
x: movePlayer.x,
y: movePlayer.y,
angle: movePlayer.angle
}
// 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
this.broadcast.emit('enemy_move', moveplayerData);
}
// 클라이언트가 연결을 끊을 때 전화를 걸고 발신자를 제외한
// 클라이언트에게 연결이 끊긴 플레이어를 제거하라고 알립니다.
function onClientdisconnect() {
console.log('disconnect');
var removePlayer = find_playerid(this.id);
if (removePlayer) {
player_lst.splice(player_lst.indexOf(removePlayer), 1);
}
console.log("removing player " + this.id);
// 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
this.broadcast.emit('remove_player', {id: this.id});
}
// 고유 소켓 ID로 플레이어 찾기
function find_playerid(id) {
for (var i = 0; i < player_lst.length; i++) {
if (player_lst[i].id == id) {
return player_lst[i];
}
}
return false;
}
// io connection
var io = require('socket.io')(serv,{});
io.sockets.on('connection', function(socket){
console.log("socket connected");
// listen for disconnection;
socket.on('disconnect', onClientdisconnect);
// listen for new player
socket.on("new_player", onNewplayer);
// listen for player position update
socket.on("move_player", onMovePlayer);
});
서버 측에서는 플레이어의 위치를 업데이트하는 onMovePlayer를 추가했습니다. 우리에게 "this.broadcast.emit"이 있음을 주목하십시오. 즉, 보낸 사람을 제외한 모든 소켓에 데이터를 전송합니다. 우리가 가진 또 다른 유형의 메시지 전송은 "this.emit"입니다. 이것은 "발신자에게만 보내기"를 의미합니다. onNewPlayer에서는 두 가지 작업을 수행합니다. 첫째, 새 플레이어가 연결되면 이전에 이미 게임에 연결된 모든 사람에 대해 특정 새 플레이어에게 보내야합니다. 두 번째로, 우리는 새로운 플레이어에 관해 이미 연결되어있는 모든 플레이어 (새로운 플레이어가 아닌)를 보내야합니다.
이 socket.io 치트 시트를 사용하여 서버의 특정 클라이언트에게 보낼 수 있습니다 : https://socket.io/docs/emit-cheatsheet/
그러나 클라이언트가 클라이언트에서 직접 위치를 전송하기 때문에 이것은 멀티 플레이어의 매우 순진하고 위험한 구현입니다. 해커가 이 순진한 구현을 파악하고 서버와 다른 위치를 전송하려고 시도하는 경우를 상상해 보십시오. 모두는 사기꾼으로부터 가난한 게임 플레이 경험을 겪습니다! 우리는 다음 튜토리얼에서이 순진 구현을 수정하려고 노력할 것입니다!
댓글 없음:
댓글 쓰기