온라인 멀티플레이 이동을 구현하기에 앞서 기존 이동을 tween으로 개선했습니다.
tween은 캐릭터를 (x, y)로 이동 시키는 기능입니다.
클라이언트
Loading Scene에서 '시작하기'를 클릭하면 MyGame Scene으로 이동하고,
MyGame Scene에서는 웹소켓 연결을 생성하고 서버로부터 JOIN이나 EXIT 같은 메세지를 주고 받으면서 다른 플레이어 정보를 주고 받습니다.
// CANVAS 크기
const WIDTH = 800;
const HEIGHT = 600;
// 웹소켓 : 클라이언트 --> 서버
const C_JOIN = 1;
const C_EXIT = 2;
// 웹소켓 : 서버 --> 클라이언트
const S_JOIN = 1;
const S_EXIT = 2;
const SERVER_URL = 'http://localhost:8080/quiz';
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Making your first Phaser 3 Game</title>
<!-- Phaser 3 라이브러리 불러오기 -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.11.0/dist/phaser.js"></script>
<!-- 웹소켓 라이브러리 불러오기 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.js"></script>
<script src="js/define.js"></script>
</head>
<body>
<script type="text/javascript">
// 웹소켓
var webSocket;
// 접속한 모든 플레이어 저장
var players = {};
var loadingScene;
var myGameScene;
class Loading extends Phaser.Scene {
constructor() {
// 식별자
super('loading');
loadingScene = this;
}
preload() {
}
create() {
// 클릭 메세지
this.clickToStart = this.add.text(
config.width / 2
, config.height / 2
, '시작시 클릭'
)
.setFont('고딕')
.setFontSize(50)
.setOrigin(0.5)
// 클릭시 게임 화면으로 이동
this.input.once('pointerdown', () => {
// 페이드 아웃
loadingScene.cameras.main.fadeOut(1000, 0, 0, 0)
// Scene 전환
loadingScene.scene.transition({ target: 'myGame', duration: 1000 });
}, this);
}
update() {
}
}
class MyGame extends Phaser.Scene {
constructor() {
// 식별자
super('myGame');
myGameScene = this;
}
preload() {
this.load.image('sky', 'assets/sky.png');
this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 });
}
create() {
// SockJS 연결 설정
webSocket = new SockJS(SERVER_URL);
// 웹소켓 메시지가 수신되었을 때
webSocket.onmessage = function (event) {
// 서버에서 받은 캐릭터 생성 데이터 처리
var data = JSON.parse(event.data);
// 입장하면 캐릭터 생성
if (data.cmd === S_JOIN) {
if (!players[data.sessionId]) {
// 화면에 캐릭터 생성
let player = myGameScene.physics.add.sprite(data.x, data.y, 'dude');
player.setCollideWorldBounds(true);
// 각 플레이어를 players 객체에 저장
players[data.sessionId] = player;
}
}
// 퇴장하면 오른쪽 하단으로 이동 후 캐릭터 삭제
else if (data.cmd === S_EXIT) {
let player = players[data.sessionId];
// 플레이어 이동
if (player) {
player.anims.play('right', true);
// Tween을 사용하여 부드럽게 이동
myGameScene.tweens.add({
targets: player,
x: WIDTH,
y: HEIGHT,
duration: 1000, // 이동 시간 (밀리초)
ease: 'Linear',
onComplete: function () {
// 화면에서 캐릭터 삭제
player.destroy();
// players 객체에서 플레이어 삭제
delete players[data.sessionId];
}
});
}
}
};
// 웹소켓 연결이 열렸을 때
webSocket.onopen = function () {
// url 형식 --> ws://localhost/quiz/636/wt1naem2/websocket
webSocket.sessionId = webSocket._transport.url.split('/')[5];
// 서버에 사용자 입장 알림
var msgData = {
sessionId: webSocket.sessionId,
cmd: C_JOIN,
x: 0,
y: 0,
msg: ''
};
var jsonData = JSON.stringify(msgData);
webSocket.send(jsonData);
};
// 웹소켓 연결이 닫혔을 때
webSocket.onclose = function () {
};
// 페이드 인
this.cameras.main.fadeIn(1000, 0, 0, 0);
// 배경
this.add.image(config.width / 2, config.height / 2, 'sky');
// 왼쪽
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1
});
// 오른쪽
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
frameRate: 10,
repeat: -1
});
// 위
this.anims.create({
key: 'up',
frames: [{ key: 'dude', frame: 4 }],
frameRate: 10,
repeat: -1
});
// 아래
this.anims.create({
key: 'down',
frames: [{ key: 'dude', frame: 4 }],
frameRate: 10,
repeat: -1
});
// 정지상태
this.anims.create({
key: 'turn',
frames: [{ key: 'dude', frame: 4 }],
frameRate: 10
});
// Input Events
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
// 현재 플레이어 캐릭터
let player = players[webSocket.sessionId];
// JOIN 시간차 처리
if (player === undefined) {
return;
}
// 이동 속도
let speed = 16;
// 목표 좌표
let targetX = player.x;
let targetY = player.y;
// 왼쪽 이동
if (this.cursors.left.isDown) {
targetX = player.x - speed;
player.anims.play('left', true);
}
// 오른쪽 이동
else if (this.cursors.right.isDown) {
targetX = player.x + speed;
player.anims.play('right', true);
}
// 위 이동
else if (this.cursors.up.isDown) {
targetY = player.y - speed;
player.anims.play('up', true);
}
// 아래 이동
else if (this.cursors.down.isDown) {
targetY = player.y + speed;
player.anims.play('down', true);
}
// 정지 상태
else {
player.anims.play('turn');
}
// tween을 사용해 이동
this.tweens.add({
targets: player,
x: targetX,
y: targetY,
duration: 100,
ease: 'Linear'
});
}
}
// 게임 설정 구성
var config = {
type: Phaser.AUTO,
width: WIDTH,
height: HEIGHT,
physics: {
default: 'arcade'
},
scene: [Loading, MyGame]
};
var game = new Phaser.Game(config);
</script>
</body>
</html>
서버
서버는 자바 스프링프레임워크로 구성되어 있습니다.
웹소켓 Handler 부분만 올립니다.
@Component
public class WebSocketHandler extends TextWebSocketHandler {
// <세션ID, <x, y>>
public static Map<String, Map<String, Object>> sessionMap = new HashMap<String, Map<String, Object>>();
// <세션-세션-세션-세션...>
public static List<WebSocketSession> sessionList = new LinkedList<WebSocketSession>();
// 클라이언트가 웹소켓서버로 메시지를 전송했을 때 실행되는 메소드
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
super.handleTextMessage(session, message);
// message JSON 파싱
ObjectMapper objectMapper = new ObjectMapper();
QuizMsgDto quizMsgDto = objectMapper.readValue(message.getPayload(), QuizMsgDto.class);
switch (quizMsgDto.getCmd()) {
case Code.C_JOIN:
// 게임 입장
C_JOIN(session, quizMsgDto);
break;
}
} catch (Exception e) {
System.out.println("***** handleTextMessage Exception *****");
e.printStackTrace();
}
}
// 클라이언트가 연결을 했을 때 실행되는 메소드
@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("***** WebSocket Connection Established *****");
try {
super.afterConnectionEstablished(session);
} catch (Exception e) {
System.out.println("***** afterConnectionEstablished Exception *****");
e.printStackTrace();
}
}
// 클라이언트가 연결을 끊었을 때 실행되는 메소드
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
try {
super.afterConnectionClosed(session, status);
ObjectMapper objectMapper = new ObjectMapper();
// 세션 관리 삭제
sessionList.remove(session);
sessionMap.remove(session.getId());
// 다른 플레이어에게 퇴장한 플레이어 정보 전송
for (WebSocketSession sess : sessionList) {
try {
QuizMsgDto dtoChatServerMsg = new QuizMsgDto(session.getId(), Code.S_EXIT, 0, 0, "퇴장");
String jsonStr = objectMapper.writeValueAsString(dtoChatServerMsg);
if(sess != null && sess.isOpen()) {
sess.sendMessage(new TextMessage(jsonStr));
}
} catch(Exception ex) {
// nothing todo
}
}
} catch (Exception e) {
System.out.println("***** afterConnectionClosed Exception *****");
e.printStackTrace();
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
System.out.println("***** handleTransportError Exception *****");
}
// 게임 입장
private void C_JOIN(WebSocketSession session, QuizMsgDto quizMsgDto) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
// 좌표 랜덤 생성
int newPlayerX = Util.getRandomNumber(200, Define.WIDTH - 200);
int newPlayerY = Util.getRandomNumber(150, Define.HEIGHT - 150);
// 새로 입장한 플레이어 세션 관리에 추가
// <세션-세션-세션-세션...>
sessionList.add(session);
// <세션ID, <x, y>>
Map<String, Object> newPlayerMap = new HashMap<String, Object>();
newPlayerMap.put("x", newPlayerX);
newPlayerMap.put("y", newPlayerY);
sessionMap.put(session.getId(), newPlayerMap);
// 다른 플레이어에게 새로 입장한 플레이어 정보 전송
for (WebSocketSession sess : sessionList) {
try {
QuizMsgDto dtoChatServerMsg = new QuizMsgDto(session.getId(), Code.S_JOIN, newPlayerX, newPlayerY, "입장");
String jsonStr = objectMapper.writeValueAsString(dtoChatServerMsg);
if(sess != null && sess.isOpen()) {
sess.sendMessage(new TextMessage(jsonStr));
}
} catch(Exception ex) {
// nothing todo
}
}
// 새로 입장한 플레이어에게 기존 다른 플레이어 정보 전송
for (WebSocketSession sess : sessionList) {
try {
Map<String, Object> otherPlayer = sessionMap.get(sess.getId());
int otherPlayerX = (int) otherPlayer.get("x");
int otherPlayerY = (int) otherPlayer.get("y");
QuizMsgDto dtoChatServerMsg = new QuizMsgDto(sess.getId(), Code.S_JOIN, otherPlayerX, otherPlayerY, "입장");
String jsonStr = objectMapper.writeValueAsString(dtoChatServerMsg);
if(session != null && session.isOpen()) {
session.sendMessage(new TextMessage(jsonStr));
}
} catch(Exception ex) {
// nothing todo
}
}
}
}
결과화면
다른 플레이어가 접속할 때마다 캐릭터가 추가로 생기고, 다른 플레이어가 창을 닫으면 캐릭터가 오른쪽 하단으로 퇴장하고 사라집니다.
'개발 - phaser3 강좌' 카테고리의 다른 글
phaser3 강좌(7) 온라인 멀티플레이 이동 (0) | 2024.10.24 |
---|---|
phaser3 강좌(5) 온라인 멀티플레이 (0) | 2024.10.15 |
phaser3 강좌(4) Scene 전환 (0) | 2024.10.10 |
phaser3 강좌(3) 캐릭터 이동 (0) | 2024.10.07 |
phaser3 강좌(2) 스프라이트 이미지 불러오기 (0) | 2024.10.07 |