목차
Tic Tac Toe 서버 만들기
25.03.06
지난 시간에 놓친 활동하기
: Tic Tac Toe 자동 로그인 만들기
- 점수 불러오기 기능과 로그인 기능을 이용해 자동 로그인 기능을 만들어 보세요.
--> 한번 로그인을 하면 자동으로 로그인이 되지만, 아직 안한 상태라면 로그인 창이 뜨도록
활동
: TicTacToe 스코어 등록 및 리더보드 만들기
- 싱글 플레이에서 게임에 승리하면 유저에게 10점 부여하세요 --> 현재 싱글 플레이에는 Minimax 알고리즘이 적용되어 절대 승리할 수 없기 때문에 2인 플레이에서 승리하면 유저에게 10점 부여하는 식으로 확인 가능
- 전체 유저를 대상으로 랭킹을 보여주는 리더보드를 만드세요
- 리더보드는 Scroll View를 이용해 만드세요
- Scroll View에는 유저 닉네임 + 점수를 표시하세요
- 서버에서 유저 랭킹을 보여주는 기능을 만드세요
- 리더보드를 실행했을 때 자신의 점수를 바로 볼 수 있게 만들어주세요
※ Hint : 처음 로그인 했을 때 Cookie가 전달되고 Session 파일이 존재하면 Cookie가 전달되지 않을 것이기 때문에 로그인 했을 때 받은 Cookie 값을 저장해놓고 점수 저장과 불러오기를 구현하면 된다.
※ UnityWebRequest에서도 Cookie가 사용된다. 정확히는 게임 세션 동안 Cookie를 캐싱한다.
--> UnityWebRequest.ClearCookieCache(); 를 사용하면 캐싱된 쿠키를 제거할 수 있다.
리더보드 만들기
>> 코드 작성
- NetworkManager.cs
- SigninPanelController.cs
- app.js
- leaderboard.js
채팅 서버 만들기
>> www 파일에 코드 추가
var game = require('../game'); // 추가
/**
* 게임 서버 실행
*/
game(server); // 추가
>> game.js 생성 및 코드 작성
>> 'Main Panel'의 자식으로 'Chatting Panel' 생성
: Color는 임의로 정한 색
--> 이름에 t 하나 빼먹음..
>> Chating Panel의 자식으로 Scroll View 생성
--> Anchor는 Alt + Shift
>> Content
>> Content의 자식으로 Text 추가
>> Chating Panel의 자식으로 Input Field 생성
--> Anchor는 Alt + Shift
>> Placeholder와 Text의 Alignment를 Middle로 설정
최종 코드
>> NetworkManager.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Networking;
public class NetworkManager : Singleton<NetworkManager>
{
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
public IEnumerator Signup(SignupData signupData, Action success, Action failure)
{
string jsonString = JsonUtility.ToJson(signupData);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
// () 안의 객체가 {}를 벗어나면 Disposing되는 문법
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/signup", UnityWebRequest.kHttpVerbPOST))
{
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
Debug.Log("Error : " + www.error);
if (www.responseCode == 409) // 중복 사용자
{
// TODO: 중복 사용자 생성 팝업 표시
Debug.Log("중복 사용자");
GameManager.Instance.OpenConfirmPanel("이미 존재하는 사용자입니다.", () =>
{
failure?.Invoke();
});
}
}
else
{
var result = www.downloadHandler.text;
Debug.Log("Result : " + result);
// 회원가입 성공 팝업 표시
GameManager.Instance.OpenConfirmPanel("회원가입이 완료 되었습니다.", () =>
{
success?.Invoke();
});
}
}
}
public IEnumerator Signin(SigninData signinData, Action success, Action<int> failure)
{
string jsonString = JsonUtility.ToJson(signinData);
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonString);
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/signin", UnityWebRequest.kHttpVerbPOST))
{
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
}
else
{
// 점수 불러오기 기능을 구현하기 위해 cookie 값 저장 (수강생님 코드 참고)
var cookie = www.GetResponseHeader("set-cookie");
if (!string.IsNullOrEmpty(cookie))
{
//int lastIndex = cookie.LastIndexOf(";");
//string sid = cookie.Substring(0, lastIndex);
//PlayerPrefs.SetString("sid", sid);
int lastIndex = cookie.IndexOf('='); // 첫 번째 '='의 위치 찾기
if (lastIndex != -1 && lastIndex + 1 < cookie.Length) // '='이 존재하고, 뒤에 값이 있을 경우
{
string sid = cookie.Substring(lastIndex + 1).Split(';')[0]; // '=' 다음부터 ';' 이전까지 추출
Debug.Log("Session ID : " + sid);
PlayerPrefs.SetString("sid", sid);
}
else
{
// 올바른 쿠키 형식이 아님
}
}
else
{
Debug.Log("쿠키가 비어있습니다.");
}
var resultString = www.downloadHandler.text;
var result = JsonUtility.FromJson<SigninResult>(resultString);
if (result.result == 0)
{
// username이 유효하지 않음
GameManager.Instance.OpenConfirmPanel("Username이 유효하지 않습니다.", () =>
{
failure?.Invoke(0);
});
}
else if (result.result == 1)
{
// password가 유효하지 않음
GameManager.Instance.OpenConfirmPanel("Password가 유효하지 않습니다.", () =>
{
failure?.Invoke(1);
});
}
else if (result.result == 2)
{
// 성공
GameManager.Instance.OpenConfirmPanel("로그인에 성공하였습니다.", () =>
{
success?.Invoke();
});
}
}
}
}
public IEnumerator GetScore(Action<ScoreResult> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/users/score", UnityWebRequest.kHttpVerbGET))
{
www.downloadHandler = new DownloadHandlerBuffer();
string sid = PlayerPrefs.GetString("sid", "");
if (!string.IsNullOrEmpty(sid))
{
www.SetRequestHeader("Cookie", sid);
}
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
if (www.responseCode == 403)
{
Debug.Log("로그인이 필요합니다.");
}
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
var userScore = JsonUtility.FromJson<ScoreResult>(result);
Debug.Log(userScore.score);
success?.Invoke(userScore);
}
}
}
public IEnumerator GetLeaderboard(Action<Scores> success, Action failure)
{
using (UnityWebRequest www =
new UnityWebRequest(Constants.ServerURL + "/leaderboard", UnityWebRequest.kHttpVerbGET))
{
www.downloadHandler = new DownloadHandlerBuffer();
string sid = PlayerPrefs.GetString("sid", "");
if (!string.IsNullOrEmpty(sid))
{
www.SetRequestHeader("Cookie", sid);
}
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError ||
www.result == UnityWebRequest.Result.ProtocolError)
{
if (www.responseCode == 403)
{
Debug.Log("로그인이 필요합니다.");
}
failure?.Invoke();
}
else
{
var result = www.downloadHandler.text;
var scores = JsonUtility.FromJson<Scores>(result);
success?.Invoke(scores);
}
}
}
}
>> SigninPanelController.cs
using TMPro;
using UnityEngine;
public struct SigninData
{
public string username;
public string password;
}
public struct SigninResult
{
public int result;
}
public struct ScoreResult
{
public string id;
public string username;
public string nickname;
public int score;
}
public struct ScoreInfo
{
public string username;
public string nickname;
public int score;
}
public struct Scores
{
public ScoreInfo[] scores;
}
public class SigninPanelController : MonoBehaviour
{
[SerializeField] private TMP_InputField usernameInputField;
[SerializeField] private TMP_InputField passwordInputField;
public void OnClickSigninButton()
{
string username = usernameInputField.text;
string password = passwordInputField.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
// TODO: 누락된 값 입력 요청 팝업 표시
return;
}
var signinData = new SigninData();
signinData.username = username;
signinData.password = password;
StartCoroutine(NetworkManager.Instance.Signin(signinData, () =>
{
Destroy(gameObject);
}, result =>
{
if (result == 0)
{
usernameInputField.text = "";
}
else if (result == 1)
{
passwordInputField.text = "";
}
}));
}
public void OnClickSignupButton()
{
GameManager.Instance.OpenSignupPanel();
}
}
>> app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var mongodb = require('mongodb'); // 추가
var MongoClient = mongodb.MongoClient; // 추가
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var leaderboardRouter = require('./routes/leaderboard'); // 리더보드 구현
const req = require('express/lib/request');
const session = require('express-session'); // 추가
var fileStore = require('session-file-store')(session); // 추가
var app = express();
// 로그인 만들기
app.use(session({
secret: process.env.SESSION_SECRET || 'session-login',
resave: false,
saveUninitialized: false,
store: new fileStore({
path: './sessions',
ttl: 24 * 60 * 60, // 24시간
reapInterval: 60 * 60 // 1시간
}),
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24시간
}
}));
// MongoDB 추가
async function connectDB() {
var databaseURL = "mongodb://localhost:27017/tictactoe";
try {
const database = await MongoClient.connect(databaseURL, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log("DB 연결 완료 : " + databaseURL);
app.set('database', database.db('tictactoe'));
// 연결 종료 처리
process.on("SIGINT", async () => {
await database.close();
console.log("DB 연결 종료");
process.exit(0);
});
} catch (err) {
console.error("DB 연결 실패 : " + err);
process.exit(1);
}
}
connectDB().catch(err => {
console.error("초기 DB 연결 실패 : " + err);
process.exit(1);
});
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/leaderboard', leaderboardRouter); // 리더보드 구현
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
>> leaderboard.js
var express = require('express');
var router = express.Router();
router.get('/', async function(req, res, next) {
try {
if (!req.session.isAuthenticated) {
return res.status(400).send("로그인이 필요합니다.");
}
var database = req.app.get('database');
var users = database.collection('users');
const allUsers = await users.find({}, {
projection: {
username: 1,
nickname: 1,
score: 1
}
}).sort({score: -1}).toArray();
const scoreList = allUsers.map(user => ({
username: user.username,
nickname: user.nickname,
score: user.score || 0
}));
const result = {
scores: scoreList
};
res.status(200).json(result);
} catch (err) {
console.error("리더보드 조회 중 오류 발생 : ", err);
res.status(500).send("서버 오류가 발생했습니다.");
}
});
module.exports = router;
>> www
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('tictactoe-server:server');
var http = require('http');
var game = require('../game'); // 추가
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* 게임 서버 실행
*/
game(server); // 추가
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
>> game.js
const { v4: uuidv4 } = require('uuid');
module.exports = function(server) {
const io = require('socket.io')(server);
var rooms = [];
io.on('connection', function(socket) {
// 클라이언트가 연결되면 실행되는 이벤트 핸들러
console.log('사용자가 연결 되었습니다.');
if (rooms.length > 0) {
var roomId = rooms.shift();
socket.join(roomId);
socket.emit('joinRoom', { roomId: roomId});
socket.to(roomId).emit('startGame', { userId: socket.id });
} else {
var roomId = uuidv4();
socket.join(roomId);
socket.emit('createRoom', { roomId: roomId});
rooms.push(roomId);
}
socket.on('leaveRoom', function() {
socket.leave(roomId);
socket.emit('leaveRoom', { roomId: roomId });
socket.to(roomId).emit('gameEnded', { userId: socket.id });
});
socket.on('sendMessage', function(message) {
console.log('메세지를 받았습니다 : ' + message.nickName + ' : ' + message.message + ' : ' + message.roomId);
socket.to(message.roomId).emit('receiveMessage', { nickName: message.nickName, message: message.message });
});
socket.on('disconnect', function() {
console.log('사용자가 연결을 해제했습니다.');
});
});
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 69일차 (0) | 2025.03.07 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 67일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 66일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 65일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |