公司动态
游戏服务器架构实战:基于Netty与Spring Boot构建高可用后端系统
最近在游戏开发社区中不少开发者都在讨论如何构建一个稳定、可扩展且具备良好平衡性的游戏服务器。无论是大型多人在线游戏还是策略类手游服务器的性能、数据一致性以及玩家间的公平性即“平衡性”都是决定项目成败的关键。本文将围绕构建一个高可用游戏服务器的核心实战展开从环境搭建、架构设计、核心代码实现到性能调优与常见问题排查提供一个完整的闭环解决方案。无论你是正在学习后端开发的学生还是面临游戏服务器性能瓶颈的工程师都能从本文中找到可直接复用的代码和思路。1. 背景与核心概念什么是游戏服务器的“平衡性”在游戏开发领域尤其是策略类、竞技类游戏中“平衡性”是一个多维度的概念。它不仅仅指角色或单位的数值强弱更涵盖了整个游戏生态的稳定与公平。从技术视角看服务器的“平衡性体验”主要依赖于以下几个核心支柱数据一致性确保所有玩家客户端看到的世界状态是同步的。例如A玩家攻击B玩家B玩家和服务器必须在同一时刻计算伤害结果任何延迟或不同步都会导致“我打中了却没掉血”的糟糕体验这本质上是服务器状态同步的平衡。请求公平性服务器处理玩家请求如移动、攻击、建造的队列机制和资源分配必须公平。不能因为某个玩家连接质量好或请求频繁就获得更快的响应这需要通过合理的网络协议和逻辑帧设计来保障。性能与负载均衡当在线玩家数量激增时服务器不能出现明显的性能衰减。这需要通过水平扩展增加服务器节点、有效的负载均衡策略以及高效的资源管理来实现确保所有玩家享有相近的流畅度。逻辑与规则公平这是游戏策划层面的平衡但需要服务器代码严谨实现。例如伤害计算公式、资源产出速率、匹配算法等必须杜绝因代码漏洞导致的不公平优势。本文的实战将聚焦于技术层面通过构建一个模拟策略游戏如“无尽的拉格朗日”中的部分功能的服务器原型来具体阐述如何实现上述的技术平衡性。2. 环境准备与版本说明在开始编码前我们需要搭建一个标准的游戏服务器开发环境。本例将使用Java作为后端语言Spring Boot框架简化Web服务开发Netty处理高性能网络通信Redis作为缓存和会话存储数据库使用MySQL。版本说明操作系统Windows 10 / macOS / Linux (Ubuntu 20.04) 均可本文命令以Linux为例。JavaJDK 11 或 17 (LTS版本)。推荐 OpenJDK。Spring Boot2.7.x 或 3.0.x (注意JDK版本对应关系本文示例用2.7.18)。Netty4.1.x。Redis6.x 或 7.x。MySQL8.0.x。构建工具Maven 3.6 或 Gradle。IDEIntelliJ IDEA, VS Code 或 Eclipse。重要提示版本需要根据你的项目实际情况调整。本文重点演示架构思路和核心代码版本差异可能导致部分配置或API略有不同请以官方文档为准。3. 核心架构与原理拆解一个典型的游戏服务器架构可以分为三层网关层、逻辑层和数据层。玩家客户端 --(WebSocket/TCP)-- [网关服务器] --(RPC/HTTP)-- [游戏逻辑服务器] --(JDBC/MyBatis)-- [数据库/缓存] | | (会话管理、协议编解码) (游戏核心逻辑、状态计算)3.1 网关层 (Gateway)作用作为客户端连接的第一入口负责维护长连接如WebSocket、协议解析将二进制或JSON数据包解析为内部对象、身份验证、流量整形和请求路由。它屏蔽了内部逻辑服务器的复杂性是实现高并发和负载均衡的关键。技术选型Netty。因其异步事件驱动的特性非常适合处理海量并发连接。3.2 逻辑层 (Game Logic Server)作用处理具体的游戏业务逻辑如移动、战斗、建造、任务等。它接收来自网关的请求执行业务计算更新游戏状态并将结果广播给相关玩家。技术选型Spring Boot。提供完善的依赖注入、配置管理和模块化支持便于快速开发复杂的业务逻辑。3.3 数据层 (Data Layer)作用持久化存储玩家数据、游戏世界状态。为了性能通常会引入缓存。缓存 (Redis)存储热点数据如在线玩家信息、全局配置、会话信息、排行榜等降低数据库压力。数据库 (MySQL)持久化存储玩家基础数据、邮件、日志等。3.4 状态同步与帧同步对于实时性要求高的游戏如MOBA、FPS常采用帧同步服务器以固定的频率如每秒20帧向所有客户端发送当前帧的指令集客户端根据指令自行计算并渲染。这要求所有客户端初始状态和逻辑计算必须完全一致。 对于策略类、MMORPG更多采用状态同步服务器是唯一权威状态源。客户端发送操作请求服务器计算后将结果状态或状态差值同步给相关客户端。本文示例采用状态同步。4. 完整实战案例构建一个简易的策略游戏服务器我们将实现一个极简的“星系探索”游戏服务器功能包括玩家登录、查看星系列表、派遣舰队到某个星系。4.1 项目结构与依赖首先使用 Spring Initializr 创建一个 Spring Boot 项目选择Web,Redis,MySQL依赖。然后手动在pom.xml中添加 Netty 依赖。项目结构game-server-demo ├── src/main/java/com/example/gameserver │ ├── gateway # 网关模块 │ │ ├── NettyServer.java │ │ ├── GameMessageHandler.java │ │ └── protocol # 协议编解码包 │ ├── logic # 逻辑模块 │ │ ├── controller # 处理内部RPC或HTTP请求 │ │ ├── service │ │ └── model # 数据模型 │ ├── common # 公共模块 │ │ ├── constant │ │ ├── util │ │ └── exception │ └── GameServerApplication.java └── src/main/resources ├── application.yml └── ...关键依赖 (pom.xml):dependencies !-- Spring Boot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Netty -- dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.94.Final/version /dependency !-- 工具类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.43/version /dependency /dependencies4.2 网关服务器实现 (Netty)网关的核心是启动一个 Netty 服务器监听指定端口并处理 WebSocket 连接。文件src/main/java/com/example/gameserver/gateway/NettyServer.javapackage com.example.gameserver.gateway; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; Component Slf4j public class NettyServer { Value(${netty.port:8081}) private int port; private EventLoopGroup bossGroup new NioEventLoopGroup(1); // 接收连接 private EventLoopGroup workerGroup new NioEventLoopGroup(); // 处理连接 PostConstruct public void start() throws InterruptedException { new Thread(() - { try { ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { // 添加HTTP编解码器 ch.pipeline().addLast(new HttpServerCodec()); // 支持大数据流 ch.pipeline().addLast(new ChunkedWriteHandler()); // 聚合HTTP请求/响应的完整内容 ch.pipeline().addLast(new HttpObjectAggregator(65536)); // 处理WebSocket握手和帧 ch.pipeline().addLast(new WebSocketServerProtocolHandler(/ws)); // 自定义游戏消息处理器 ch.pipeline().addLast(new GameMessageHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture future bootstrap.bind(port).sync(); log.info(Netty 网关服务器启动成功端口{}, port); future.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error(Netty 服务器异常, e); Thread.currentThread().interrupt(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }).start(); } PreDestroy public void destroy() { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); log.info(Netty 网关服务器关闭); } }文件src/main/java/com/example/gameserver/gateway/GameMessageHandler.javapackage com.example.gameserver.gateway; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.example.gameserver.common.constant.MessageType; import com.example.gameserver.logic.service.PlayerService; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; Slf4j Component public class GameMessageHandler extends SimpleChannelInboundHandlerTextWebSocketFrame { Autowired private PlayerService playerService; // 通过Spring容器注入逻辑层服务 Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { String text msg.text(); log.info(收到客户端消息: {}, text); try { JSONObject json JSON.parseObject(text); int msgType json.getIntValue(type); JSONObject data json.getJSONObject(data); switch (msgType) { case MessageType.LOGIN: handleLogin(ctx, data); break; case MessageType.GET_GALAXY_LIST: handleGetGalaxyList(ctx, data); break; case MessageType.DISPATCH_FLEET: handleDispatchFleet(ctx, data); break; default: log.warn(未知的消息类型: {}, msgType); sendError(ctx, 未知操作类型); } } catch (Exception e) { log.error(消息处理异常, e); sendError(ctx, 消息格式错误); } } private void handleLogin(ChannelHandlerContext ctx, JSONObject data) { String username data.getString(username); String token data.getString(token); // 简单演示实际需验证 // 调用逻辑层服务验证用户并绑定Channel与玩家ID boolean success playerService.login(username, token, ctx.channel()); JSONObject resp new JSONObject(); resp.put(type, MessageType.LOGIN_RESP); resp.put(data, new JSONObject().fluentPut(success, success)); ctx.writeAndFlush(new TextWebSocketFrame(resp.toJSONString())); } private void handleGetGalaxyList(ChannelHandlerContext ctx, JSONObject data) { // 从逻辑层获取星系列表 JSONObject galaxyList playerService.getGalaxyList(); JSONObject resp new JSONObject(); resp.put(type, MessageType.GET_GALAXY_LIST_RESP); resp.put(data, galaxyList); ctx.writeAndFlush(new TextWebSocketFrame(resp.toJSONString())); } private void handleDispatchFleet(ChannelHandlerContext ctx, JSONObject data) { Long playerId data.getLong(playerId); Long galaxyId data.getLong(galaxyId); // 调用逻辑层处理派遣逻辑 boolean result playerService.dispatchFleet(playerId, galaxyId); JSONObject resp new JSONObject(); resp.put(type, MessageType.DISPATCH_FLEET_RESP); resp.put(data, new JSONObject().fluentPut(success, result)); ctx.writeAndFlush(new TextWebSocketFrame(resp.toJSONString())); } private void sendError(ChannelHandlerContext ctx, String errorMsg) { JSONObject resp new JSONObject(); resp.put(type, MessageType.ERROR); resp.put(data, new JSONObject().fluentPut(msg, errorMsg)); ctx.writeAndFlush(new TextWebSocketFrame(resp.toJSONString())); } Override public void handlerAdded(ChannelHandlerContext ctx) { log.info(客户端连接: {}, ctx.channel().id()); } Override public void handlerRemoved(ChannelHandlerContext ctx) { log.info(客户端断开: {}, ctx.channel().id()); playerService.logout(ctx.channel()); // 清理会话 } }4.3 逻辑层与数据模型消息类型常量// src/main/java/com/example/gameserver/common/constant/MessageType.java package com.example.gameserver.common.constant; public class MessageType { public static final int LOGIN 1001; public static final int LOGIN_RESP 1002; public static final int GET_GALAXY_LIST 2001; public static final int GET_GALAXY_LIST_RESP 2002; public static final int DISPATCH_FLEET 3001; public static final int DISPATCH_FLEET_RESP 3002; public static final int ERROR 9999; }玩家服务实现// src/main/java/com/example/gameserver/logic/service/impl/PlayerServiceImpl.java package com.example.gameserver.logic.service.impl; import com.alibaba.fastjson2.JSONObject; import com.example.gameserver.logic.service.PlayerService; import io.netty.channel.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; Service Slf4j public class PlayerServiceImpl implements PlayerService { // 内存中维护 Channel 与 PlayerId 的映射 (生产环境需考虑分布式) private static final ConcurrentHashMapChannel, Long CHANNEL_PLAYER_MAP new ConcurrentHashMap(); private static final ConcurrentHashMapLong, Channel PLAYER_CHANNEL_MAP new ConcurrentHashMap(); Resource private RedisTemplateString, Object redisTemplate; Override public boolean login(String username, String token, Channel channel) { // 1. 实际项目应从数据库或认证中心验证 token if (!valid_token.equals(token)) { // 示例验证 return false; } // 2. 模拟获取玩家ID Long playerId 10000L username.hashCode() % 1000; // 3. 绑定 Channel CHANNEL_PLAYER_MAP.put(channel, playerId); PLAYER_CHANNEL_MAP.put(playerId, channel); // 4. 将玩家登录状态存入Redis设置过期时间 String key player:online: playerId; redisTemplate.opsForValue().set(key, 1, 30, TimeUnit.MINUTES); log.info(玩家[{}]登录成功绑定Channel: {}, playerId, channel.id()); return true; } Override public void logout(Channel channel) { Long playerId CHANNEL_PLAYER_MAP.remove(channel); if (playerId ! null) { PLAYER_CHANNEL_MAP.remove(playerId); redisTemplate.delete(player:online: playerId); log.info(玩家[{}]登出, playerId); } } Override public JSONObject getGalaxyList() { // 模拟返回星系数据实际应从数据库或配置中心读取 JSONObject data new JSONObject(); data.put(galaxies, new JSONObject[]{ new JSONObject().fluentPut(id, 1).fluentPut(name, 太阳系).fluentPut(status, 和平), new JSONObject().fluentPut(id, 2).fluentPut(name, 半人马座α星).fluentPut(status, 争夺中), new JSONObject().fluentPut(id, 3).fluentPut(name, 仙女座星系).fluentPut(status, 未探索) }); return data; } Override public boolean dispatchFleet(Long playerId, Long galaxyId) { // 1. 验证玩家是否在线 if (!PLAYER_CHANNEL_MAP.containsKey(playerId)) { log.warn(玩家[{}]不在线无法派遣舰队, playerId); return false; } // 2. 验证星系是否存在、是否可派遣 (此处省略复杂逻辑) if (galaxyId 1 || galaxyId 3) { return false; } // 3. 执行派遣逻辑 (更新数据库记录舰队行程) // 模拟一个耗时操作 try { Thread.sleep(50); // 模拟数据库IO或计算 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info(玩家[{}]派遣舰队至星系[{}], playerId, galaxyId); // 4. (可选) 通知相关玩家星系状态变化 // pushMessageToPlayer(affectedPlayerId, message); return true; } }4.4 配置文件与数据库application.yml配置server: port: 8080 # Spring Boot 管理端口可与Netty端口不同 spring: datasource: url: jdbc:mysql://localhost:3306/game_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: # 如果有密码则填写 database: 0 netty: port: 8081 # Netty网关监听端口 logging: level: com.example.gameserver: DEBUGMySQL 表结构示例 (game_db):CREATE TABLE player ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, level int(11) DEFAULT 1, last_login_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE fleet ( id bigint(20) NOT NULL AUTO_INCREMENT, player_id bigint(20) NOT NULL, galaxy_id bigint(20) NOT NULL, dispatch_time datetime NOT NULL, status tinyint(4) DEFAULT 0 COMMENT 0:航行中,1:抵达,2:返回, PRIMARY KEY (id), KEY idx_player_id (player_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;4.5 运行与验证启动基础设施确保 MySQL 和 Redis 服务已启动并正确配置。启动应用运行GameServerApplication的 main 方法。控制台应看到 Spring Boot 和 Netty 启动成功的日志。使用 WebSocket 测试工具可以使用Postman(New - WebSocket Request) 或编写一个简单的 HTML/JS 测试页面。连接地址ws://localhost:8081/ws发送登录消息{type: 1001, data: {username: testUser, token: valid_token}}接收响应应类似{type: 1002, data: {success: true}}请求星系列表{type: 2001, data: {}}派遣舰队{type: 3001, data: {playerId: 10001, galaxyId: 2}}5. 常见问题与排查思路在开发和高并发测试中你可能会遇到以下典型问题问题现象可能原因排查思路与解决方案客户端无法连接到网关1. Netty 服务未启动或端口被占用。2. 防火墙阻止了端口。3. 客户端连接地址或协议错误。1. 检查应用日志确认 Netty 启动成功。2. 使用netstat -an | grep 8081(Linux) 或netstat -ano | findstr 8081(Windows) 查看端口状态。3. 确认客户端使用ws://协议及正确的路径/ws。登录成功但收不到游戏广播1. Channel 映射丢失。2. 广播逻辑有误未找到目标 Channel。3. 消息序列化/反序列化出错。1. 检查CHANNEL_PLAYER_MAP和PLAYER_CHANNEL_MAP在登录/登出时的维护逻辑。2. 在广播代码处添加详细日志打印目标 PlayerId 和找到的 Channel。3. 检查发送的消息格式是否为TextWebSocketFrame。高并发下出现消息乱序或丢失1. Netty 的EventLoopGroup线程数配置不合理。2. 业务逻辑处理耗时过长阻塞了EventLoop线程。3. 未做消息队列和流量控制。1. 根据 CPU 核心数调整workerGroup线程数。2.绝对禁止在ChannelHandler的channelRead0中执行耗时 IO 操作。应将耗时任务提交到独立的业务线程池。3. 引入消息队列如 Disruptor或使用 Netty 的DefaultEventExecutorGroup隔离业务逻辑。服务器内存持续增长 (内存泄漏)1.ChannelHandlerContext或Channel未被正确释放。2. 静态 Map (CHANNEL_PLAYER_MAP) 中的对象未及时清理。3. Redis 连接未关闭。1. 确保handlerRemoved或channelInactive方法被调用并清理资源。2. 实现心跳机制定期清理失效的连接。3. 使用Resource注入的RedisTemplate由 Spring 管理通常无需手动关闭但要检查连接池配置。数据库连接池耗尽1. 业务逻辑中数据库操作未及时关闭连接。2. 连接池最大连接数设置过小。3. 存在慢 SQL 导致连接占用时间过长。1. 确保使用 MyBatis 时SqlSession 被正确关闭推荐用try-with-resources或 Spring 事务管理。2. 在application.yml中调大spring.datasource.hikari.maximum-pool-size。3. 分析并优化慢 SQL为频繁查询的字段添加索引。Redis 响应变慢1. Redis 内存不足触发淘汰策略或使用交换分区。2. 存在大 Key 或热 Key。3. 网络延迟或 Redis 本身负载过高。1. 使用redis-cli info memory查看内存使用情况。2. 使用redis-cli --bigkeys扫描大 Key优化数据结构如将大 Hash 拆分为多个小 Key。3. 考虑使用 Redis 集群或读写分离。6. 最佳实践与工程建议将服务器原型投入生产环境需要考虑更多工程化因素以确保“平衡性”和稳定性。6.1 网关层优化心跳与断线重连实现客户端-服务器心跳包机制及时检测死连接并清理资源。规定客户端在超时未收到心跳响应时应主动重连。协议优化对于高频小消息可以考虑使用自定义二进制协议如 Protobuf替代 JSON以减少序列化开销和网络流量。限流与防刷在网关层针对 IP 或玩家 ID 进行请求频率限制防止恶意刷请求压垮服务器。分布式网关单机网关有性能上限。需要引入Nginx或LVS进行 TCP/WebSocket 负载均衡将连接分散到多个网关实例。此时CHANNEL_PLAYER_MAP需改用 Redis 等外部存储来实现共享。6.2 逻辑层设计线程模型清晰严格区分 Netty 的 I/O 线程和业务逻辑线程。所有涉及数据库、Redis、RPC 调用的操作必须提交到业务线程池避免阻塞EventLoop。服务拆分与微服务化随着功能增长应将不同的游戏系统如战斗、聊天、邮件、商城拆分为独立的微服务。服务间通过 RPC如 gRPC, Dubbo或消息队列如 Kafka, RocketMQ通信。事件驱动架构使用事件总线如 Spring Event解耦系统模块。例如“舰队抵达”事件可以触发“资源更新”、“任务检查”、“通知推送”等多个监听器提高可维护性。6.3 数据一致性保障数据库事务对于涉及多个表更新的操作如购买物品扣钱加物品必须使用Transactional保证原子性。缓存更新策略经典的Cache-Aside模式。先读缓存缓存没有则读库并写入缓存。更新时先更新数据库再删除缓存而非更新缓存以避免并发下的数据不一致。对于极端一致性要求可考虑使用分布式锁。异步持久化非关键数据如玩家操作日志可以采用异步方式写入数据库先存入内存队列再由后台线程批量入库提升主逻辑响应速度。6.4 监控与运维全链路日志使用SLF4J Logback记录结构化日志并集成ELK(Elasticsearch, Logstash, Kibana) 或Graylog进行日志收集和查询。为每个请求分配唯一的traceId便于追踪。指标监控集成Micrometer暴露 JVM 指标、业务指标如在线人数、请求QPS、平均耗时并推送至Prometheus用Grafana制作仪表盘。健康检查与优雅上下线提供/actuator/health端点供负载均衡器检查。在服务关闭时PreDestroy应先拒绝新请求等待处理中的请求完成再释放资源实现优雅下线。6.5 安全考虑通信安全生产环境务必使用WSS(WebSocket Secure) 即wss://对传输内容进行加密。数据验证对所有客户端传入的数据进行严格验证包括类型、范围、业务逻辑合法性防止恶意数据导致程序异常或逻辑漏洞。防作弊关键逻辑如战斗计算、资源产出必须在服务器端执行客户端仅负责展示。定期校验客户端状态与服务器状态是否一致。构建一个平衡、稳定、可扩展的游戏服务器是一个系统工程需要从架构设计、代码实现到运维监控全方位考虑。本文提供了一个从零开始的实战起点涵盖了核心的网关、逻辑与数据交互。真正的挑战在于随着玩家数量和功能复杂度的增长如何持续优化和迭代这套架构。建议从单机高性能模型开始逐步引入分布式组件并始终将数据一致性、服务可用性和玩家体验公平性放在首位。