游戏服务架构(四)
这篇章涉及的比较多, 可能需要做好前置的知识:
TCP|UDP|HTTP相关(具体网上资料查询)字节位编码处理(总结过)数据大小端序列(总结过)Json|Protobuf传输协议Godot|Unity至少某款游戏客户端开发就行了, 主要思路都是一致
建议如果以上都没有概念需要网上查漏补缺下, 这些基础知识基本上或多或少会遇到需要处理.
这里用
Godot做演示, 具体Unity也可以去处理.
Web简单游戏
单纯利用 Web 做游戏, 这种简单的游戏服务端形态在差不多 2015 年见过, 那时候用于各地 地方麻将
游戏( PS: 那时候国内游戏项目讲究快速上线, 所以诞生很多为了压低成本而速成的技术, 单纯 Web 请求做的三消游戏也是当年时代产物 ).
当年衍生很多奇奇怪怪的技术栈, 所以能够看到很多现在看起来怪异的技术选型, 而且 2015 年的时候 Websocket 还没爆发性的推广
开发游戏必须先要确定 玩法, 影射现实来说 只有确定玩法的游戏才能叫游戏, 哪怕猜拳都是得知道 '剪刀-石头-布'.
这里采用 骰子比大小 玩法这种数值对拼玩法( 戏称 '电子斗蛐蛐' ), 直接用 php+json 实现个 web 无状态服务:
<?php
// 解析 RAW + JSON 参数
$_PARAMS = file_get_contents('php://input');
$_PARAMS = $_PARAMS ? json_decode($_PARAMS, 1) : [];
$_PARAMS = $_PARAMS ?: [];
// JSON输出工具
function json_response(int $status, string $message = "SUCCESS", array $data = []) {
exit(json_encode([
'status' => $status,
'message' => $message,
'data' => $data
]));
}
// 数据库, 玩家数据肯定需要数据库落地, 这里临时采用 MYSQL 做内存, 实际可以考虑 MongoDB 处理来保证告诉内存数据读写
$_DB = new mysqli("127.0.0.1:3306", "meteorcat", "meteorcat", "game");
if ($_DB->connect_errno) {
json_response(1, "failed by connection database");
}
if (!isset($_PARAMS['proto'])) {
json_response(1, "failed by proto");
}
// 获取提交的状态
$proto = intval($_PARAMS['proto']);
switch ($proto) {
// 默认协议找不到
default:
json_response(1, "failed by not found proto");
break;
// 登录协议
case 101:
// 这里获取账号名称
$username = trim($_PARAMS['username'] ?? "");
$password = trim($_PARAMS['password'] ?? "");
if (strlen($username) <= 0 || strlen($password) <= 0) {
json_response(1, "Failed by Username|Password");
return;
}
// 确定是否存在的账户, 不存在需要帮助注册
$table_name = "player_info";
$hash_password = md5($password);
$sql = "SELECT id FROM {$table_name} WHERE username=? AND password=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Database");
return;
}
$stmt->bind_param("ss", $username, $hash_password);
$stmt->execute();
$row = $stmt->get_result();
$row = $row->fetch_assoc() ?: [];
$stmt->close();
if (!$row) {
// 创建玩家
$sql = "INSERT INTO {$table_name} (username, password, token) VALUES(?, ?, '')";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Database");
return;
}
$stmt->bind_param("ss", $username, $hash_password);
$stmt->execute();
$stmt->close();
// 重新查询数据
$sql = "SELECT id FROM {$table_name} WHERE username=? AND password=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Database");
return;
}
$stmt->bind_param("ss", $username, $hash_password);
$stmt->execute();
$row = $stmt->get_result();
$row = $row->fetch_assoc() ?: [];
$stmt->close();
}
// 确定玩家ID
$uid = intval($row['id'] ?? "0");
if (!$uid) {
json_response(1, "Failed by Player");
return;
}
// 更新Token写入记录
$micro = microtime(true);
$micro_md5 = md5($micro);
$sql = "UPDATE {$table_name} SET token=? WHERE id=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Token");
return;
}
$stmt->bind_param("si", $micro_md5, $uid);
$stmt->execute();
$stmt->close();
// 响应数据返回
json_response(0, "SUCCESS", [
"uid" => $uid,
"token" => $micro_md5
]);
break;
// 登出协议
case 102:
$uid = intval($_PARAMS['uid'] ?? 0);
$token = $_PARAMS['token'] ?? "";
// Token失效机制
$table_name = "player_info";
$sql = "UPDATE {$table_name} SET token='' WHERE id=? AND token=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Token");
return;
}
$stmt->bind_param("is", $uid, $token);
$stmt->execute();
$stmt->close();
json_response(0);
break;
// 详情信息, 获取胜利分数等信息
case 103:
// todo: 这里为了简单实现暂不实现
break;
// 加入房间, 清空结算之前进行的活动
case 201:
$uid = intval($_PARAMS['uid'] ?? 0);
$token = $_PARAMS['token'] ?? "";
// todo: 用于进入返回获取房间码之后结算之前的骰子比赛, 这里目前没考虑实现
break;
// 开始游玩骰子游戏, 投出骰子确定自己和AI的点数, 返回给客户端做表现
case 202:
$uid = intval($_PARAMS['uid'] ?? 0);
$token = $_PARAMS['token'] ?? "";
// 获取是否登录
$table_name = "player_info";
$sql = "SELECT id,value FROM {$table_name} WHERE id=? AND token=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Database");
return;
}
$stmt->bind_param("is", $uid, $token);
$stmt->execute();
$row = $stmt->get_result();
$row = $row->fetch_assoc() ?: [];
$stmt->close();
if (!$row) {
json_response(1, "Failed by Token");
return;
}
// 确定玩家ID
$score = intval($row['value'] ?? "0");
$uid = intval($row['id'] ?? "0");
if (!$uid) {
json_response(1, "Failed by Player");
return;
}
// 投出随机值并写入数据库值
$value = mt_rand(10000, 69999);
$value = intval($value / 10000);
$sql = "UPDATE {$table_name} SET value=value+{$value} WHERE id=? AND token=? LIMIT 1";
$stmt = $_DB->prepare($sql);
if (!$stmt) {
json_response(1, "Failed by Record");
return;
}
$stmt->bind_param("is", $uid, $token);
$stmt->execute();
$stmt->close();
// 响应值返回
json_response(0, "SUCCESS", [
"index" => $value - 1,
"value" => $value,
"score" => $score + $value,
]);
break;
}
之后数据库表只需要暂时生成如下架构:
/*!40101 SET @OLD_CHARACTER_SET_CLIENT = @@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE = @@TIME_ZONE */;
/*!40103 SET TIME_ZONE = '+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS = 0 */;
/*!40101 SET @OLD_SQL_MODE = @@SQL_MODE, SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES = @@SQL_NOTES, SQL_NOTES = 0 */;
-- 导出 game 的数据库结构
CREATE DATABASE IF NOT EXISTS `game` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */;
USE `game`;
-- 导出 表 game.player_info 结构
CREATE TABLE IF NOT EXISTS `player_info`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(64) NOT NULL COMMENT '账号名',
`password` varchar(64) NOT NULL COMMENT '密码',
`token` varchar(64) NOT NULL DEFAULT '',
`value` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '投掷游戏值',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='玩家信息';
-- 数据导出被取消选择。
/*!40103 SET TIME_ZONE = IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE = IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS = IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT = @OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES = IFNULL(@OLD_SQL_NOTES, 1) */;
客户端实现
这里就是做的初版 Web 简单游戏服务器, 之后客户端先暂时实现 Http 推送服务( 基于Godot ):

这里就是最简单的 Web 线上游戏, 很简陋但是已经涵盖有自己所在的玩法系统(
其实主要是懒得找客户端美术资源随便处理下的案例 ),
可以先跑下游戏能否运行, 后续篇章解析出内部存在的问题并且解析出怎么处理存在的问题.
当然这个项目有很多问题, 可能安全性地下/数据传输冗余浪费等不重复十几个问题, 但是你已经做出实现玩法自己玩法的游戏! 有句话说得好, 你制作出来前十个大概率垃圾游戏, 你要做的就是赶快把这前十个做完! 不要总是瞻前顾后, 而是要赶快行动起来边制作边思考总结.