first commit
0
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/README.md
Normal file
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/1559293031316.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/hint001.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/image071.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps1.jpg
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps10.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps11.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps12.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps13.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps14.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps15.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps16.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps17.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps18.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps19.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps2.jpg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps20.jpg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps21.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps22.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps23.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps24.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps25.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps3.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps30.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps5.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps6.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps7.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/images/wps9.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
60
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第1节:框架.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
front: https://mc.res.netease.com/pc/zt/20201109161633/mc-dev/assets/img/1559293031316.8e6c38fc.png
|
||||
hard: 入门
|
||||
time: 10分钟
|
||||
---
|
||||
|
||||
# 框架
|
||||
## 框架介绍
|
||||

|
||||
|
||||
|
||||
- DB是全局存储系统,所有游戏服共享,可以是redis、mysql或mongodb等。其中,redis用于缓存临时数据,比如玩家在线状态、当日在线时长等,mysql和mongo用于持久化存储游戏数据。开发者根据需求自选。
|
||||
- proxy是代理服,功能包括消息的加密和解密,消息的压缩和解压缩,登录认证和消息转发。它保持客户端到服务端的连接。 开发者不能对proxy进行开发。
|
||||
- game是游戏服,提供游戏逻辑功能,一个在线玩家只存在于一个game或lobby中。 开发者在game上进行游戏玩法开发,比如实现酷跑游戏、射击游戏、战斗游戏。
|
||||
- lobby是大厅服,提供大厅各项功能。 开发者在lobby开发大厅的功能,比如提供NPC选服入口,提供战斗副本入口。
|
||||
- master是控制服,用于管理其他服,是全服单点,对外提供http服务。http服务是运营指令(gm指令)入口。 开发者可以在master开发运营指令,比如发奖励指令、禁止发言指令。下面通过禁言指令需求介绍master功能:
|
||||
- 需求:某玩家言词不当,需要禁止他聊天。
|
||||
- 实现:在master添加禁言指令。开发者使用http给master发送禁言请求,master会把禁言信息记录到db,然后给玩家所在服务器发消息,禁止玩家发言。玩家下次登陆时,从db中读取禁言信息,判断是否还可以聊天。
|
||||
- service是功能服,用于提供分布式单点服务。开发者可以在service开发公会、全服boss、全服匹配等功能。下面通过全服匹配需求介绍service功能:
|
||||
- 需求:存在lobby1和lobby2两个大厅服,两个大厅服内玩家要按照等级、战力等属性进行匹配,进入同一个副本游戏。
|
||||
- 实现:玩家申请匹配时,lobby1或lobby2向service申请匹配,service维护一个匹配队列,记录所有正匹配玩家,service会定时取出队列玩家,按照等级和战力匹法,将匹配后玩家分配到指定副本游戏。
|
||||
|
||||
## 使用示例
|
||||
通过一个简单网络游戏需求介绍开服工具框架的使用。可在McStudio——基岩版网络服分页,选择“简易网络服”模板,点击新建按钮创建该示例。
|
||||
### 需求
|
||||
玩家进入大厅服后,选择体验三种游戏:生存服,“钻石服”,对战pvp,另外游戏提供禁言指令。对战pvp要求一场战斗最多两个玩家。
|
||||
### 实现
|
||||
|
||||
下面介绍一下简易服的功能:
|
||||
|
||||
- masterMod:实现了一个获取玩家在线状态的运营指令
|
||||
|
||||
- serviceMod:实现全服匹配。service维护匹配队列,记录所有正在匹配玩家,接着按照玩家等级匹配,将匹配成功的两个玩家分配到pvp服,
|
||||
|
||||
- AwesomeGameMod:实现了一个基础的生存服
|
||||
|
||||
- TutorialGameMod:玩家在聊天框里面输入"钻石剑","钻石镐","钻石头盔","钻石胸甲","钻石护腿","钻石靴子"会获得相应的装备
|
||||
|
||||
- OrdinaryGameMod:简单的对战pvp
|
||||
|
||||
- lobbyMod:提供三个NPC,点击不同NPC分别进入不同的game服
|
||||
|
||||
|
||||
### 功能执行过程
|
||||
|
||||
说明玩家体验游戏过程中,引擎(开服工具框架)和开发者mod分别完成的功能。
|
||||
|
||||
#### 进入钻石服
|
||||
1. 玩家登陆进入lobby:引擎会将玩家分配到lobby。
|
||||
2. 玩家点击NPC-B,玩家切服到的TutorialGameMod:lobbyMod实现NPC和切服功能
|
||||
3. 玩家在聊天框输入"钻石剑",会获得一把钻石剑
|
||||
4. 点击回城NPC,玩家可以回到lobby
|
||||
#### 进入对战pvp
|
||||
1. 玩家A和玩家B登陆到lobby:引擎会将玩家分配到对应lobby
|
||||
2. 玩家A和玩家B点击NPC-C申请匹配:开发者lobbyMod向service申请匹配,serviceMod完成匹配,将玩家分配到OrdinaryGameMod
|
||||
3. 点击回城NPC,玩家可以回到lobby
|
||||
#### 进入生存服
|
||||
1. 玩家登陆进入lobby:引擎会将玩家分配到lobby。
|
||||
2. 玩家点击NPC-A,玩家切服到的AwesomeGameMod:lobbyMod实现NPC和切服功能
|
||||
3. 点击回城NPC,玩家可以回到lobby
|
||||
151
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第2节:服务器Mod目录.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
front:
|
||||
hard: 入门
|
||||
time: 10分钟
|
||||
---
|
||||
|
||||
# 服务器Mod目录
|
||||
|
||||
## Mod类型
|
||||
|
||||
服务器Mods通常由控制服Mod,功能服Mod,大厅服Mod与游戏服Mod等组成,各种类型的Mod概念如下:
|
||||
|
||||
- 控制服Mod:包含控制服(master)的developer_mods目录下所有mod。
|
||||
|
||||
- 功能服Mod:包含功能服(service)的developer_mods目录下所有mod。
|
||||
|
||||
- 大厅服Mod:包含大厅服的 behavior_packs、resource_packs、developer_mods目录下所有的mod以及worlds(地图存档)。
|
||||
|
||||
- 游戏服Mod:包含游戏服的 behavior_packs、resource_packs、developer_mods目录下所有的mod以及worlds(地图存档)。
|
||||
|
||||
一个游戏服Mod的**标准目录格式**如下:
|
||||
|
||||

|
||||
|
||||
## developer_mods
|
||||
|
||||
服务端加载的mod目录,不会被传送到客户端。
|
||||
下面以neteaseAnnounce为例介绍其目录结构:
|
||||
|
||||
neteaseAnnounceDev
|
||||
netease_require.json
|
||||
neteaseAnnounceScript
|
||||
__init__.py
|
||||
announceConsts.py
|
||||
announceServerSystem.py
|
||||
modMain.py
|
||||
timermanager.py
|
||||
mod.json
|
||||
|
||||
|文件/文件夹|解释|
|
||||
|--------|----------|
|
||||
|neteaseAnnounceDev| 顶层neteaseAnnounceDev,表示mod的名字;第二级neteaseAnnounceScript表示行为包的目录,开发者从从该目录开始import module,比如import neteaseAnnounceScript.announceConsts as announceConsts |
|
||||
|`__init__.py`| 是python module的标识,表示这是一个可以import的module,同时也可以做一些初始化的操作,内容可为空,但是文件必须有。 |
|
||||
|announceConsts.py| mod中的一些宏定义|
|
||||
|announceServerSystem.py|mod业务逻辑|
|
||||
|modMain.py|该文件名称不可以更改,用来初始化我们的Mod,具体使用参考<a href="../../20-玩法开发/13-模组SDK编程/2-Python脚本开发/0-脚本开发入门.html#modmain-py是什么" target="_blank">mod开发简介</a>|
|
||||
|timermanager.py|实现了一个定时器,提供了一次性定时器和循环定时器(当前ModSDK中已经有实现相同功能的组件,不再需要自己实现)|
|
||||
|netease_require.json| 用于控制服务器Mod加载顺序的配置文件,非必须,具体内容见《1-9 控制服务器Mod加载顺序的办法》|
|
||||
|mod.json| 用于配置mod的具体功能实现细节行为的配置文件,非必须|
|
||||
|
||||
developer_mods和behavior_packs区别:
|
||||
|
||||
- developer_mods控制服务端行为,behavior_packs控制客户端行为。
|
||||
- behavior_packs会下载给客户端,developer_mods不会。
|
||||
- developer_mods可以使用Server Mod SDK全部接口和MOD SDK中服务端相关接口,behavior_packs使用MOD SDK中客户端相关接口。
|
||||
- behavior_packs必须包含manifest.json文件,且需要在地图目录下world_behavior_packs.json文件中配置pack id和version。
|
||||
|
||||
developer_mods支持多个mod,每个mod对应一个目录,下面是neteaseAnnounce和neteaseAlert两个mod的目录结构:
|
||||
|
||||
neteaseAnnounceDev
|
||||
netease_require.json
|
||||
neteaseAnnounceScript
|
||||
__init__.py
|
||||
announceConsts.py
|
||||
announceServerSystem.py
|
||||
modMain.py
|
||||
timermanager.py
|
||||
mod.json
|
||||
neteaseAlertDev
|
||||
neteaseAlertScript
|
||||
__init__.py
|
||||
alertConst.py
|
||||
alertServerSystem.py
|
||||
modMain.py
|
||||
mod.json
|
||||
|
||||
## resource_packs
|
||||
- 存放客户端资源
|
||||
|
||||
- 资源版本信息存放在manifest.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 1,
|
||||
"header": {
|
||||
"description": "By tnm",
|
||||
"name": "tnm_glove_pve",
|
||||
"uuid": "1c850d23-64f4-46be-aec0-16c8e4618072",
|
||||
"version": [0, 0, 1]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"description": "By tnm",
|
||||
"type": "resources",
|
||||
"uuid": "637ff742-7003-4a8b-8d99-722a1b704f12",
|
||||
"version": [0, 0, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
- 并非所有resource_packs都会被客户端下载
|
||||
|
||||
- 需要将`manifest.json`配置header中uuid配置到worlds/level/`world_resource_packs.json`中才会被客户端下载
|
||||
|
||||
- `world_resource_packs.json`内容如下:
|
||||
```python
|
||||
[
|
||||
{
|
||||
"pack_id" : "1c850d23-64f4-46be-aec0-16c8e4618072",
|
||||
"version" : [ 0, 0, 1 ]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## behavior_packs
|
||||
- 存放客户端MOD
|
||||
- manifest.json& world_behavior_packs.json配置与resource_packs类似
|
||||
- behavior_packs里只允许有一个目录,如果有多个,则只有第一个会被使用,请按*behavior格式命名,大小写不敏感
|
||||
- `*behavior`目录内脚本需要存放在*scripts目录内,且只允许有一个
|
||||
- `*behavior`目录内需包含版本信息配置`manifest.json`
|
||||
|
||||
## worlds
|
||||
- 只允许有一个子目录,如demo中的level
|
||||
- 目录名作为地图名称,不允许使用中文
|
||||
- 不要忘了配置`world_behavior_packs.json`等两个json
|
||||
|
||||
一个示例的目录结构如下:
|
||||
|
||||
worlds
|
||||
level
|
||||
db
|
||||
level.dat
|
||||
levelname.txt
|
||||
world_behavior_packs.json
|
||||
world_resource_packs.json
|
||||
|
||||
|文件/文件夹|解释|
|
||||
|--------|----------|
|
||||
|worlds| 存放服务器地图目录,名字不可以更改|
|
||||
|level| 存放一个地图存档目录,目录名也就是地图名|
|
||||
| db| 地图存档目录|
|
||||
| level.dat| 存储关于地图的全局信息|
|
||||
|levelname.txt|地图的名字|
|
||||
|world_behavior_packs.json|配置客户端需要下载的behavior mods|
|
||||
|world_resource_packs.json|配置客户端需要下载的resource mods|
|
||||
375
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第3节:进入与退出游戏.md
Normal file
@@ -0,0 +1,375 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 进入和退出游戏
|
||||
|
||||
主要包含登录、定时存档、登出、切服等功能。
|
||||
|
||||
## 登录
|
||||
|
||||
Apollo引擎在登录过程中会处理顶号问题,开发者开发过程不用考虑顶号。登录过程主要涉及下面事件:
|
||||
|
||||
- client 相关事件:
|
||||
- OnUIInitFinished:此时可以创建UI了,但是玩家每次切维度都会触发这个事件
|
||||
- OnLocalPlayerStopLoading:出生点地形加载完成时触发,切维度不会触发本事件
|
||||
|
||||
- master相关事件:
|
||||
- PlayerLoginServerEvent:登录master事件,可以区分登录和切服
|
||||
- lobby/game相关事件:
|
||||
- AddServerPlayerEvent:登录到lobby/game 事件,可以区分登录和切服
|
||||
|
||||
### 服务端登录开发
|
||||
|
||||
下面开发LobbyMod服务端登录功能。首先处理登录逻辑,监听AddServerPlayerEvent事件,初始化玩家信息,核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ServerSystem.__init__(self, namespace, systemName)
|
||||
#注册事件
|
||||
self.ListenForEvent(
|
||||
serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
modConfig.AddServerPlayerEvent,
|
||||
self, self.OnAddServerPlayer
|
||||
)
|
||||
...
|
||||
|
||||
def OnAddServerPlayer(self, args):
|
||||
'''
|
||||
添加玩家的监听函数
|
||||
'''
|
||||
playerId = args.get('id','-1')
|
||||
uid = netgameApi.GetPlayerUid(playerId)
|
||||
self.mPlayerid2uid[playerId] = uid
|
||||
|
||||
```
|
||||
### 读写数据库
|
||||
|
||||
玩家游戏数据通常存储到mysql。apollo应该尽量异步读写mysql,避免阻塞游戏逻辑,导致全服玩家卡顿。apollo提供“mysql连接池”,提供各种异步读写mysql接口。
|
||||
|
||||
我们以LobbyMod为例介绍“mysql连接池”使用方法。首先我们创建表playerCol,用于存储玩家数据,创表语句如下:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `playerCol` (
|
||||
`uid` bigint unsigned NOT NULL,
|
||||
`nickname` varchar(50) NOT NULL,
|
||||
`login_time` bigint unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
接着,我们读写表playerCol,玩家登录时从mysql读取玩家数据,若没查到数据,则向mysql插入玩家信息,记录新玩家信息。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
...
|
||||
self.mysqlMgr = MysqlOperation()
|
||||
|
||||
def OnAddServerPlayer(self, args):
|
||||
'''
|
||||
添加玩家的监听函数
|
||||
'''
|
||||
...
|
||||
self.mysqlMgr.QueryPlayerData(playerId,uid,lambda data: self.QuerySinglePlayerCallback(playerId, uid, data))
|
||||
|
||||
def QuerySinglePlayerCallback(self, player_id, uid, data):
|
||||
'''
|
||||
回调函数。若玩家存在,则注册玩家;否则记录玩家信息
|
||||
'''
|
||||
# 数据库请求返回时,玩家已经主动退出了
|
||||
if not self.playerid2uid.has_key(player_id):
|
||||
return
|
||||
if not data:# 找不到玩家数据,注册一个新玩家
|
||||
nickname = netgameApi.GetPlayerNickname(player_id)
|
||||
data = playerData.PlayerData.getNewPlayerInfo(uid, nickname)
|
||||
self.InsertPlayerData(player_id, uid)
|
||||
#记录玩家数据
|
||||
player = playerData.PlayerData()
|
||||
if isinstance(data,tuple):
|
||||
data = player.changeMysqlTupleToPlayerDict(data)
|
||||
player.initPlayer(player_id, data)
|
||||
#刷新玩家登录时间
|
||||
player.refreshLoginTime()
|
||||
self.player_map[uid] = player
|
||||
|
||||
```
|
||||
|
||||
### 同步玩家数据
|
||||
玩家登录后,需要从服务端获取玩家信息。我们以LobbyMod为例,客户端登录后,从服务端获取玩家登录时间和昵称。
|
||||
|
||||
#### 客户端登录开发
|
||||
|
||||
监听OnOnLocalPlayerStopLoading事件,请求获取玩家数据,接着监听服务端发过来的LoginResponseEvent事件,然后记录玩家数据。客户端监听UiInitFinished事件,开始初始化UI。代码如下:
|
||||
```python
|
||||
class AwesomeClient(ClientSystem):
|
||||
def __init__(self,namespace,systemName):
|
||||
ClientSystem.__init__(self,namespace,systemName)
|
||||
...
|
||||
# 注册事件
|
||||
self.ListenForEvent(
|
||||
modConfig.Minecraft,
|
||||
modConfig.LobbyServerSystemName,
|
||||
modConfig.LoginResponseEvent,
|
||||
self, self.OnLoginResponse
|
||||
)
|
||||
self.ListenForEvent(
|
||||
clientApi.GetEngineNamespace(),
|
||||
clientApi.GetEngineSystemName(),
|
||||
‘OnLocalPlayerStopLoading’,
|
||||
self, self.OnOnLocalPlayerStopLoading
|
||||
)
|
||||
self.ListenForEvent(
|
||||
clientApi.GetEngineNamespace(),
|
||||
clientApi.GetEngineSystemName(),
|
||||
'UiInitFinished',
|
||||
self, self.OnUiInitFinished)
|
||||
self.mMyPlayerData = None
|
||||
|
||||
def OnUiInitFinished(self, args):
|
||||
'''
|
||||
初始化UI
|
||||
'''
|
||||
print 'OnUiInitFinished', args
|
||||
self.InitUi()
|
||||
|
||||
def OnOnLocalPlayerStopLoading(self,args):
|
||||
'''
|
||||
请求登录到服务端,获取玩家数据
|
||||
'''
|
||||
logger.info("OnOnLocalPlayerStopLoading : %s", args)
|
||||
playerId = clientApi.GetLocalPlayerId()
|
||||
loginData = {}
|
||||
loginData['id'] = playerId
|
||||
self.NotifyToServer(modConfig.LoginRequestEvent, loginData)
|
||||
|
||||
def OnLoginResponse(self, args):
|
||||
'''
|
||||
初始化玩家数据,然后开始客户端逻辑
|
||||
'''
|
||||
logger.info("OnLoginResponse : %s", args)
|
||||
player_info = args
|
||||
self.mMyPlayerData = playerData.PlayerData()
|
||||
self.mMyPlayerData.initPlayer(player_info['player_id'], player_info)
|
||||
self.InitUi()
|
||||
```
|
||||
#### 服务端登录开发
|
||||
监听客户端自定义LoginRequestEvent事件,设置玩家出生点和维度,将玩家数据返回给客户端。服务端从mysql中读取玩家数据是个异步过程,收到LoginRequestEvent事件后可能还没初始化玩家信息,因此需要延迟推送。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
...
|
||||
self.ListenForEvent(modConfig.Minecraft, modConfig.LobbyClientSystemName,
|
||||
modConfig.LoginRequestEvent, self, self.OnLoginRequest)
|
||||
|
||||
def OnLoginRequest(self, data):
|
||||
'''
|
||||
玩家登录逻辑
|
||||
'''
|
||||
player_id = data['id']
|
||||
uid = netgameApi.GetPlayerUid(player_id)
|
||||
# 设置玩家位置和维度
|
||||
comp = serverApi.GetEngineCompFactory().CreateDimension(player_id)
|
||||
comp.ChangePlayerDimension(4, (1395.664, 5.2, 51.441))
|
||||
CoroutineMgr.StartCoroutine(self._DoSendLoginResponseData(player_id, uid))
|
||||
|
||||
def _DoSendLoginResponseData(self, player_id, uid):
|
||||
'''
|
||||
将玩家数据推送给客户端。若还没从db获取玩家数据,则延迟5帧再试
|
||||
'''
|
||||
if uid in self.player_map:
|
||||
player = self.player_map[uid]
|
||||
event_data = player.toSaveDict()
|
||||
event_data['player_id'] = player_id
|
||||
self.NotifyToClient(player_id, modConfig.LoginResponseEvent, event_data)
|
||||
return
|
||||
yield -5
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 验证功能
|
||||
|
||||
用MCStudio进入游戏,查看相关信息:
|
||||
|
||||
- 登录到linux开发机,切到lobby的logs目录,可以看到OnAddServerPlayer方法打印的登录日志:
|
||||

|
||||
|
||||
- 进入mysql,可以查看到玩家数据:
|
||||

|
||||
|
||||
- MCStudio中查看玩家登录日志,也即OnLoginResponse方法打印的日志:
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
## 定时存档
|
||||
|
||||
通过SetUseDatabaseSave函数打开定时存档功能,引擎会定时触发savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。mod监听这两个事件,然后执行存档逻辑。定时存档把存档从游戏逻辑中解耦出来,让开发者集中于游戏逻辑的开发。
|
||||
|
||||
### 服务端mod开发
|
||||
|
||||
设置定时存档,然后监听savePlayerDataEvent/savePlayerDataOnShutDownEvent事件。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
...
|
||||
netgameApi.SetUseDatabaseSave(True, "awesome", 120)#定时存档,时间间隔是120s
|
||||
netgameApi.SetNonePlayerSaveMode(True)
|
||||
|
||||
self.ListenForEvent(
|
||||
serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
'savePlayerDataEvent',
|
||||
self, self.OnSavePlayerData
|
||||
)
|
||||
self.ListenForEvent(
|
||||
serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
'savePlayerDataOnShutDownEvent',
|
||||
self, self.OnSavePlayerData
|
||||
)
|
||||
...
|
||||
def OnSavePlayerData(self, args):
|
||||
'''
|
||||
把玩家数据存档。这个函数一定要调用save_player_data_result函数,把存档状态告知引擎。
|
||||
'''
|
||||
uid = int(args["playerKey"])
|
||||
cpp_callback_idx = int(args["idx"])
|
||||
player_data = self.mPlayerMap.get(uid, None)
|
||||
if not player_data:
|
||||
#告知引擎,存档状态。注意传入回调函数id
|
||||
netgameApi.SavePlayerDataResult(cpp_callback_idx, True)
|
||||
def _SavePlayerCb(args):
|
||||
uid, ret = args
|
||||
if ret:
|
||||
netgameApi.SavePlayerDataResult(cpp_callback_idx, True)
|
||||
else:
|
||||
netgameApi.SavePlayerDataResult(cpp_callback_idx, False)
|
||||
self.SavePlayerByUid(uid, _SavePlayerCb)
|
||||
|
||||
def SavePlayerByUid(self, uid, cb = None):
|
||||
'''
|
||||
保存玩家数据
|
||||
'''
|
||||
player = self.mPlayerMap.get(uid, None)
|
||||
if not player:
|
||||
return
|
||||
player_dict = player.toSaveDict()
|
||||
if self.mDBType == DbType.Mongo:
|
||||
self.mMongoMgr.SavePlayerByUid(uid,player_dict,cb)
|
||||
elif self.mDBType == DbType.Mysql:
|
||||
self.mMysqlMgr.SavePlayerByUid(uid,player_dict,cb)
|
||||
```
|
||||
|
||||
### 验证功能
|
||||
|
||||
- 用MCStudio进入游戏,在游戏停留2min,然后在db中查看玩家数据,发现login_time发生了变化。
|
||||
|
||||
- 开发者也可以在OnSavePlayerData函数中添加额外日志,接着用MCStudio进入游戏不退出,然后查看 lobby服日志,可以发现lobby会定时打印对应日志。
|
||||
|
||||
### 总结
|
||||
|
||||
- 通过SetUseDatabaseSave函数开启定时存档。
|
||||
|
||||
- 监听savePlayerDataEvent/savePlayerDataOnShutDownEvent事件,处理存档逻辑。
|
||||
|
||||
## 登出
|
||||
|
||||
### 登出过程介绍
|
||||
|
||||
登录过程主要涉及下面事件:
|
||||
|
||||
- master相关事件:
|
||||
- PlayerLogoutServerEvent:玩家退出或切服
|
||||
- lobby/game 相关事件:
|
||||
- DelServerPlayerEvent:玩家退出或切服,可以处理玩家登出逻辑
|
||||
|
||||
下面介绍LobbyMod登出逻辑的开发。
|
||||
|
||||
### 服务端登出逻辑。
|
||||
|
||||
监听DelServerPlayerEvent事件,把玩家数据存档,并清除玩家数据。代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
...
|
||||
self.ListenForEvent(
|
||||
serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
'DelServerPlayerEvent',
|
||||
self, self.OnDelServerPlayer
|
||||
)
|
||||
|
||||
def OnDelServerPlayer(self, args):
|
||||
'''
|
||||
清除玩家内存数据。
|
||||
'''
|
||||
player_id = args.get('id','-1')
|
||||
logout.info("OnDelServerPlayer player id=%s"% player_id)
|
||||
uid = self.mPlayerid2uid.get(player_id, None)
|
||||
if not uid:
|
||||
return
|
||||
self.SavePlayerByUid(uid)
|
||||
del self.mPlayerid2uid[player_id]
|
||||
if uid in self.mPlayerMap:
|
||||
del self.mPlayerMap[uid]
|
||||
if uid in self.mUid2dimension:
|
||||
del self.mUid2dimension[uid]
|
||||
```
|
||||
### 功能验证
|
||||
|
||||
用MCStudio进入游戏后立马退出。打开lobby日志,可以查看到登出日志:
|
||||
|
||||

|
||||
|
||||
### 总结
|
||||
|
||||
- 监听OnDelServerPlayer事件将玩家数据存档,并清除脚本层中玩家内存数据。
|
||||
|
||||
## 切服
|
||||
|
||||
玩家切服是从一个服务器退出,然后再登录到指定服务器过程,实质是退出游戏然后再进入游戏过程。目前,登入事件(AddServerPlayerEvent)是可以区分登录和切服,登出事件(DelServerPlayerEvent)也可以区分切服过程中登出还是退出游戏。
|
||||
|
||||
## 服务器关服
|
||||
|
||||
服务器关服前会触发ServerWillShutDownEvent事件,开发者可以在该事件中处理存档和清理现场的逻辑。
|
||||
|
||||
AwesomeGame服务端在退出时,需要保存所有在线玩家数据。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
...
|
||||
self.ListenForEvent(
|
||||
serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
'ServerWillShutDownEvent',
|
||||
self, self.OnServerWillShutDown
|
||||
)
|
||||
...
|
||||
|
||||
def OnServerWillShutDown(self, args):
|
||||
# 即将关机,先给所有还在线玩家挂一个存档任务
|
||||
for uid, player in self.mPlayerMap.iteritems():
|
||||
self.SavePlayerByUid(uid)
|
||||
# 同步完成所有还挂着的异步数据库操作
|
||||
if self.mDBType == DbType.Mongo:
|
||||
self.mMongoMgr.Destroy()
|
||||
elif self.mDBType == DbType.Mysql:
|
||||
self.mMysqlMgr.Destroy()
|
||||
```
|
||||
总结:
|
||||
|
||||
- 监听OnServerWillShutDown事件,清理服务端现场。
|
||||
|
||||
|
||||
|
||||
409
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第4节:游戏玩法.md
Normal file
@@ -0,0 +1,409 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 游戏玩法
|
||||
|
||||
这里介绍简易网络服模板的玩法实现,包括NPC以及匹配两块内容。
|
||||
|
||||
简易网络服模板的大厅服中有三个NPC,玩家点击NPC可以实现切服功能。点击NPC-A显示GameA在线人数,可以跳转到包含AwesomeGameMod的GameA,GameA实现的是常规的生存服。点击NPC-B可以显示GameB在线人数,可以跳转到包含TutorialGameMod的GameB,在GameB的聊天栏中输入“钻石剑”,可在背包中获得钻石剑*1。点击NPC-C可以实现简单匹配,当匹配中玩家≥2人时,将这些玩家传入gameC服。在每个Game服中,都有一个返回大厅NPC。
|
||||
|
||||
|
||||
|
||||
## NPC
|
||||
|
||||
### NPC实现
|
||||
|
||||
npc由npc插件实现,只需要配置插件里的mod.json,即可生成对应npc。具体配置如下:
|
||||
```python
|
||||
{
|
||||
"_comment":"mod的名字",
|
||||
"netgame_mod_name":"neteaseNpcDev",
|
||||
|
||||
"_comment":"mod的版本号",
|
||||
"netgame_mod_version":"1.0.4",
|
||||
|
||||
"_comment":"适用最低的引擎版本",
|
||||
"min_app_version":"1.15.0.release20191226",
|
||||
|
||||
"_comment":"该服务器Mod隶属于“功能NPC”插件",
|
||||
"description":"Npc mod插件",
|
||||
|
||||
"_comment":"适用的服务器类型",
|
||||
"support_server_type":["lobby","game"],
|
||||
|
||||
"_comment":"作者名字",
|
||||
"author": "NetEase",
|
||||
|
||||
"_comment":"NPC类型参数配置",
|
||||
"NPCS_TYPE": {
|
||||
"_comment":"typeId,和NPCS_DISTRIBUTE中的typeId对应",
|
||||
"1001":{
|
||||
"name": "NPC-A",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "Minecraft",
|
||||
"systemName": "AwesomeLobby",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["gameA"]
|
||||
},
|
||||
"1002":{
|
||||
"name": "NPC-B",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "Minecraft",
|
||||
"systemName": "AwesomeLobby",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["gameB"]
|
||||
},
|
||||
"1003":{
|
||||
"name": "NPC-C",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "Minecraft",
|
||||
"systemName": "AwesomeLobby",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["gameC"]
|
||||
},
|
||||
"1004":{
|
||||
"name": "返回大厅NPC",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "AwesomeGame",
|
||||
"systemName": "FpsServerSystem",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["lobby"]
|
||||
},
|
||||
"1005":{
|
||||
"name": "返回大厅NPC",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "Minecraft",
|
||||
"systemName": "TutorialGame",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["lobby"]
|
||||
},
|
||||
"1006":{
|
||||
"name": "返回大厅NPC",
|
||||
"identifier": "minecraft:npc",
|
||||
"simpleStyle": false,
|
||||
"modName": "Minecraft",
|
||||
"systemName": "gameMod",
|
||||
"funcName": "OnNpcTouched",
|
||||
"funcArgs": ["lobby"]
|
||||
}
|
||||
},
|
||||
"_comment":"NPC分布列表",
|
||||
"NPCS_DISTRIBUTE": [
|
||||
{
|
||||
"typeId" : "1001",
|
||||
"server" : "lobby",
|
||||
"pos" : [1396, 4, 57],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 4
|
||||
},
|
||||
{
|
||||
"typeId" : "1002",
|
||||
"server" : "lobby",
|
||||
"pos" : [1403, 4, 57],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 4
|
||||
},
|
||||
{
|
||||
"typeId" : "1003",
|
||||
"server" : "lobby",
|
||||
"pos" : [1410, 4, 57],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 4
|
||||
},
|
||||
{
|
||||
"typeId" : "1004",
|
||||
"server" : "gameA",
|
||||
"pos" : [5, 4, 5],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 0
|
||||
},
|
||||
{
|
||||
"typeId" : "1005",
|
||||
"server" : "gameB",
|
||||
"pos" : [5, 4, 5],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 0
|
||||
},
|
||||
{
|
||||
"typeId" : "1006",
|
||||
"server" : "gameC",
|
||||
"pos" : [5, 4, 5],
|
||||
"orientations" : [0, 180],
|
||||
"dimensionId" : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
### 功能验证
|
||||
|
||||
用MCStudio进入游戏,可以看到玩家前方有三个NPC:
|
||||

|
||||
|
||||
|
||||
|
||||
### NPC插件总结:
|
||||
|
||||
- 创建NPC前,用CheckChunkState函数检查chunk状态。
|
||||
- 推荐用定时器创建NPC。
|
||||
|
||||
|
||||
|
||||
## 匹配
|
||||
|
||||
### 匹配的设计
|
||||
|
||||
点击NPC-C后,把多个玩家匹配分配到GameC。匹配是把多个玩家分配到另外一个单独服务器的过程,它是全服单点逻辑,建议在service实现匹配功能。
|
||||
|
||||
通常匹配功能设计思路如下:
|
||||
|
||||
* lobby向service请求匹配。
|
||||
* service包含一个待匹配玩家队列。玩家中途退出时,需要将该玩家从队列中剔除。
|
||||
* service每帧遍历所有待匹配玩家,根据一定算法,将多个玩家分配到指定game服务器。
|
||||
* service告知game,玩家即将进入,并告知玩家信息。
|
||||
* service告知所有玩家切服到指定game。
|
||||
* 玩家进入game,完成匹配过程。
|
||||
|
||||
|
||||
|
||||
### 简易网络服模板匹配功能开发
|
||||
|
||||
匹配过程如下所示:
|
||||

|
||||
|
||||
* lobby服务端开发
|
||||
|
||||
服务端监听EntityBeKnockEvent事件,处理点击NPC行为,根据NPC的种类,处理不同的请求。点击NCP-A和NPC-B需要向master查询GameA和GameB的在线人数,点击NPC-C需要处理匹配逻辑。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
...
|
||||
|
||||
def OnNpcTouched(self, npc_entity_id, player_entity_id, gameType):
|
||||
'''
|
||||
点击npc回调函数。
|
||||
'''
|
||||
uid = self.playerid2uid[player_entity_id]
|
||||
if gameType == 'gameA':
|
||||
logger.info("%s touch NPC gameA",player_entity_id)
|
||||
#请求gameA玩家人数
|
||||
request_data = {'game': 'gameA', 'player_id': player_entity_id,'uid': uid,'client_id':netgameApi.GetServerId()}
|
||||
self.NotifyToMaster(modConfig.GetPlayerNumOfGameEvent,request_data)
|
||||
elif gameType == 'gameB':
|
||||
logger.info("%s touch NPC gameB",player_entity_id)
|
||||
#请求gameB玩家人数
|
||||
request_data = {'game': 'gameB', 'player_id': player_entity_id, 'uid': uid,
|
||||
'client_id': netgameApi.GetServerId()}
|
||||
self.NotifyToMaster(modConfig.GetPlayerNumOfGameEvent, request_data)
|
||||
elif gameType == 'gameC':
|
||||
logger.info("%s touch NPC gameC",player_entity_id)
|
||||
# 请求gameC匹配队列人数
|
||||
request_data = {'uid': uid, 'player_id': player_entity_id, 'game': 'gameC'}
|
||||
self.RequestToService(modConfig.awesome_match, modConfig.RequestMatchNum, request_data)
|
||||
|
||||
def OnSureGame(self,args):
|
||||
'''
|
||||
切服逻辑,如果是gameA和gameB则直接传去对应服,如果是gameC则加入匹配队列
|
||||
'''
|
||||
logger.info("OnSureGame {}".format(args))
|
||||
if args['game'] == "gameA":
|
||||
netgameApi.TransferToOtherServer(args['playerId'], "gameA")
|
||||
elif args['game'] == "gameB":
|
||||
netgameApi.TransferToOtherServer(args['playerId'], "gameB")
|
||||
elif args['game'] == "gameC":
|
||||
playerId = args['playerId']
|
||||
uid = self.mPlayerid2uid[playerId]
|
||||
levelcomp = self.CreateComponent(playerId, modConfig.Minecraft, "lv")
|
||||
playerLevel = levelcomp.GetPlayerLevel()
|
||||
if playerLevel >= 0:#大于0级才能匹配
|
||||
request_data = {'uid': uid, 'player_id': playerId,'game':args["game"]}
|
||||
self.RequestToService(
|
||||
modConfig.awesome_match,
|
||||
modConfig.RequestMatch,
|
||||
request_data
|
||||
)
|
||||
tipData = {'tipType' : TipType.matching} #1匹配中
|
||||
self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData)
|
||||
else:
|
||||
tipData = {'tipType': TipType.levelNotEnough} #0等级不够
|
||||
self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData)
|
||||
def OnMatchResultEvent(self, args):
|
||||
'''
|
||||
处理匹配结果。切到指定服务器。
|
||||
'''
|
||||
logger.info("OnMatchResultEvent {}".format(args))
|
||||
playerId = args['player_id']
|
||||
desc_game = args['desc_game']
|
||||
if args['game'] == 'gameC':
|
||||
#如果是gameC则延时1S传送
|
||||
tipData = {'tipType': TipType.toTransfer} # 2 即将传送
|
||||
self.NotifyToClient(playerId, modConfig.MatchResultTip, tipData)
|
||||
self.mTransferPlayerQueue.append(playerId)
|
||||
CoroutineMgr.StartCoroutine(self.Transfer2Server(playerId, desc_game))
|
||||
def Transfer2Server(self,playerId,descGame):
|
||||
'''
|
||||
把玩家传送至对应的服
|
||||
'''
|
||||
yield -30
|
||||
#判断玩家是否在待传送队列里,若玩家中途下线,则不作处理
|
||||
if playerId in self.mTransferPlayerQueue:
|
||||
netgameApi.TransferToOtherServerById(playerId, descGame)
|
||||
self.mTransferPlayerQueue.remove(playerId)
|
||||
```
|
||||
- service开发
|
||||
|
||||
service监听UpdateServerStatusEvent事件,可以获取所有game的状态,这些可用game构成了可用资源池。当有玩家请求匹配时,则从可用资源池中分配资源(也就是匹配算法),然后告知玩家。核心代码如下:
|
||||
|
||||
```python
|
||||
class AwesomeService(ServiceSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ServiceSystem.__init__(self, namespace, systemName)
|
||||
self.mFrameCnt = 0
|
||||
self.mPlayerServer = {}
|
||||
self.mGameCMatchingPlayer = []#gameC的匹配玩家
|
||||
self.mActiveGameServerIds = [] #可用game列表
|
||||
self.mGameStatus = {}#serverid => server status.server status:
|
||||
#注册service接口
|
||||
self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatch, self.OnRequestMatch)
|
||||
self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatchCancel, self.OnRequestMatchCancel)
|
||||
self.RegisterRpcMethod(modConfig.awesome_match, modConfig.RequestMatchNum, self.OnRequestMatchNum)
|
||||
|
||||
def OnRequestMatchCancel(self,server_id, callback_id,args):
|
||||
logger.info("OnRequestMatchCancel {}".format(args))
|
||||
player_id = args["player_id"]
|
||||
if player_id in self.mGameCMatchingPlayer:
|
||||
self.mGameCMatchingPlayer.remove(player_id)
|
||||
|
||||
def OnRequestMatchNum(self,server_id, callback_id, args):
|
||||
'''
|
||||
返回匹配队列人数
|
||||
:return:
|
||||
'''
|
||||
logger.info("OnRequestMatchNum {}".format(args))
|
||||
result_data = {
|
||||
'uid':args["uid"],'player_id':args["player_id"],
|
||||
'playernum':len(self.mGameCMatchingPlayer),
|
||||
"game": args["game"]
|
||||
}
|
||||
self.NotifyToServerNode(server_id, modConfig.MatchNumEvent, result_data)
|
||||
|
||||
def OnRequestMatch(self, server_id, callback_id, args):
|
||||
'''
|
||||
请求匹配进入gameC的游戏
|
||||
'''
|
||||
logger.info("OnRequestMatch {}".format(args))
|
||||
player_id = args['player_id']
|
||||
self.mPlayerServer[player_id] = server_id
|
||||
#如果已经在匹配队列,则不加入匹配队列
|
||||
if player_id in self.mGameCMatchingPlayer:
|
||||
return
|
||||
else:
|
||||
logger.info("%s matching",player_id)
|
||||
self.mGameCMatchingPlayer.append(player_id)
|
||||
|
||||
def GameCMatch(self):
|
||||
'''
|
||||
检查匹配队列,匹配成功,清空匹配队列
|
||||
:return:
|
||||
'''
|
||||
if not self.mGameCMatchingPlayer:
|
||||
return
|
||||
desc_game = -1
|
||||
if len(self.mGameCMatchingPlayer) >=2:
|
||||
desc_game = self.MatchAlgorithm()
|
||||
if desc_game == -1:
|
||||
return
|
||||
for i in range(len(self.mGameCMatchingPlayer)):
|
||||
playerId = self.mGameCMatchingPlayer[i]
|
||||
self.NotifyToServerNode(self.mPlayerServer[playerId], modConfig.MatchResultEvent, {'player_id': playerId,'desc_game':desc_game,'game':'gameC'})
|
||||
self.mGameCMatchingPlayer = []#清空匹配队列
|
||||
|
||||
def Update(self):
|
||||
self.mFrameCnt += 1
|
||||
if self.mFrameCnt % 10 == 0:#10帧匹配一次
|
||||
self.GameCMatch()
|
||||
|
||||
def MatchAlgorithm(self):
|
||||
'''
|
||||
匹配算法
|
||||
'''
|
||||
serverid = -1
|
||||
serverlistConf = serviceConf.netgameConf['serverlist']
|
||||
for serverConf in serverlistConf:
|
||||
if serverConf['type'] == "gameC":
|
||||
serverid = serverConf['serverid']
|
||||
break
|
||||
return serverid
|
||||
|
||||
def OnUpdateServerStatusEvent(self, args):
|
||||
'''
|
||||
记录服务器状态
|
||||
'''
|
||||
logger.info("OnUpdateServerStatusEvent {}".format(args))
|
||||
self.mGameStatus = {}
|
||||
self.mActiveGameServerIds = []
|
||||
for server_id, status in args.iteritems():
|
||||
id = int(server_id)
|
||||
int_status = int(status)
|
||||
self.mGameStatus[id] = int_status
|
||||
if int_status == EServerStatus.OK:
|
||||
self.mActiveGameServerIds.append(id)
|
||||
```
|
||||
- Master开发
|
||||
|
||||
Master查询对应游戏玩家人数用。核心代码如下:
|
||||
```python
|
||||
class AwesomeMaster(MasterSystem):
|
||||
... ...
|
||||
def GetPlayerNumOfGame(self,args):
|
||||
serverlistConf = masterConf.netgameConf['serverlist']
|
||||
print "OnGetPlayerNumOfGameResponse",args
|
||||
checkServeridList = []
|
||||
for serverConf in serverlistConf:
|
||||
if serverConf['type'] == args["game"]:
|
||||
serverid = serverConf['serverid']
|
||||
checkServeridList.append(serverid)
|
||||
playernum = 0
|
||||
for serverid in checkServeridList:
|
||||
playernum += serverManager.GetOnlineNumByServerId(serverid)
|
||||
request_data = {
|
||||
'game': args["game"],
|
||||
'playernum': playernum,
|
||||
'player_id':args["player_id"]
|
||||
}
|
||||
self.NotifyToServerNode(
|
||||
args["client_id"],
|
||||
modConfig.GetPlayerNumOfGameRequestEvent,
|
||||
request_data)
|
||||
```
|
||||
用MCStudio进入游戏,点击不同的NPC发现切服到对应game。
|
||||
|
||||
备注:新建一个mod.json于ServiceMod/developer_mods/AwsomeService下面,mod.json的内容是
|
||||
{
|
||||
"netgame_mod_name": null,
|
||||
"netgame_mod_version": "1.0.0",
|
||||
"min_app_version": null,
|
||||
"author": null,
|
||||
"module_names": ["awsome_match"],
|
||||
"support_server_type": null
|
||||
"unsupport_app_version": null,
|
||||
"CustomPath": null
|
||||
}
|
||||
|
||||
### 总结
|
||||
|
||||
- service监听UpdateServerStatusEvent事件,记录可用服务器列表。
|
||||
|
||||
- 匹配过程主要包括:请求匹配、匹配算法、玩家迁移。
|
||||
|
||||
|
||||
|
||||
151
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第5节:游戏外功能.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
front: https://mc.res.netease.com/pc/zt/20201109161633/mc-dev/assets/img/wps13.a2434b82.jpg
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 游戏外功能
|
||||
|
||||
## 运营指令
|
||||
|
||||
运营指令:接收外部http请求,处理游戏相关逻辑,比如给某个玩家发物品,公告等。
|
||||
|
||||
master是运营执行入口,开发者可以根据需要,把请求分发到lobby/game/service。下面给AwesomeGame新增一个运营指令,功能是打印指定玩家信息。
|
||||
|
||||
### 获取玩家数据运营指令
|
||||
|
||||
由于lobby是异步定时存档的,因此mysql数据可能不是最新的。这里实现方案是:
|
||||
|
||||
* 玩家在lobby,从对应lobby的内存中拉取玩家数据。
|
||||
* 玩家不在lobby,选择任意一个可用lobby,从db中读取玩家数据。
|
||||
|
||||
处理过程如下所示:
|
||||

|
||||
|
||||
master主要接受请求然后转发,核心代码如下所示:
|
||||
|
||||
```python
|
||||
class AwesomeMaster(MasterSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
MasterSystem.__init__(self, namespace, systemName)
|
||||
# 注册gm指令
|
||||
masterHttp.RegisterMasterHttp('/get-user-info', self, self.OnGetUserInfo) self.DefineEvent('GetUserInfoRequestEvent')
|
||||
self.ListenForEvent(
|
||||
'Minecraft', 'AwesomeLobby',
|
||||
'GetUserInfoResponseEvent',
|
||||
self, self.OnGetUserInfoResponse
|
||||
)
|
||||
|
||||
def OnGetUserInfo(self, client_id, request_body):
|
||||
'''
|
||||
获取gm指令
|
||||
'''
|
||||
import ujson as json
|
||||
request = json.loads(request_body)
|
||||
uid = request['uid']
|
||||
redis_key_player = "online_user_%d" % uid
|
||||
#获取玩家在线状态
|
||||
redisPool.AsyncHgetall(
|
||||
redis_key_player,
|
||||
lambda record:self._GetUserInfoCb(client_id, uid, record)
|
||||
)
|
||||
|
||||
def _GetUserInfoCb(self, client_id, uid, record):
|
||||
'''
|
||||
回调函数。获取目标lobby,向lobby请求在线人数。
|
||||
'''
|
||||
serverid = None
|
||||
serverlistConf = masterConf.netgameConf['serverlist']
|
||||
if record:
|
||||
#若玩家在game中,则随机从一个lobby获取在线人数。
|
||||
serverid = record.get('serverid', None)
|
||||
tmpServerConf = masterConf.serverListMap.get(serverid, None)
|
||||
if not tmpServerConf or tmpServerConf['type'] != 'lobby':
|
||||
serverid = None
|
||||
if not serverid:
|
||||
for serverConf in serverlistConf:
|
||||
#服务器可用且是lobby
|
||||
if serverConf['type'] == 'lobby' \
|
||||
and serverManager.IsValidServer(serverConf['serverid']):
|
||||
serverid = serverConf['serverid']
|
||||
break
|
||||
if not serverid:
|
||||
response = self.makeFailResponse(master_http.HTTP_CODE_FAIL, 'no valid lobby.')
|
||||
masterHttp.SendHttpResponse(client_id, response)
|
||||
return
|
||||
request_data = {'uid' : uid, 'client_id' : client_id}
|
||||
self.NotifyToServerNode(serverid, 'GetUserInfoRequestEvent', request_data)
|
||||
|
||||
def OnGetUserInfoResponse(self, args):
|
||||
'''
|
||||
接受玩家数据,返回http请求。
|
||||
'''
|
||||
client_id = args['client_id']
|
||||
entity = args['user_info']
|
||||
response = self.makeResponse(master_http.HTTP_CODE_SUCCESS, '', entity)
|
||||
masterHttp.SendHttpResponse(client_id, response)
|
||||
```
|
||||
Lobby主要获取玩家数据,核心代码如下所示:
|
||||
```python
|
||||
class AwesomeServer(ServerSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ServerSystem.__init__(self, namespace, systemName)
|
||||
self.ListenForEvent(
|
||||
modConfig.Minecraft, modConfig.MasterSystemName, modConfig.GetUserInfoRequestEvent,
|
||||
self, self.OnGetUserInfoRequest)
|
||||
|
||||
def OnGetUserInfoRequest(self, args):
|
||||
'''
|
||||
获取玩家数据。
|
||||
'''
|
||||
uid = args['uid']
|
||||
client_id = args['client_id']
|
||||
player_data = self.mPlayerMap.get(uid, None)
|
||||
if not player_data:
|
||||
if self.mDBType == DbType.Mongo:
|
||||
self.mMongoMgr.QueryPlayerData(
|
||||
uid, uid,
|
||||
lambda data: self._OnGetUserInfoRequestCb(client_id, data))
|
||||
elif self.mDBType == DbType.Mysql:
|
||||
self.mMysqlMgr.QueryPlayerData(
|
||||
uid, uid,
|
||||
lambda data: self._OnGetUserInfoRequestCb(client_id, data))
|
||||
else:
|
||||
self._GetUserInfoResponse(client_id, player_data.toSaveDict())
|
||||
|
||||
def _OnGetUserInfoRequestCb(self, client_id, record):
|
||||
'''
|
||||
回调函数,处理db操作结果,把玩家数据告知master。
|
||||
'''
|
||||
if record:
|
||||
player_data = playerData.PlayerData()
|
||||
player_data.initPlayer(-1, record)
|
||||
self._GetUserInfoResponse(client_id, player_data.toSaveDict())
|
||||
else:
|
||||
self._GetUserInfoResponse(client_id, {})
|
||||
|
||||
def _GetUserInfoResponse(self, client_id, player_info):
|
||||
'''
|
||||
玩家数据告知master。
|
||||
'''
|
||||
response_data = {'client_id' : client_id, 'user_info' : player_info}
|
||||
self.NotifyToMaster('GetUserInfoResponseEvent', response_data)
|
||||
```
|
||||
### 验证
|
||||
|
||||
登录到开发机,然后给master发送curl请求,即可获取结果,如下图示:
|
||||
<img src="./images/wps14.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
## 官方运营指令
|
||||
|
||||
查看“服务器MOD SDK”中【运营指令】部分,里面介绍了常用的指令,比如禁言、踢人等。
|
||||
|
||||
## 总结
|
||||
|
||||
- 运营指令的实现通常分为两个步骤:
|
||||
|
||||
- master接受响应指令,将指令请求转发到其他服务器;
|
||||
- lobby/game/serivce实现指令功能。
|
||||
|
||||
- 官方实现了常见的运营指令,具体可以查看“服务器MOD SDK”中【运营指令】部分。
|
||||
|
||||
118
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第6节:优化与维护.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 15分钟
|
||||
---
|
||||
|
||||
# 优化和维护
|
||||
|
||||
优化部分介绍如何查找脚本层内存泄漏。维护部分介绍如何追查线上问题。
|
||||
|
||||
## 内存检查
|
||||
|
||||
下面结合简易网络服模板介绍如何检查内存泄漏。
|
||||
|
||||
### 制造内存泄漏问题
|
||||
|
||||
lobby玩家退出时,不清理内存数据。按照下面方式修改代码:
|
||||
|
||||
```python
|
||||
def OnDelServerPlayer(self, args):
|
||||
'''
|
||||
清除玩家内存数据。
|
||||
'''
|
||||
player_id = args.get('id','-1')
|
||||
uid = self.mPlayerid2uid.get(player_id, None)
|
||||
if not uid:
|
||||
return
|
||||
del self.mPlayerid2uid[player_id]
|
||||
# if uid in self.mPlayerMap:
|
||||
# del self.mPlayerMap[uid]
|
||||
# if uid in self.mUid2dimension:
|
||||
# del self.mUid2dimension[uid]
|
||||
```
|
||||
### 检查内存泄漏
|
||||
|
||||
然后给master发送check-memory-run指令,生成一个内存快照,结果如下图示:
|
||||
<img src="./images/wps15.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
查看lobby日志:
|
||||

|
||||
|
||||
用MCStudio进入游戏,然后退出,再给master发送check-memory-run指令,再生成一次内存快照,结果如下图示:
|
||||
<img src="./images/wps17.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
再次查看lobby日志:
|
||||

|
||||
|
||||
分析:
|
||||
|
||||
\[Top 10 differences\]记录占用内存最多的10行代码。简单分析下面日志:
|
||||
<img src="./images/wps19.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
obj_report.py文件43行占用内存最多,占用48k内存,实例个数为1,两次内存快照间增加了一个实例,平均每个实例占用48k。、
|
||||
|
||||
\[DIFF_MORE\]记录了内存变化最多的类型。简单分析下面日志:
|
||||
<img src="./images/wps20.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
两次内存快照间新增了一个PlayerData类型实例。
|
||||
|
||||
接着分析AwesomeGame内存泄漏问题。第一次内存快照时,没有玩家登录,接着玩家登录再登出,然后再生成了一次内存快照。第二次内存快照时,没有玩家在游戏中,因此服务端内存是不会有玩家数据,也即不会有PlayerData类型实例的。但是,\[DIFF_MORE\]中显示内存中还是多了一个PlayerData类型实例,这说明存在内存泄漏。
|
||||
|
||||
|
||||
### 总结
|
||||
|
||||
* check-memory-run指令可以检测两次内存快照间内存变化。
|
||||
* 内存泄漏检查要点:在游戏平稳时生成内存快照(比如没有玩家登录登出),然后分析内存变化。
|
||||
|
||||
## Hunter
|
||||
|
||||
开发者先阅读“服务器MOD SDK”中“Hunter调试命令”部分。下面结合“AwesomeGame”网络游戏介绍如何在线调试mod。
|
||||
|
||||
### 获取lobby服在线玩家信息
|
||||
|
||||
用MCStudio进入游戏,然后给master发送/hunter-debug指令:
|
||||
<img src="./images/wps21.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
实质是在lobby中执行下面代码:
|
||||
|
||||
```python
|
||||
import server.extraServerApi as serverApi
|
||||
#获取AwesomeServer实例
|
||||
mainSys=serverApi.GetSystem("Minecraft", "AwesomeLobby")
|
||||
#打印玩家信息
|
||||
print mainSys.mPlayerid2uid
|
||||
```
|
||||
执行结果需要查看lobby日志:
|
||||
<img src="./images/wps22.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
日志说明,lobby服只有一个玩家,日志打印了该玩家的player id和uid。
|
||||
|
||||
### 清空lobby服玩家信息
|
||||
|
||||
然后给master发送/hunter-debug指令:
|
||||
<img src="./images/wps23.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
实质是在lobby中执行下面代码:
|
||||
|
||||
```python
|
||||
import server.extraServerApi as serverApi
|
||||
#获取AwesomeServer实例
|
||||
mainSys=serverApi.GetSystem("Minecraft", "AwesomeLobby")
|
||||
#清除玩家信息
|
||||
mainSys.mplayerid2uid = {}
|
||||
mainSys.mPlayerMap = {}
|
||||
mainSys.mUid2dimension = {}
|
||||
print “clear ok”
|
||||
```
|
||||
执行结果需要查看lobby日志:
|
||||
<img src="./images/wps24.jpg" alt="img" style="zoom:150%;" />
|
||||
|
||||
日志打印”clear ok”,说明清除成功。
|
||||
|
||||
### 总结
|
||||
|
||||
hunter-debug指令支持在线执行一段python脚本,使用该指令可以方便查看变量信息,修改变量内容。
|
||||
|
||||
|
||||
|
||||
36
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第7节:控制服务器Mod加载顺序.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 5分钟
|
||||
---
|
||||
|
||||
# 控制服务器Mod加载顺序
|
||||
|
||||
## 为每个Mod命名
|
||||
|
||||
* 在mod的根目录自主添加一个netease_require.json的文件,举例来说,对于【neteaseMonitor】这个Mod,就应该在【developer_mods/neteaseMonitor/】目录下创建netease_require.json文件
|
||||
* 文件举例
|
||||
```json
|
||||
{
|
||||
"modName": "neteaseMonitor",
|
||||
"modRequire": [
|
||||
]
|
||||
}
|
||||
```
|
||||
* 其中,modName的值【neteaseMonitor】就是当前mod的名字,而modRequire的值为空list,说明当前Mod不需要其他任何前置Mod
|
||||
* 假如json文件不存在,当前Mod的名字默认等于插件根目录下developer_mods文件夹下的服务端mod文件夹的名字
|
||||

|
||||
|
||||
## 设置Mod的前置Mod
|
||||
* 同样是在netease_require.json文件中,通过修改modRequire属性来设置当前Mod的前置Mod,服务器启动并加载某个Mod时,会尽量保证这个Mod的前置Mod已经加载完成。(假如设置中,出现循环require,那么会打印ERROR日志,但是依旧会加载mod,所有出现循环require的mod都会在最后加载)
|
||||
* 举例,【neteaseMonitorSample】这个Mod依赖【neteaseMonitor】,需要在【neteaseMonitor】加载完毕之后才加载【neteaseMonitorSample】,那么【neteaseMonitorSample】的netease_require.json文件如下:
|
||||
```json
|
||||
{
|
||||
"modName":"neteaseMonitorSample",
|
||||
"modRequire":[
|
||||
"neteaseMonitor"
|
||||
]
|
||||
}
|
||||
```
|
||||
* modRequire中的neteaseMonitor说明当前Mod需要在【neteaseMonitor】之后加载
|
||||
* 假如json文件不存在,modRequire默认为空list,也就是没有加载顺序要求。
|
||||
45
docs/mcguide/27-网络游戏/课程4:简易网络服模板知识讲解/第8节:性能数据.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 5分钟
|
||||
---
|
||||
|
||||
# 性能数据
|
||||
|
||||
## 确定一台机器部署服务进程数量
|
||||
|
||||
mc的自由度非常大,每种网络游戏的实现千差万别。不同的网络游戏使用不同的mod,不同mod的性能消耗也不一样的。有的网络游戏玩法复杂,一台物理机可能部署20个game;有的网络游戏玩法简单,一台物理机可能部署30个game。
|
||||
|
||||
我们建议网络游戏服主积极联系我们,我们会指导服主按照最佳方式部署。
|
||||
|
||||
## 评估游戏服的最大在线人数
|
||||
|
||||
- 服务器承载能力与在线玩家数平方成反比。在线玩家越多,服务器消耗cpu越多。
|
||||
- 地图大小主要影响服务器占用内存,对服务器承载能力影响较弱。目前单服进程最大内存是固定8G。
|
||||
- 服务器不会一次加载所有地图,它会按需加载区块,会将没使用到的区块存储到磁盘。
|
||||
- 服务器性能消耗由mod和引擎组成,mod性能消耗也是不能忽略的。服主优化mod就能提升网络游戏性能,服主应尽力优化mod。
|
||||
- 最大在线人数评估方法:网络服正式上线前建议先进行限量删档测试,在删档测试期间观察服务器性能,得到lobby/game最大在线人数。具体过程:初期将lobby最大在线人数设置50~100,game最大在线人数设置10~30, 接着观察服务器性能,然后不断滚动更新调整最大在线。
|
||||
|
||||
## 第五人格的性能数据
|
||||
|
||||
### 部署方式
|
||||
第五人格使用的是低频主机,40核,128G内存,主频2.3GHZ,一共使用18台低频物理机。具体部署方式如下:
|
||||
|
||||
- 1个master、2个service、10个game部署在一台物理机。
|
||||
- 10个proxy部署在一台物理机。
|
||||
- 20个lobby部署在一台物理机。
|
||||
- 300个game部署到20台物理机,每个物理机部署15个game。
|
||||
- 每个服务器进程的内存限制为最大8G。
|
||||
|
||||
### 承载人数
|
||||
服务器可以承载这么多人,要求满足以下条件:
|
||||
- 服务器进程的主线程cpu大部分时间不超过80%。
|
||||
- 进程占用内存不允许超过8G。
|
||||
- 客户端不存在明显卡顿或延迟。
|
||||
|
||||
下面介绍第五人格各个服的承载能力:
|
||||
- 一个proxy可以承载600人,10个proxy可以承载6000人。
|
||||
- 一个lobby可以承载200人,20个lobby可以承载4000人。
|
||||
- 一个game可以承载15人,310个game可以承载4650人。第五人格一个game承载人数较低,主要是玩法限制导致,游戏一个game只能开3局,一局只有5人。
|
||||
|
||||
结论:第五人格使用18台物理机条件下部署,proxy可以承载6000人,lobby+game可以承载8650人,取两者最小值,第五人格可以承载6000人。
|
||||