This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,88 @@
---
front:
hard: 入门
time: 10分钟
---
# 什么是联机大厅
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=63468088e6c041f2578d9204" width="800" height="600" allow="fullscreen"/>
## 在开始之前
不论是共赏美景、齐心协力,还是竞技对抗,联机功能使玩家们在虚拟的游戏世界中不会孤单。
当玩家体验一张特殊困难生存玩法地图时,觉得要是有个伙伴一起挑战就好了;
当玩家想要比试战斗技术就需要一张竞技类玩法地图和几个同样热爱PVP的对手
![1-1](./image/0_0.jpg)
通过联机大厅,玩家可以方便地找到满意的玩法,然后马上遇到数个同样和你看中了这个玩法的伙伴,志趣相投的几人携手开启一段美妙的冒险旅程。
**本系列教程主要围绕如何将联机大厅玩法商业化,即我们假定你已经了解[什么是玩法地图](../玩法地图基础教程/1-玩法地图是追求完整游戏体验的不二选择.html),甚至已经制作好一个成型的多人玩法地图,我们将在此基础上设计、实现数个内购商品,并测试、上架这些商品供玩家购买。**
**2023.2更新:**
我们讲到设计、实现数个内购商品此前是指利用预设系统编写几行Python代码以定制你的发货零件。现在《我的世界》开发工作台支持使用<a href="../../../mcguide/20-玩法开发/12-可视化编程/10-新版逻辑编辑器使用说明/01-什么是逻辑编辑器.html">逻辑编辑器</a>实现发货功能,真正做到不需要写一行代码就可以做出丰富的内购商品功能。
## 玩法地图和联机大厅的关系
在开平发布新资源时,你可能会对玩法地图和联机大厅玩法之间的关系产生困惑:玩法地图可以同时上架联机大厅,联机大厅资源又可以同时上架玩法地图,勾选了商业化内购功能的联机大厅资源则不可以同时上架玩法地图?什么跟什么,简直太乱了。
解答来了:准确地讲,联机大厅玩法是一种特殊的玩法地图。
联机大厅本身是一种运行方式,向玩家提供了灵活的联机服务。实际上,联机大厅的房间可以理解为虚拟化出来的一个容器,理论上可以用任意一张玩法地图来启动。而一开始就为多人游戏设计的、可能包含内购商品、运营逻辑、云成就系统的玩法地图,可以称之为联机大厅玩法(资源)。
![image-20220831044554657](./image/0_1.png)
## 联机大厅和其他多人游戏方式的区别
要了解联机大厅,首先需要知道联机大厅的定位。在我的世界中,主要通过如下几种方式实现玩家间的联机:
![image-20220831050630245](./image/0_2.png)
相比其他几种多人游戏方式,特别是本地联机,联机大厅有如下特点:
- **服务端引擎运行在专用云服务器而不是房主手机上,网络质量、服务器性能较好。** 因此我们在设计商品时进一步不受桎梏,可以任意发挥,例如制作更精美的特效、要求服务器进行批量的实体生成、方块创建销毁等。
- **玩家间流动更灵活。** 相对动辄几十、百人的租赁服、网络游戏,联机大厅由一个个房间组成,将玩家分割成更小的部分,玩家随时会从一个房间退出后加入另一个房间;房间也会随时启动新的、销毁旧的。这就让玩家更容易遇见随机的陌生人,从而一定程度上实现了网络游戏中随机匹配几个玩家开启一局游戏的功能。![image-20220831040849335](./image/0_3.png)
- **内容统一而可控。** 联机大厅的房间由开发者提供的包体启动而来相比本地存档联机环境更加确定不会有无法预测的其他组成部分能对玩家做更好的限制而不会让不遵守游戏规则的玩家轻易破坏机制玩成TNT满天飞失去玩法原本设计的意义。同时由于环境的封闭使得你提供一些增值服务作为商品成为可能例如起床战争的自动铺路、战墙时的连锁挖矿等。
## 房间的生命周期
上面提到,房间是联机大厅的基本单位,我们借由下图了解一个房间从启动到销毁会发生什么。在开发过程中,只需要额外注意一下游戏进行到各个状态时玩家列表的改变造成的影响,并正确处理即可。
![image-20220831080025017](./image/0_4.png)
## 进阶的创作需求
联机大厅在2.0更新了一些新功能,将这个赛道推到前所未有的新高度。本系列教程主要围绕商业化讨论,由于技术需要会同时涉及到云数据储存。关于运营配置的部分,请参阅<a href="../../../mcguide/26-联机大厅/6-联机大厅商品2.0文档.html">联机大厅商品2.0文档</a>。
<img src="./image/0_5.png" alt="image-20220831082737891" style="zoom: 50%;" />
### 云数据储存
上面已经聊到,玩家会很容易地在房间之间流动。那么玩家在不同房间之间的体验要一致,积累的分数、背包、资产要带走,这就很重要。
我们知道房间承载存档,存档本身可以储存数据,但不同存档的数据不能互通。所以为了实现这些数据的同步,需要一个第三方媒介——云数据库。
云数据库是什么样,什么原理,如何连接,如何操作,这些我们通通无需关心,引擎已经为我们封装和处理好,我们只需要调用两个新接口,将需要储存的数据交给云数据库,再等需要的时候使用接口获取回来即可。
只有一点需要稍稍注意由于是云数据库我们使用接口本质上是在进行http操作所以callback是异步的在编码的时候需要留个心眼。
![image-20220831084628374](./image/0_6.png)
### 商业化(商品内购)
在完成了基本的玩法设计后,我们当然希望可以将玩法商业化,让开发者的热爱有所回响。
在游戏内我们可以吸引玩家或玩家主动打开商店neteaseStore玩家选购商品后商店会通过引擎通知你的代码流程类似下图
![image-20220831085859318](./image/0_7.png)
当然,有时候玩家购买的商品需要**持久化**,那么就需要借助云数据库的力量,过程进化为如下:
![image-20220831091058594](./image/0_8.png)
在下一章中,我们将了解如何设计、实现商品。

View File

@@ -0,0 +1,33 @@
---
front:
hard: 入门
time: 5分钟
---
# 什么是联机大厅课后作业
1.服务端是高性能服务器的有?
- A. 联机大厅和租赁服。
- B. 联机大厅和自定义联机。
- C. 自定义联机和租赁服。
- D. 网络游戏和自定义联机。
<!--解答答案A。 **自定义联机的服务端是房主的终端。**-->
2.联机大厅玩法是?
- A. 上架地图的可以多人游玩的玩法。
- B. 没有勾选同时上架联机大厅的玩法地图。
- C. 联机大厅的一种。
- D. 玩法地图的一种。
<!--解答答案D。 **联机大厅玩法是一种特殊的玩法地图。 **一开始就为多人游戏设计的、可能包含内购商品、运营逻辑、云成就系统的玩法地图,可以称之为联机大厅玩法(资源)。-->
3.当我要在联机大厅玩法内制作一个起床战争击杀榜,应该用到如下什么技术?
- A. 运营配置。
- B. 商业化。
- C. 云数据储存。
- D. 成就系统。
<!--解答答案C。 **击杀榜的多房间数据同步需要用到云数据储存。**-->

View File

@@ -0,0 +1,114 @@
---
front:
hard: 高级
time: 30分钟
---
# 什么是内购商品
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6346815ac6dfd1bb76f2bfac" width="800" height="600" allow="fullscreen"/>
## 商品的类型
谈到根据玩法设计商品,策划同学应该就不困了。开发者可以根据自己手上的作品,具体问题具体分析,针对性的设计。在此期间,可能会脑洞出很多可能和提案,为了方便思考和整理,可以从两个维度区分商品:
![image-20220913151939381](./image/1_1.png)
- 单局商品
就是说这个商品只在当前这一个联机大厅房间有效,而若玩法设计了重复开局,那么新的一局是否清除商品的效用则开发者自行决定。
若玩家退出房间并再次进入,只要逻辑系统尚未卸载,理论上商品可以继续生效,也可以选择在玩家退出时让商品失效。
但无论如何,一旦房间关闭,内存将会丢失,单局商品将彻底失效。开发者仍然可以通过接口查询到玩家曾经购买单局商品的历史记录。
- 持久化,永久生效的商品
持久化是指将玩家购买此商品记入云端数据库,以达到玩家即使换房间,或隔几天又来玩这个地图,逻辑系统始终可以知道玩家(曾经)购买了此商品。
- 持久化,定时生效的商品
定时生效有别于上面的永久生效当然也和持久化意思不冲突是指逻辑系统知道玩家购买了但并不是永久提供某项服务。又可以分为例如某权限自购买日起生效10天到期后失去权限或s10赛季对应的通行证从9月1号运行到9月30号那么无论玩家何时购买此商品 都会在9月30号失去权限。
<img src="./image/1_2.png" alt="image-20220913152008754" style="zoom:50%;" />
- 消耗品
一般有食物、药水、盔甲、各种道具这些物品形式的,由于玩法一般是生存或冒险模式,用了就没了,相当于是原版逻辑帮我们完成计费。
若是一些别的形式,例如释放一次某个技能,这种由逻辑系统实现的行为,可以用云数据库来记录消费次数。
- 非消耗品
一些常在的权限不需要计次例如某种外观、称号、VIP身份由云数据库记录使用时判断玩家是否拥有权限。
## 合理定价
我们知道,《我的世界》客户端中玩家有两种货币可供消费——钻石、绿宝石。
![image-20220901181327309](./image/1_3.png)
- **钻石** 一般是玩家用人民币购买的,也在开平中作为收益结算的来源。因此建议将价值高的商品用钻石定价。
- **绿宝石** 是玩家在客户端和游戏中通过各种行为积攒的免费积分,绿宝石收入在开平中作为开发者积分、等级的重要依据,若想提升开发者等级,绿宝石收入不可或缺。因此建议将一般价值的商品,特别是单局或消耗品商品用绿宝石定价。
有时候我们想要让玩家通过一些额外渠道获得高价值等价物例如参与某活动赠送10钻石但显然你不能扩展玩家的钻石来源于是可以考虑设计一种新的中间货币——金币玩家用钻石兑换金币再用金币兑换商品而你管控金币有权赠送玩家金币。
关于金币如何实现,本教程受限于篇幅不能详细讲到,建议参考<a href="../../../mcguide/20-玩法开发/13-模组SDK编程/60-Demo示例.html">lobbyGoodsMod2.0</a>这个官方示例demo。
## 玩家购买商品的方式
你设计的商品一般在 **这两个地方** 展示,玩家可在 **这两个地方** 点击购买:
- 方法一:竖屏启动器时,联机大厅资源左下角。
<img src="./image/1_54.png" alt="image-20220901190523718" style="zoom: 42%;" />
<img src="./image/1_55.png" alt="image-20220912021836175" style="zoom: 25%;" />
**tips此处的test_1662920041字样是因为处于手机版测试端环境。商品分类功能在测试端无法生效会临时显示乱序英文和数字。**
正式环境下,会显示分类:
![image-20220831050630245](./image/2_14.png)
- 方法二游戏内的neteaseStore商店界面。
![image-20220831050630245](./image/buy1.gif)
这就是我们在第一章中提到的[neteaseStore](./0-什么是联机大厅.html?catalog=1#商业化-商品内购),它是在游戏中将商品销售给玩家的重要媒介。因为在游戏中购买商品流程更简洁、直接,且商品效果立即生效。`neteaseStore`可看作一个UI打开它的方式有两种
- 默认自带一个按钮,在左上角,点击即可打开:
![image-20220831050630245](./image/1_60.png)
tips这个自带按钮并不是一开始就显示的毕竟不是所有玩法都需要它。所以需要在玩家进入游戏时调用一个接口显示这个按钮。
```python
import mod.client.extraClientApi as clientApi
clientApi.HideNeteaseStoreGui(False)
```
- 不显示自带按钮,通过玩法激发玩家购买欲望,然后在合适时机用<a href="../../../mcdocs/1-ModAPI/接口/原生UI.html?catalog=1#openneteasestoregui">接口</a>拉起UI引导玩家购买
| 参数名 | 数据类型 | 说明 |
| :----------- | :------- | :----------- |
| categoryName | str | 商品分类名称 |
| itemName | str | 商品名称 |
```python
import mod.client.extraClientApi as clientApi
clientApi.OpenNeteaseStoreGui("商品", "测试商品1")
```

View File

@@ -0,0 +1,35 @@
---
front:
hard: 高级
time: 30分钟
---
# 什么是内购商品 课后作业
1.某开发者欲设计一种付费方式,划分赛季并售卖战令,购买战令后达到满级可获得限定皮肤。
从生效时间维度来区分, **战令** 属于什么类型的联机大厅商品?
- A. 单局商品。
- B. 持久化,永久生效商品。
- C. 持久化,定时生效商品。
- D. 以上都不是。
<!--解答答案C。战令即通行证类商品达到某个时间点会失效通常是赛季截至时间。-->
从生效时间维度来区分, **限定皮肤** 属于什么类型的联机大厅商品?
- A. 单局商品。
- B. 持久化,永久生效商品。
- C. 持久化,定时生效商品。
- D. 以上都不是。
<!--解答答案D。限定皮肤由战令系统扣费、发放不通过联机大厅商品系统所以不属于任何一种。-->
2.如果你开发一个起床战争联机大厅玩法,售卖如下什么商品可获得更高评分?
- A. 秒杀剑。
- B. 无敌盔甲。
- C. 会员脚底特效光圈。
- D. 解锁毁天灭地VIP商店。
<!--解答答案C。**PVP类玩法不建议设计影响平衡性甚至pay to win的商品。**-->

View File

@@ -0,0 +1,41 @@
---
front:
hard: 入门
time: 5分钟
---
# 为玩法设计内购商品 课后作业
设计一个属于你的新职业,并独立完成制作。
第一步,策划设计方案模板格式:
- 治疗师Healer
- 预设使用模型:女巫。
- 定位:辅助。
- 护甲:铁胸甲,铁护腿,铁靴子。
- 武器:木剑。
- 物品喷溅型伤害药水x3喷溅型治疗药水x3喷溅型生命恢复x1。
- 售价100钻石不可重复购买
- 类型:购买后持久化,永久生效。
第二步,选择 Python/逻辑编辑器 其中的一个,参考本章正文,完成实现过程。
若选择Python参考
- [制作新职业-Python](./4-为玩法设计内购商品.html?catalog=1#Python程序向实战-制作新职业)。
- [制作会员特效-Python](./4-为玩法设计内购商品.html?catalog=1#Python程序向实战-制作会员特效)。
若选择逻辑编辑器,参考:
- [制作新职业-逻辑编辑器](./4-为玩法设计内购商品.html?catalog=1#逻辑编辑器程序向实战-制作新职业)。
- [制作会员特效-逻辑编辑器。](./4-为玩法设计内购商品.html?catalog=1#逻辑编辑器程序向实战-制作会员特效)

View File

@@ -0,0 +1,169 @@
---
front:
hard: 高级
time: 15分钟
---
# 上传并售卖商品
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=63468187c6dfd1bb76f2bfb4" width="800" height="600" allow="fullscreen"/>
本章将带你手把手的创建联机大厅资源、上传商品,有关更多系统性的信息,请查阅<a href="../../../mcguide/26-联机大厅/5-联机大厅作品与商品上传文档.html">商品上传文档</a>。
## 创建地图和商品
创建资源时,资源类别选择**联机大厅**,勾选商业化内购功能。
<img src="./image/1_41.png" alt="image-20220901182339201" style="zoom:50%;" />
创建好后,该资源将出现在 **联机大厅商品栏**
![image-20220901182537714](./image/1_42.png)
点击 **添加商品**
![image-20220901182801120](./image/1_43.png)
可选:编辑商品分类信息。
<img src="./image/1_44.png" alt="image-20220912015916334" style="zoom:50%;" />
编辑基本信息。
<img src="./image/1_45.png" alt="image-20220912020216785" style="zoom:50%;" />
实现指令见下一部分,补充剩余商品信息,并保存商品。
![image-20220912021147595](./image/4_0.png)
将商品 **提交自测** ,联机大厅资源本身也 **提交自测** ,便可看到此商品出现在橱窗。
当然这只是测试版客户端的橱窗,正式版并不会上架此商品,若商品要投入生产环境,需要提交审核,然后更新到橱窗。
![image-20220912021147595](./image/4_1.png)
<img src="./image/4_2.png" alt="image-20220901190523718" style="zoom: 42%;" />
<img src="./image/4_3.png" alt="image-20220901190523718" style="zoom: 42%;" />
<img src="./image/4_4.png" alt="image-20220901190523718" style="zoom: 42%;" />
如上所示,添加剩余两个商品。
![image-20220901182537714](./image/4_5.png)
<img src="./image/4_6.png" alt="image-20220912023500454" style="zoom:25%;" />
## 什么是实现指令
在上传商品时,你需要了解 **实现指令** 的概念。实现指令是一个 **代号** ,由平台和开发者 **提前约定好** 用于识别玩家购买的商品并授予游戏内的实现权限。当玩家在游戏中购买了某个商品后商店会向联机大厅房间中运行的逻辑系统Python/蓝图发送一个事件告诉逻辑系统玩家购买了XX商品。实现指令的内容就是这个商品。该事件包含一个实现指令参数开发者通过这个参数判断玩家购买的具体商品以便给玩家发货。
<img src="./image/4_7.png" alt="image-20230319000115766" style="zoom:50%;" />
举个例子,如果你出售一个皮肤包,可将 **实现指令** 参数设置为 **sendSkin_001** 。如果有一个叫做 **火箭发射器** 的商品,那么对应的实现指令可以是 **rocket_launcher** 。一件魔法武器,实现指令可设为 **magic_sword**
在下图中neteaseStore官方商店UI告知房间1 **玩家3购买了1个金苹果** ,那么实现指令就是 **golden_apple**
![image-20220901182537714](./image/4_8.png)
简而言之,实现指令是连接平台和游戏内容逻辑代码的重要纽带,它帮助逻辑代码识别出哪个商品需要被发货,以便给玩家发放商品实现权限,确保购买商品的顺畅交付。
## 实现指令的两种格式
实现指令格式支持`str`(字符串)或`json`。若您对编程接触不深,建议使用字符串即可(填写时注意输入精准、注意大小写、不要有多余字符/空格、若有符号注意全半角问题)。
<img src="./image/1_46.png" alt="image-20230319000115766" style="zoom:50%;" />
也可以使用`json`格式。
<img src="./image/1_47.png" alt="image-20230319000115766" style="zoom:50%;" />
使用`json`格式,即使需要发生部分变化,只要核心内容没有改变, **仍不会影响识别功能本身**
<img src="./image/1_48.png" alt="image-20230319000115766" style="zoom:50%;" />
`json`体内可包含任意字段只要起到标识作用即可可以是code或name或任意形式取决于开发者喜好。
<img src="./image/1_49.png" alt="image-20230319000115766" style="zoom:50%;" />
实际填写实现指令之前为了保证不出现无法反序列化错误建议一定要在格式化工具里校验、压缩json。
<img src="./image/1_50.png" alt="image-20230319000115766" style="zoom:50%;" />
<img src="./image/1_51.png" alt="image-20230319000115766" style="zoom:50%;" />
## 实现指令的使用
若你使用本教程提供的demo和工具进行学习那么参照以下流程在两个地方输入同样的实现指令即可绑定商品和发货零件。如果你是经验丰富的开发者可参阅<a href="../../../mcguide/26-联机大厅/6-联机大厅商品2.0文档.html">联机大厅商品2.0文档</a>完全自行实现。
首先拟好一个实现指令例如vip1在开平输入vip1。
<img src="./image/1_46.png" alt="image-20230319000115766" style="zoom:50%;" />
创建一个Vip1零件取名随意继承ShipBase。在此零件中编写Python代码或绑定蓝图实现此VIP特权功能。
![image-20220901182537714](./image/4_9.png)
在零件的属性面板中,输入实现指令。
![image-20220901182537714](./image/4_10.png)
这样一来,开平上的这个商品和这个零件就绑定了。玩家购买这个商品,所有发货零件会收到通知,但其他零件不会反应,而这个零件会检测到是它负责的商品,会执行发货实现逻辑。
```python
def COnPlayerBrought(self, playerId, expireTime=-1.0, newBuy=False, orderTime=None): # 当玩家购买此商品
preset = self.GetParent().ToEffectPreset()
preset.Play() # (实现你的逻辑)
def COnPlayerExpired(self, playerId): # 当玩家商品权限过期
preset = self.GetParent().ToEffectPreset()
preset.Stop() # (实现你的逻辑)
```
## 更好的包装与宣传
上传好了商品,实现了功能,同时要注重包装与宣传,才能获得更高的销量。下图展示了商品宣传图片出现的位置,帮助你对美术素材准备建立预期。
![未标题-1](./image/6_49.png)
下图展示了一段真实的游戏内商品购买的流程场景,观察**商品宣传图片**、**介绍文本**出现的位置,帮助你对美术素材准备建立预期。
![item_ship](./image/buy1.gif)
更多关于商品宣传的信息,参阅[宣传素材的设计与制作](../作品推广基础教程/1-宣传素材的设计与制作.html)。

View File

@@ -0,0 +1,32 @@
---
front:
hard: 入门
time: 5分钟
---
# 售卖商品课后作业
1.当我想售卖一个有效期30天的会员皮肤如下哪个定价方案更合理
- A. 100000钻石。
- B. 100000绿宝石。
- C. 200钻石。
- D. 免费并倒贴玩家50块。
<!--解答答案C。钻石和人民币的兑换比是1001绿宝石来自免费积分行为。 **合理的定价能使玩法走得更远。**-->
2.以下哪些打开neteaseStore的方法是正确的多选题
- A. 进入游戏时,设置自带按钮显示,让玩家主动点击进入商店。
- B. 觉得自带按钮不美观制作一个自己玩法的UI按钮点击按钮进入商店。
- C. 通过玩法引导产生弹窗“解锁此功能需要购买xx商品是否跳转到商店若点击是拉起商店。
- D. 读取玩家意念,当玩家想买东西时,自动打开商店。
<!--解答答案ABC。①由`HideNeteaseStoreGui`接口实现, ②③由`OpenNeteaseStoreGui`接口实现。-->

View File

@@ -0,0 +1,681 @@
---
front:
hard: 进阶
time: 40分钟
---
# 实现发货逻辑
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=634681ada240f794f8c6eaef" width="800" height="600" allow="fullscreen"/>
**2023/3月更新**
- 新增逻辑编辑器部分教学。
- **ShipBase** 更新了v2版本优化了部分代码、增加向预设系统广播事件、增加对蓝图的支持。点击下载[订单轮询、发货零件v2包](https://g79.gdl.netease.com/OrderPollAndShipBase_v2.zip)。
- **完整demo下载** [联机大厅内购教程demo包v1_Python版](https://g79.gdl.netease.com/in_game_purchases_demo.zip)[联机大厅内购教程demo包v2蓝图版](https://g79.gdl.netease.com/in_game_purchases_demo_v3.zip)。
## 订单轮询、发货零件
工欲善其事,必先利其器。为了使新手开发者更快上手商品的制作,编者准备了两个工具零件,只要在编辑器里使用这两个零件,即可很轻松就实现一个简单的商品。
它们没有经过太多测试验证,建议仅供学习参考,慎重投入生产环境使用。
![image-20220916053416616](./image/2_1.png)
不要被复杂的图片吓到,这个只是为了更好的说明各部件之间的关系,以及它们如何工作。请下载并导入这两个零件,让我们从一个最简单的物品商品开始做起。
<img src="./image/2_2.png" alt="image-20220916053656047" style="zoom:50%;" />
## 美味鲜菇
首先将订单轮询OrderPoll零件挂载到GM类预设下没有就创建一个
*上一章已经提到GM类预设指GameMananger通常是勾选常加载、预加载的空预设
<img src="./image/2_3.png" alt="image-20220916055117184" style="zoom:50%;" />
跟随上面下载的两个零件还会下载到一个物品发货ItemShip零件是使用上述两个零件的一个例子。点击玩家预设将物品发货零件挂载到玩家预设下。
![image-20220916055334895](./image/2_4.png)
展开物品发货零件的属性,可以看到如下配置:
![image-20220916055422496](./image/2_5.png)
其中实现指令类型、key、值都根据开平中商品的实现指令配置来填写例如上图对标的开平中实现指令是`{"code":2001}`。当然写`{"code": 2001, "text": "美味鲜菇", "version": 1}`也是可以的。
物品一局里使用掉就没有了,故不属于持久化商品,不勾选;打印事件信息在开发阶段建议勾选以方便调试。
以上都是发货ShipBase零件默认提供的配置剩下一个`给予物品列表`是物品发货ItemShip零件提供的配置。
使用喜闻乐见的UI配置好这个商品要发货的物品列表
<img src="./image/2_6.png" alt="image-20220916060234166" style="zoom:50%;" />
<img src="./image/2_7.png" alt="image-20220916060249795" style="zoom:50%;" />
Congratulations! 你的第一个商品制作完成!
打包作品并自测,进入游戏查看效果:
![item_ship](./image/item_ship.gif)
## 治疗师
由于治疗师零件为了实现职业功能已经足够复杂且已经继承自TriggerPart我们新建一个治疗师发货HealerShipPart零件把权限判定独立出来。
创建空零件继承ShipBasePart命名为HealerShipPart
![image-20220916061728522](./image/2_8.png)
由于治疗师的商品实现指令是`{"code": 1001}`所以我们值就填1001。勾选持久化商品因为职业商品是要求玩家退出换房后仍然记住玩家已购买的状态。
云储存表名按照建议编一个,上线后玩家的购买信息就会保存到这个名称的容器下,若你突然更改,所有之前已购买的玩家都会失去购买记录,而出现权限丢失的情况。当然只要没有覆写或删除,只要改回去就会一切恢复正常,这么说是为了帮助理解它的含义。
治疗师是一个购买后永久生效的商品,所以不需要勾选定时商品。
<img src="./image/2_9.png" alt="image-20220916061843338" style="zoom:50%;" />
要写点代码了,不过不多。首先编辑`HealerPart.py`,让治疗师零件将玩家变成治疗师之前,询问一下其他零件要不要阻止:
```python
def OnTriggerEntityEnter(self, e):
for entityId in e['EnterEntityIds']:
if entityId in self.GetLoadedPlayers():
self.NotifyOneMessage(entityId, '你尝试选择治疗师职业')
eventData = {
'playerId': entityId,
'role': self.classType.replace('Part', ''),
'cancel': False,
}
self.BroadcastPresetSystemEvent('PlayerTryChoiceRoleEvent', eventData)
if not eventData['cancel']:
self.TurnHealer(entityId)
```
可见,如果监听`PlayerTryChoiceRoleEvent`事件并cancel玩家就没有办法选择治疗师职业。
接着编辑`HealerShipPart.py`,监听这个事件,并调用一个发货零件提供的接口`IsPlayerService`传入一个玩家id它将返回当前此玩家有没有权限享受此商品的服务。若玩家没有权限也就是没有购买或购买过但过期了就cancel掉事件并友好的提示玩家。此时可以发挥才艺弹出一些吸引消费的浮动窗口之类的总之你懂的。
- IsPlayerService 服务端
- method in ShipBasePart
- 描述
- 获取当前此玩家是否有权限享受此商品的服务
- 返回值bool
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
```python
def InitServer(self):
"""
@description 服务端的零件对象初始化入口
"""
ShipBasePart.InitServer(self)
self.ListenPresetSystemEvent('PlayerTryChoiceRoleEvent', self, self.PlayerTryChoiceRoleEvent)
def PlayerTryChoiceRoleEvent(self, e):
if e['role'] == self.classType.replace('ShipPart', ''):
if not self.IsPlayerService(e['playerId']):
self.NotifyOneMessage(e['playerId'], '你还没有购买治疗师职业')
e['cancel'] = True
```
完成啦进入游戏查看效果这个gif有点长于是把购买部分加速了玩家模型透明了不要问我为啥我也不知道
![healer_ship](./image/healer_ship.gif)
## 会员特效
由于会员特效零件本身并不复杂,主要就是在客户端加载时调用预设的播放方法:
```python
def COnUIInitFinished(self, e):
self.GetParent().ToEffectPreset().Play()
```
所以重新创建一个VipEffectShip零件并继承ShipBase即可将原本上面的代码修改为下面
```python
def COnPlayerBrought(self, playerId, expireTime=-1.0, newBuy=False, orderTime=None):
preset = self.GetParent().ToEffectPreset()
preset.Play()
def COnPlayerExpired(self, playerId):
preset = self.GetParent().ToEffectPreset()
preset.Stop()
```
`COnPlayerBrought`是ShipBase提供的一个可供重写的客户端事件声明同名函数即可监听当**玩家购买商品**或**已购买过的玩家进入房间时**会触发,所以直接在它下面调用预设的播放即可。
| 参数名 | 数据类型 | 说明 |
| ---------- | -------- | ----------------------------------- |
| playerId | str | 玩家id |
| expireTime | flout | 商品到期时间戳 |
| newBuy | bool | 是否新购买False则为购买过进入房间 |
| orderTime | flout | 订单创建时间戳(购买时间) |
同理,`COnPlayerExpired`是**玩家权限过期时**会被触发,则停止特效的播放。
展开会员特效零件的属性栏,像刚才一样配置好实现指令、云储存表名。
<img src="./image/2_10.png" alt="image-20220916065857753" style="zoom:50%;" />
勾选定时商品。
![image-20220916070022911](./image/2_11.png)
定时类型选择第二种。
![image-20220916065956812](./image/2_12.png)
比如我们想让这个商品每购买一次增加10分钟有效期每秒检查一次是否过期就设置为如下图
![image-20220916070009770](./image/2_13.png)
保存零件,打包作品并自测,进入游戏查看效果:
![vip_effect_ship](./image/vip_effect_ship.gif)
####
## 治疗师(蓝图部分)
在此之前我们已经创建好了发货零件。请区分好发货零件和实现零件,不要混淆。
- 发货零件是继承自ShipBase的空零件负责识别实现指令和处理云表单、计时。
- 实现零件(蓝图)负责商品的实际功能。
由于蓝图零件无法继承其他零件,所以我们只能使用两两一组的绑定形式来制作,不过不要紧,绑定制作法并不复杂。
![vip_effect_ship](./image/6_0.png)
在发货零件的属性栏选择实现方式为blueprint蓝图。然后访问[在线生成uuid网站](https://www.uuidgenerator.net/)生成一个uuid填入 **蓝图识别uuid** 。此uuid是用于两个零件之间监听事件时识别绑定。
![vip_effect_ship](./image/6_1.png)
![vip_effect_ship](./image/6_2.png)
进入逻辑编辑器,编辑实现零件绑定的蓝图。
回顾之前制作的实现蓝图,我们在玩家尝试选择治疗师职业后,为玩家执行 **f_Turnhealer** 接口。
![vip_effect_ship](./image/6_3.png)
现在我们要对玩家权限进行判定,如果玩家没有购买商品,则不执行 **f_Turnhealer** 接口,并发一条消息告诉玩家你没有购买。
如何判断玩家有没有购买商品?获取到绑定的发货零件 **HealerShipPart** ,调用其接口[IsPlayerService](#接口)会返回一个玩家是否购买商品的bool值。
首先断开之前的执行连接线。
![vip_effect_ship](./image/6_4.png)
右键输入 **GetGameObjectByTypeName** ,调出节点。
![vip_effect_ship](./image/6_5.png)
对象类型选择str输入治疗师发货零件的文件名 **HealerShipPart**
![vip_effect_ship](./image/6_6.png)
右键输入 **partapi** ,调出节点。
![vip_effect_ship](./image/6_7.png)
右侧属性栏面板, **调用接口** 输入IsPlayerService。
![vip_effect_ship](./image/6_8.png)
添加一个输入参数命名playerId。
![vip_effect_ship](./image/6_9.png)
将获取到的游戏对象,作为调用对象。创建数据连接线。
![vip_effect_ship](./image/6_10.png)
为playerId参数引入数据从之前的遍历节点输出值位置创建连接线。
![vip_effect_ship](./image/6_11.png)
调出布尔值比较节点。
![vip_effect_ship](./image/6_12.png)
**IsPlayerService** 接口返回的结果为 **是** ,则为玩家执行 **f_Turnhealer** 接口。按照下图创建连接线,打勾。
![vip_effect_ship](./image/6_13.png)
创建执行连接线。
![vip_effect_ship](./image/6_14.png)
若玩家没有购买,还要发送消息提示。输入 **oneme** ,调出节点。
![vip_effect_ship](./image/6_15.png)
创建数据连接线传输playerId。
![vip_effect_ship](./image/6_16.png)
消息内容输入 **你没有购买治疗师**
![vip_effect_ship](./image/6_17.png)
按照图示创建执行连接线当IsPlayerService返回 **否** 的时候向玩家发送消息。
![vip_effect_ship](./image/6_18.png)
## 会员特效(蓝图部分)
在此之前我们已经创建好了发货零件。请区分好发货零件和实现零件,不要混淆。
- 发货零件是继承自ShipBase的空零件负责识别实现指令和处理云表单、计时。
- 实现零件(蓝图)负责商品的实际功能。
由于蓝图零件无法继承其他零件,所以我们只能使用两两一组的绑定形式来制作,不过不要紧,绑定制作法并不复杂。
![vip_effect_ship](./image/6_19.png)
在发货零件的属性栏选择实现方式为blueprint蓝图。然后访问[在线生成uuid网站](https://www.uuidgenerator.net/)生成一个uuid填入 **蓝图识别uuid** 。此uuid是用于两个零件之间监听事件时识别绑定。
![vip_effect_ship](./image/6_1.png)
![vip_effect_ship](./image/6_20.png)
进入逻辑编辑器,编辑实现零件绑定的蓝图。
回顾之前制作的实现蓝图我们在游戏ui加载完成时获取父预设特效预设转换类型然后调用播放特效接口。
![vip_effect_ship](./image/6_21.png)
现在我们将这个流程更换为,监听 **ShipBase** 的两个事件 **COnPlayerBrought****COnPlayerExpired** **当玩家购买商品****已购买商品的玩家进入房间** 时调用Play()播放特效, **当玩家商品权限过期** 时调用Stop()停止播放特效。
首先我们将播放和停止包装成两个自定义接口。在左侧自定义接口栏创建两个接口,分别命名为 **f_VipEffectPlay****f_VipEffectStop** 备注分别为VIP特效播放和VIP特效停止播放。
![vip_effect_ship](./image/6_22.png)
两个接口均要添加一个参数,命名为 **event** ,类型选择 **Any**
![vip_effect_ship](./image/6_23.png)
将刚才的播放流程使用Ctrl+X剪切到接口内
![vip_effect_ship](./image/6_24.png)
刚才生成的uuid要在这时派上用场。先断开第一条输入连接线。
![vip_effect_ship](./image/6_25.png)
右键 **获取属性** ,调出节点。
![vip_effect_ship](./image/6_26.png)
创建连接线,`key`选择`str`,输入`bpBindUuid`,获取`event``bpBindUuid`参数。
![vip_effect_ship](./image/6_27.png)
右键 **=** ,调出是否相等节点。
![vip_effect_ship](./image/6_28.png)
参数2选择`str`,输入刚才配套的发货零件的蓝图识别`uuid`
![vip_effect_ship](./image/6_29.png)
右键 **bool** ,调出布尔值比较节点。
![vip_effect_ship](./image/6_30.png)
按下图创建剩余连接线,当`蓝图识别uuid`等于`bpBindUuid`参数时,才继续执行后面的播放流程。
![vip_effect_ship](./image/6_31.png)
完成后将整个流程框选Ctrl+C复制。
![vip_effect_ship](./image/6_32.png)
粘贴到 **f_VipEffectStop** 接口中。
![vip_effect_ship](./image/6_33.png)
修复连接线。
![vip_effect_ship](./image/6_34.png)
删除播放预设节点。
![vip_effect_ship](./image/6_35.png)
右键 **停止播放特效** ,调出节点。
![vip_effect_ship](./image/6_36.png)
重建连接线。
![vip_effect_ship](./image/6_37.png)
返回Graph。
右键输入 **获取自身** ,调出节点。
![vip_effect_ship](./image/6_38.png)
右键输入 **获取属性** ,调出 **两个获取属性** 节点。
![vip_effect_ship](./image/6_39.png)
点击获取属性节点,右侧属性栏分别输入`key``f_VipEffectPlay``f_VipEffectStop`,也就是刚才创建的两个自定义接口名。
![vip_effect_ship](./image/6_40.png)
![vip_effect_ship](./image/6_41.png)
按照下图创建连接线。
![vip_effect_ship](./image/6_42.png)
右键 **监听预设系统事件** ,调出 **两个监听预设系统事件** 节点。
![vip_effect_ship](./image/6_43.png)
事件名称选择`str`,分别输入`COnPlayerBrought``COnPlayerExpired`
![vip_effect_ship](./image/6_44.png)
![vip_effect_ship](./image/6_45.png)
将零件自身作为目标对象,按照下图创建数据连接线。
![vip_effect_ship](./image/6_46.png)
将获取到的两个自定义接口作为回调函数,按照下图创建数据连接线。
![vip_effect_ship](./image/6_47.png)
最后,按照下图创建执行连接线,让监听本身得以执行。
![vip_effect_ship](./image/6_48.png)
**至此我们在章节三设计的三种商品均以实现发货功能。下面是ShipBase开放的接口和事件希望对你的使用有帮助。**
## 事件
- ## SOnPlayerBrought 服务端COnPlayerBrought 客户端
- 描述
- **当玩家购买商品**或**已购买商品的玩家进入房间**时触发
| 参数名 | 数据类型 | 说明 |
| ---------- | -------- | ----------------------------------- |
| playerId | str | 玩家id |
| expireTime | flout | 商品到期时间戳 |
| newBuy | bool | 是否新购买False则为购买过进入房间 |
| orderTime | flout | 订单创建时间戳(购买时间) |
- ## SOnPlayerExpired 服务端COnPlayerExpired 客户端
- 描述
- **当玩家商品权限过期**时触发
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## SOnPlayerNeverBought 服务端COnPlayerNeverBought 客户端
- 描述
- **当一个从未购买过此零件负责的商品的玩家进入游戏**时触发
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------------------------ |
| playerId | str | 玩家id |
| regTable | bool | 是否已经创建表,但空数据 |
## 接口
- ## IsMyService 服务端
- method in ShipBasePart
- 描述
- 此订单是否由本零件负责
- 返回值bool
| 参数名 | 数据类型 | 说明 |
| --------- | -------- | ---------------------------------- |
| orderBody | dict | QueryLobbyUserItem的cb提供的订单体 |
- ## IsPlayerService 服务端
- method in ShipBasePart
- 描述
- 获取当前此玩家是否有权限享受此商品的服务
- 返回值bool
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## GetPlayerLastServiceTime 服务端
- method in ShipBasePart
- 描述
- 获取当前此玩家商品权限剩余有效期,-1为永久或非定时商品
- 返回值flout/int
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## SetCloudPersistent 服务端
- method in ShipBasePart
- 描述
- 在本零件自己的云数据表中记录商品购买订单状态
- 返回值:无
| 参数名 | 数据类型 | 说明 |
| ---------- | -------- | ---------------------- |
| playerId | str | 玩家id |
| orderId | int | 订单id |
| orderTime | flout | 订单创建时间戳 |
| expireTime | flout | 商品到期时间戳,-1永久 |
- ## SetOrderShip 服务端
- method in ShipBasePart
- 描述
- 通知网易商店系统订单已发货
- 返回值:无
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
| orderId | int | 订单id |
- ## GetPlayerUid 服务端
- method in ShipBasePart / OrderPollPart
- 描述
- 同官方httpComp功能获取玩家uid
- 返回值str
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## ShipPlayer 服务端
- method in **OrderPollPart**
- 描述
- 此接口零件默认每4s调用一次你也可以手动调用检测玩家是否有未发货订单若有则启动发货流程
- 返回值:无
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## CheckExpire 服务端
- method in ShipBasePart
- 描述
- 此接口零件根据属性面板设置的频率自动调用你也可以手动调用检查玩家权限有效期若过期则取消权限并触发OnPlayerExpired
- 返回值:无
- ## DebugDelTableAllData 服务端
- method in ShipBasePart
- 描述
- **慎用**,清除云储存表所有订单数据,启用调试功能后聊天框输入`claer 表名`同功能
- 返回值:无
| 参数名 | 数据类型 | 说明 |
| -------- | -------- | ------ |
| playerId | str | 玩家id |
- ## DiffForHumans 服务端
- method in ShipBasePart
- 描述
- 将时间戳转换为人类友好语言(仅支持向前),例如`刚刚``x分钟前`这是个残血版建议只用来描述商品是何时购买的更好的在GitHub上
- 返回值:无
| 参数名 | 数据类型 | 说明 |
| --------- | --------- | ------ |
| timestamp | int/flout | 时间戳 |

View File

@@ -0,0 +1,173 @@
---
front:
hard: 进阶
time: 20分钟
---
# 实现发货逻辑课后作业
## Python向
在学习了如何通过零件实现发货逻辑后,就可以举一反三,完成下面的课后作业。
## 作业要求
- 阅读物品发货零件源码。
- 模仿物品发货零件,制作一个状态效果发货零件。
- 运行状态效果发货零件,测试效果。
## 操作步骤
1.阅读`ItemShipPartMeta.py`
```python
@sunshine_class_meta
class ItemShipPartMeta(ShipBasePartMeta):
CLASS_NAME = "ItemShipPart"
PROPERTIES = {
"itemList": PArray(text="给予物品列表", group="发货设置", sort=30, childAttribute=PDict(children={
"itemDict": PCustom(
sort=0,
text="物品选择",
editAttribute="MCItems",
default=("minecraft:wooden_sword", 0),
withNamespace=True,
withAuxValue=True,
isBlock=None,
),
"count": PInt(sort=1, text="物品数量", default=1)
})),
}
```
2.阅读`ItemShipPart.py`
```python
@registerGenericClass("ItemShipPart")
class ItemShipPart(ShipBasePart):
def __init__(self):
ShipBasePart.__init__(self)
# 零件名称
self.name = "物品发货零件"
self.itemList = [{'itemDict': ('bestmap:testItem1', 0), 'count': 1}]
self.cmdType = 0
self.cmdValue = 2001
def CanAdd(self, parent):
if not isinstance(parent, PlayerPreset):
return "物品发货零件只能挂在玩家预设下"
def SOnPlayerBrought(self, playerId, expireTime=-1.0, newBuy=False, orderTime=None):
parent = self.GetParent()
if not parent or parent.entityId != playerId:
return
for item in self.itemList:
self.SpawnItemToPlayerInv({
'newItemName': item['itemDict'][0],
'newAuxValue': item['itemDict'][1],
'count': item['count']
}, playerId)
```
3.分析
可以看到`ItemShipPartMeta.py`元数据文件将物品列表作为一项配置暴露在编辑器的属性栏中,使得对应的数据成员`itemList`可以被可视化编辑,进而在`SOnPlayerBrought`玩家购买商品时将物品发放到玩家背包。
4.模仿
创建StatusEffectShip零件继承自ShipBase。
![image-20220916082210883](./image/5_1.png)
修改零件中文名称,和默认属性。
![image-20220916082321038](./image/5_2.png)
编辑元数据文件主要注意sort建议30起步group设为和父零件一样的“发货设置”
```python
@sunshine_class_meta
class StatusEffectShipPartMeta(ShipBasePartMeta):
CLASS_NAME = "StatusEffectShipPart"
PROPERTIES = {
"effectList": PArray(sort=30, text="给予状态效果列表", group="发货设置", childAttribute=PDict(children={
"effectName": PStr(text="状态原版名称", sort=1, default="speed"),
"duration": PInt(text="持续时间", sort=2, default=1),
"amplifier": PInt(text="状态等级", sort=3, default=0),
"showParticles": PBool(text="显示粒子效果", sort=4, default=True)
})),
}
```
编辑逻辑文件的`__init__`方法添加名为effectList的成员。
```python
def __init__(self):
ShipBasePart.__init__(self)
self.name = "状态效果发货零件"
self.cmdValue = 1234
self.effectList = [] # 添加这个
```
重写`SOnPlayerBrought`然后遍历effectList给玩家添加状态以发货。
```python
def SOnPlayerBrought(self, playerId, expireTime=-1.0, newBuy=False, orderTime=None):
parent = self.GetParent()
if not parent or parent.entityId != playerId:
return
for effect in self.effectList:
self.AddEffectToEntity(playerId, effect['effectName'], effect['duration'], effect['amplifier'], effect['showParticles'])
```
## 测试
1.挂载到玩家预设下,配置一个状态效果
![image-20220916083229840](./image/5_3.png)
2.修改`OrderPollPart`->`ShipPlayer`方中的测试数据实现指令1234填写和零件设置一样的值
```python
if self.debugMode:
testData = {
'entity': {
'orders': [
{
"order_id": 123456789, # 订单id
"timestamp": time.time(), # 购买时间
"cmd": '{"code":1234}', # 实现指令
"product_count": 1 # 购买数量。目前不允许一次购买多个所以返回都是1
},
]
}
}
callback(testData)
else:
httpComp.QueryLobbyUserItem(callback, uid)
```
3.进入游戏,聊天框敲入`init``ship`,观察效果
![image-20220916083528364](./image/5_4.png)
## (蓝图版)实现发货逻辑课后作业
在第五章作业中,你设计了一个属于你的新职业,并独立完成制作。现在为你的新职业制作发货逻辑,并完成测试。
1. 创建发货零件,拆分实现逻辑和发货逻辑。
2. 使用逻辑编辑器就,参考本章正文,完成发货过程。
3. 提供两个主要思路用以参考:
1. 若使用**接口**完成发货,参考[治疗师](./4-为玩法设计内购商品.html?catalog=1#逻辑编辑器程序向实战-制作新职业)。
2. 若使用**事件**完成发货,参考[会员特效](./4-为玩法设计内购商品.html?catalog=1#逻辑编辑器程序向实战-制作会员特效)。
4. 运行游戏,测试效果。

Some files were not shown because too many files have changed in this diff Show More