仙剑出鞘游戏软件开发文档

[toc]

设计说明

1.软件结构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
├── App.ts           // 入口文件
├── Globle.d.ts // 全局定义的结构体
├── Base // 功能的一些封装
│   ├── BaseComponent.ts // 基类方法
│   ├── animation // 动画管理
│   ├── audio // 声音管理
│   ├── data // 数据管理
│   ├── event // 事件管理
│   ├── layer // 图层管理
│   ├── mvc // mvc管理
│   ├── sprite // 精灵管理
│   ├── storage // 本地存储管理
│   ├── timer // 时间管理
│   └── utils // 基础工具类
├── App // 当前游戏相关
│   ├── runtimeData // 运行时数据
│   │   └── GameDataManager.ts // 游戏数据管理
│   ├── pool // 游戏中的对象池
│   │   ├── items // 对象池内的prefab个体
│ │ │   ├── SpriteItem.ts // 具体精灵逻辑
│ │ │   ├── MagicPointItem.ts // 具体精灵逻辑
│ │   │ └── ......
│ │ ├── SpritePool.ts // 精灵对象池
│ │ ├── MagicPointPool.ts // 点阵对象池
│   │ └── ......
│   ├── scene // 场景
│   │   ├── GameScene.ts // 游戏场景资源加载
│   │   ├── HomeScene.ts // 主页场景资源加载
│   │   └── LoadingScene.ts // 加载场景资源加载
│   ├── protocol // 数据结构体
│   └── ui // 对接cocos编辑器的ui操作
│      ├── dialog // 弹窗组件
│ │   ├── MailDialog.ts // 邮件弹窗
│ │   ├── MissionOverDialog.ts // 结算弹窗
│    │ └── ......
│      ├── panle // 面板组件
│ │   ├── MainCityPanle.ts // 主城面板
│    │ └── ......
│      ├── HomeUI.ts // 主页ui中事件
│      ├── LoadingUI.ts // 加载ui中事件
│      └── GameUI.ts // 游戏ui中事件
└── service // 联网服务及其他第三方服务
   ├── LoginService.ts // 登录服务(微信,抖音)
   ├── MessageService.ts // 消息服务,推送及解析
   └── WebsocketService.ts // 长连接服务 重连及广播

2.程序执行加载图

![未命名文件 -3-](http://image.jk-kj.com/mweb/2021/08/21/16295026425385未命名文件 -3-.png)

3.通讯接口设计

与后端通讯主要是websocket+probuf,登录等少量接口采用http

3.1 Websocket通信

长连接链接,实时反馈玩家间的操作及服务端数据返回的及时展现处理

3.1.1 MessageServer

MessageServer主要处理websocket的数据发送和数据解析协议头部

service/MessageServer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { msg } from "../app/protocol/msg";
import EventManager from "../base/event/EventManager";
import BufferUtil from "../base/utils/BufferUtil";
import { WebsocketService } from "./WebsocketService";

export class MessageService {

private msgidNameArray:Array<string>

static ProtocolData={
Test:0, // 基础测试
ChatREQ:5, // 发起聊天
ChatACK:6, // 接受聊天
FireREQ:7, // 发起攻击
FireACK:8, // 接受攻击
SpriteDataREQ:9, // 获取怪物分布
SpriteDataACK:10, // 通知怪物分布
MissionOver:11, // 关卡结算
EventREQ:12, // 通用简单事件
EventACK:13, // 通用简单事件
EventGetGoodsItemArray:14, // 获得奖励
}

constructor() {
// 互换 ProtocolData 的键值对
let ProtocolData = MessageService.ProtocolData
this.msgidNameArray = []
for (let k in ProtocolData) {
let value = ProtocolData[k]; //将原来的value赋值给一个变量
this.msgidNameArray[value] = k;
}
}

dealMessage(id: number,data: Uint8Array ){
let message_name = this.msgidNameArray[id]
if(message_name == null){
console.log('没有此类型的msgid')
return
}
console.log("get " + message_name + " message!");
let message = msg[message_name].decode(data);
let eventManager = MyGame.SingletonFactory.getInstance(EventManager);
eventManager.emit("WebsocketService:deal"+message_name, message);
console.log('eventManager.emit("WebsocketService:deal'+message_name+'", message)',message);
}

// 对接 easyswoole 的json发送
private _sendBuffMsgForPHP(proName:string,data:{}){
let msgid = MessageService.ProtocolData[proName]
let message = msg[proName].create(data);
let buffer = msg[proName].encode(message).finish();
let addmsgid_uint8array = BufferUtil.protoBufAddMsgid(msgid,buffer); //前两位为协议序号,故需包装一下
let websocketService = MyGame.SingletonFactory.getInstance(WebsocketService);
if (cc.sys.platform == cc.sys.WECHAT_GAME) {
// 【重要】此方法再当前版本的 微信开发者工具中,会不发送,实际是可用的
websocketService.send(addmsgid_uint8array.buffer.slice(addmsgid_uint8array.byteOffset, addmsgid_uint8array.byteLength + addmsgid_uint8array.byteOffset))
}else{
websocketService.send(addmsgid_uint8array)
}
}

public sendChatREQTest(){
let chatreq = new msg.ChatREQ();
let message = new msg.ChatMessage();
message.Type = "text";
message.Content = "test";
chatreq.Message = message
chatreq.ToId = 0
chatreq.ChatToType = msg.ChatToType.WORLD
this._sendBuffMsgForPHP('ChatREQ',chatreq)
console.log(JSON.stringify( chatreq.toJSON()))
}

// 发送攻击请求
public sendFireREQByMagicPointArray(gameData,magicPointArray:Array<msg.MagicPoint>,lineACKArray:Array<msg.LineACK>,delayACKArray:Array<msg.DelayACK>){
let firereq = new msg.FireREQ();
firereq.SwordMagicAngle = 30;
firereq.MagicPointArray = magicPointArray
firereq.LineACKArray = lineACKArray
firereq.DelayACKArray = delayACKArray
firereq.RoomId = gameData.RoomId
firereq.MissionId = gameData.playMissionId
this._sendBuffMsgForPHP('FireREQ',firereq)
}

// 发送获取精灵分布请求
public sendSpriteDataREQ(mission_id:number,room_id:number){
let req = new msg.SpriteDataREQ();
req.MissionId = mission_id;
req.RoomId = room_id
this._sendBuffMsgForPHP('SpriteDataREQ',req)
}

// 通用事件请求
public sendEventREQ(type:string,data:string){
let req = new msg.EventREQ();
req.EventType = type;
req.EventData = data
this._sendBuffMsgForPHP('EventREQ',req)
}

// 通信信息请求
public sendTextMsg(text:string){
let message = new msg.ChatMessage();
message.Type = "text";
message.Content = text;
let chatack = new msg.ChatACK();
chatack.Message = message
chatack.ToId = 0
chatack.ChatToType = msg.ChatToType.WORLD
this._sendBuffMsgForPHP('ChatREQ',chatack)
}

}

3.1.2 WebsocketService

WebsocketService主要处理长连接的连接与重连。并通过事件广播通知出去
service/WebsocketService.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

import EventManager from "../base/event/EventManager";
import { LocalStorageManager } from "../base/storage/StorageManager";
import BufferUtil from "../base/utils/BufferUtil";
import { MessageService } from "./MessageService";
import { LayerManager } from "../base/layer/LayerManager";

// 心跳管理
let heartManager = {
ws:null,
checkTime: 20*1000, // 心跳检测时长
checkTimeObj: null, // 定时变量
heartTime: 10*1000, // 心跳间隔
heartTimeObj:null,
// 设置当前的ws
setWebsocket:function(ws){
this.ws = ws
},
resetCheckTimeout: function () { // 重置定时
if(this.checkTimeObj != null){
clearTimeout(this.checkTimeObj);
}
this.checkTimeObj = setTimeout( () => {
console.log('===== 心跳时间内收不到消息,主动触发连接关闭,开始重连=====')
this.ws.close();
},this.checkTime)
},
startHeart:function(){
if(this.heartTimeObj != null){
clearTimeout(this.heartTimeObj);
}
this._heartLoop()
},
_heartLoop:function(){
this.heartTimeObj = setTimeout( () => {
if(this.ws.readyState == 1){
this.ws.send('ping')
}
this._heartLoop()
},this.heartTime)
}
}
// 长连接延时发送管理
let delaySendManager = {
delayMaxTime:10*1000, // 接受最大延迟时间
delayCallbackArray:[],
sendDelay:function(){
if(this.delayCallbackArray.length > 0){
this.delayCallbackArray.forEach( (callback) => {
callback() // console.log('发送未连接好时的事件')
})
this.delayCallbackArray = []
}
},
pushDelay:function(callback){
this.delayCallbackArray.push(callback)
}
}


export class WebsocketService {

public ws:WebSocket
lockReconnect:boolean = false

constructor() {
this.init();
}

/**
* 初始化参数
*/
private init() {
let localStorageManager = MyGame.SingletonFactory.getInstance(LocalStorageManager);
let token = localStorageManager.getString('token')
let ws = new WebSocket("wss://xianjian.jk-kj.com/websocket?appkey=xianjian&token="+token);
ws.onopen = this.onopen.bind(this);
ws.onerror = this.onerror.bind(this);
ws.onmessage = this.onmessage.bind(this);
ws.onclose = this.onclose.bind(this);
ws.binaryType = "arraybuffer";
heartManager.setWebsocket(ws)
this.ws = ws
}
onopen(evt) {
console.log("Connection open ...");
heartManager.startHeart()
heartManager.resetCheckTimeout();
delaySendManager.sendDelay()
};
onmessage(evt) {
heartManager.resetCheckTimeout(); //拿到任何消息都说明当前连接是正常的
let messageService = MyGame.SingletonFactory.getInstance(MessageService);
if (cc.sys.platform == cc.sys.WECHAT_GAME) {
if(typeof evt.data == 'string'){
console.log('真机上的11223355')
// 真机上的
let buf = BufferUtil.stringToUint8Array(evt.data)
console.log(buf)
let retdata = BufferUtil.parseArrayBufferToMsgAndUint8Array(buf);
let msgid = retdata.msgid;
let data = retdata.data;
messageService.dealMessage(msgid,data);
}else if (evt.data instanceof ArrayBuffer ){
console.log('微信开发者工具上11223355')
console.log(evt.data)
// 微信开发者工具上
let retdata = BufferUtil.parseArrayBufferToMsgAndUint8Array(evt.data);
console.log(retdata)
let msgid = retdata.msgid;
let data = retdata.data;
messageService.dealMessage(msgid,data);
}
}else{
if( evt.data instanceof ArrayBuffer){
console.log(evt.data)
let retdata = BufferUtil.parseArrayBufferToMsgAndUint8Array(evt.data);
console.log(retdata)
let msgid = retdata.msgid;
let data = retdata.data;
messageService.dealMessage(msgid,data);
}else if (evt.data instanceof Blob ){
console.log('Blob todo')
}else{
// 当前为字符串
if(evt.data[0] == '{'){
console.log('josn todo')
}else{
console.log('string todo')
}
}
}
};
onclose(evt) {
console.log("Connection closed.");
this.reConnection()
};
onerror(evt){
console.log("websocket onerror");
console.log(evt)
this.reConnection()
}
reConnection() {
console.log('尝试重连')
let layerManager = MyGame.SingletonFactory.getInstance(LayerManager);
layerManager.showError("尝试重连");
if (this.lockReconnect) return;
this.lockReconnect = true;
setTimeout( () => {
//没连接上会一直重连,设置延迟避免请求过多
this.init();
this.lockReconnect = false;
}, 2000);
}
public send(data){
this.ready( ()=>{
this.ws.send(data)
})
}
/**
* ready,第一次使用时需要
*/
public ready(callback) {
if(this.ws.readyState == 1){
console.log('ws已准备好')
callback()
}else{
console.log('ws未准备好,将长连接请求事件先存储到临时队列中')
delaySendManager.pushDelay(callback)
}
}
}

3.2 消息结构体

msg.proto与后端的通讯信息结构
定义了每个消息体的结构,通过脚本转msg.d.ts可获得ts版本的对象定义,通过脚本转PHP可获得php版本的对象定义,方便前后端通信的消息体的统一。

protocol/msg.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
syntax = "proto3";
package msg;
option go_package = "./;msg";


// 玩家对象
message Player{
string PlayerNo = 1; // 用户编码 如 f12-001 // 服务器12的001用户
string NickName = 2; // 昵称
string PicNo = 3; // 头像编码(本地资源编码)
}

// test
message Test {
string Test = 1;
}

// ========================= game scene start ==================

enum MissionOverType {
LOSS=0; // 失败
WIN=1; // 获胜
}

enum SpriteEventType {
SUB_HP=0; // 扣血
ADD_HP=1; // 回血
ADD_BUFF=2; // 加BUFF
SUB_BUFF=3; // 减BUFF
}

// 魔法点
message MagicPoint {
uint32 Index=1;
uint32 Power=2;
uint32 Type=3;
}

// 精灵抽象(不带差异属性)
message Sprite {
string SpriteNo=1;
string SpriteName=2;
uint32 SpriteType=3;
string PicNo=4;
string InfoNo=5;
}

// 实例化精灵(除编码和名称外,含其他有可变属性)
message SpriteItem {
uint32 HP=1;
uint32 MP=2;
uint32 MaxHP=3;
uint32 MaxMP=4;
int32 NodeX=5;
int32 NodeY=6;
int32 RoomId=7;
string PlayerNo=8;
string SpriteNo=9;
string SpriteName=10;
uint32 SpriteIndex=11;
string BuffOutTimes=12;
}

// 怪物身上的buff
message Buff {
string BuffNo=1;
string BuffName=2;
uint32 BuffType=3; // 同类型的buff会被替换
uint32 IsGood=4;
}

// 请求获取关卡等级的怪物当前情况
message SpriteDataREQ {
uint32 MissionId=1;
uint32 RoomId=2;
}

// Sprite场内分布情况
message SpriteDataACK {
uint32 MissionId=1; // 关卡战役id
uint32 RoomId=2; // 所处房间
uint32 Round=3; // 第几回合
repeated SpriteItem SpriteItemArray=4; // 精灵分布情况
repeated Player PlayerArray=5; // 玩家
}

// 精灵扣血、加血、加BUFF事件
message SpriteEvent {
uint32 SpriteIndex=1; // 场内经理索引
SpriteEventType SpriteEventType=2;
uint32 Value=3;
}

// 消息通知
message MissionOver{
MissionOverType MissionOverType=1;
repeated GoodsItem GoodsItemArray=2; // 多个物品
}

// 攻击线上
message LineACK {
uint32 FromIndex=1;
uint32 ToIndex=2;
repeated SpriteEvent SpriteEventArray=3;
}

// 延迟攻击
message DelayACK {
repeated SpriteEvent SpriteEventArray=2;
}

// 攻击发送
message FireREQ {
uint32 SwordMagicAngle=1; //
repeated MagicPoint MagicPointArray=2; // 发送的point
uint32 RoomId=3; // 所在房间
uint32 MissionId=4; // 所在关卡
repeated LineACK LineACKArray=5; // 直线攻击 , 可以将计算放服务器上
repeated DelayACK DelayACKArray=6; // 延迟攻击, 可以将计算放服务器上
}

// 攻击送达
message FireACK {
Player From=1;
uint32 SwordMagicAngle=2; //
repeated MagicPoint MagicPointArray=3; // 发送的point
uint32 RoomId=4; // 所在房间
uint32 MissionId=5; // 所在关卡
repeated LineACK LineACKArray=6; // 直线攻击
repeated DelayACK DelayACKArray=7; // 延迟攻击
}

// ========================= game scene end ==================

// 简单事件请求
message EventREQ{
string EventType=1;
string EventData=2;
}

// 通用简单事件
message EventACK{
string EventType=1;
string EventData=2;
}

// 事件:获得奖励物品
message EventGetGoodsItemArray{
string GetFromName=1; // 奖励来源名称
repeated GoodsItem GoodsItemArray=2; // 多个物品
}


// ========================= goods start ==================
enum GoodsType{
GOLD=0; // 金币
DIAMOND=1; // 钻石
GEM=2; // 宝石
FLYINGSWORD=3; // 飞剑
EXP=4; // 经验
}

// 抽象物品(不带差异属性)
message Goods{
string GoodsNo=1; // 物品编号
string GoodsName=2; // 物品名称
GoodsType GoodsType=3; // 物品类型
string PicNo=4; // 物品图片编码
string InfoNo=5; // 物品描述编码
uint32 Pirce=6; // 物品价格
}

// 实例化物品(除编码和名称外,含其他有可变属性)
message GoodsItem{
string GoodsName=1; // 物品
string GoodsNo=2; // 物品编码 根据GoodsNo去获取Goods的公共属性
uint32 Num=3; // 数量
}

// ========================= goods end ==================

// ========================= chat start ==================

enum ChatToType {
WORLD=0; // 世界聊天 ToId=0
UNION=1; // 工会
USER =2; // 用户
}

// 消息体
// 如 type=text Content="你好"
// 如 type=image Content=[image] Attr='{picUrl:"picUrl",width:"200px"}'
message ChatMessage{
string Type = 1;
string Content = 2;
string Attr = 3;
}

// 消息发送
message ChatREQ {
ChatMessage Message=1;
uint32 ToId=2;
ChatToType ChatToType=3;
}

// 消息送达
message ChatACK {
ChatMessage Message=1;
uint32 ToId=2;
ChatToType ChatToType=3;
Player From=4;
}

// ========================= chat end ==================

4.模块功能

4.1 基本模块的封装

为方便拓展及开发,对原cocos creator的一些原有方法和功能进行了符合自身要求的封装

4.1.1 动画管理

封装了资源的解析及组合动画帧的功能。对外只需传入:精灵编码,动作名,及挂载节点,就可以在节点上上播放帧动画

主要方法:
  • playAnimation(SpriteNo:string,playName:string,curNode:cc.Node)
    • SpriteNo:string 精灵编码
    • playName:string 动作名
    • curNode:cc.Node 在哪个节点上

4.1.2 音效管理

封装了声音资源的加载及音量的整体控制的功能。

主要方法:
  • playMusic(AudioNo:sting)
    • AudioNo: 音乐资源

4.1.3 事件管理

对事件进行的简单封装

主要方法:
  • emit(eventName: string, data?: any | void, target: Object = null)

    • eventName 事件名
    • data 参数
    • target 对象
  • on(eventName: string, handler: Function, target: Object = null)

    • eventName 事件名
    • handler 回调
    • target 对象
  • off(eventName: string | EventObject, target: Object = null)

    • eventName 事件名
    • target 对象

4.1.4 图层管理

主要对图层的加载,显示隐藏进行一个简单的封装

主要方法:
  • register(Panle,targetNode:cc.Node)
    • Panle 面板图层
    • targetNode 对象节点
  • showPanle(name:string)
    name 图层名
  • hidePanle(name:string)
    name 图层名

4.1.5 精灵管理

对精灵的资源进行管理的封装。通过简单传入:精灵编码,就能显示精灵的纹理图片

主要方法:
  • playAnimationBySpriteNo(spriteNo:string,actionName:string,targetNode:cc.Node) - Panle 面板图层
    • spriteNo 精灵编码
    • actionName 动作名
    • targetNode 对象节点

4.2 游戏模块

4.2.1 主页

因为主页的特殊性,及适配问题,将主页又划分了,上中下左右五个UI

ui/HomeMainUI.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const {ccclass, property} = cc._decorator;
import { BaseComponent } from "../../base/BaseComponent";
import { MessageService } from "../../service/MessageService";
@ccclass
export default class HomeMainUI extends BaseComponent {

onLoad(){
console.log('====HomeMainUI onLoad ===')
}

start () {

}

onStartPlayClick(){
let playData = this.gameDataManager.getPlayData()
playData.playMissionId = 2
this.gameDataManager.setPlayData(playData)
MyGame.goGame()
// 发送
let messageservice = MyGame.SingletonFactory.getInstance(MessageService);
messageservice.sendSpriteDataREQ(playData.playMissionId,playData.RoomId)
}


}

ui/HomeBottomUI.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57


const {ccclass, property} = cc._decorator;
import { BaseComponent } from "../../base/BaseComponent";

import HeroPanle from "./panle/HeroPanle";
import PackagePanle from "./panle/PackagePanle";
import LandMapPanle from "./panle/LandMapPanle";
import MainCityPanle from "./panle/MainCityPanle";
import TavernPanle from "./panle/TavernPanle";
import ForgePanle from "./panle/ForgePanle";

@ccclass
export default class HomeBottomUI extends BaseComponent {

@property(cc.Node)
panleNodeHook:cc.Node = null

onLoad(){
console.log('====HomeBottomUI onLoad ===')
}

start () {

}

onHeroPanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(HeroPanle,this.panleNodeHook);
}

onPackagePanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(PackagePanle,this.panleNodeHook);
}

onLandMapPanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(LandMapPanle,this.panleNodeHook);
}

onMainCityPanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(MainCityPanle,this.panleNodeHook);
}

onTavernPanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(TavernPanle,this.panleNodeHook);
}
onForgePanleBtnClick(){
this.uiManager.closeAllPanle()
this.uiManager.showUI(ForgePanle,this.panleNodeHook);
}

}

4.2.2 游戏页面

ui/GameMainUI.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
const {ccclass, property} = cc._decorator;
import { BaseComponent } from "../../base/BaseComponent";
import { msg } from "../protocol/msg";
import { MessageService } from "../../service/MessageService";
// import FlyingswordPool from "../../pool/FlyingswordPool"
import SpritePool from "../pool/SpritePool"
import MagicPointPool from "../pool/MagicPointPool"
import { pInQuadrangle } from "../../base/utils/OtherUtil";

@ccclass
export default class GameMainUI extends BaseComponent {

// @property(FlyingswordPool)
// flyingswordPool:FlyingswordPool = null

@property(SpritePool)
spritePool:SpritePool = null

@property(MagicPointPool)
magicPointPool:MagicPointPool = null

@property(cc.Node)
smallflyingsword:cc.Node = null // 小飞剑

onLoad() {
// 监听 攻击消息推送
this.bindEvent('WebsocketService:dealFireACK',this.flyswordMovetoMagicPoints.bind(this))
}

start () {
console.log('GameMainUI')
this.spritePool.showSpritePosition() // 显示 精灵
this.magicPointPool.showMagicPoint() // 显示 点阵
this.updateGamePanel() // 显示分值
}

// 根据playData更新数据
public updateGamePanel(){
let playData:IPlayData = this.gameDataManager.getPlayData()
this.node.getChildByName('gamePanel').getChildByName('curRound').getComponent(cc.Label).string = playData.round.toString()
}

// 出鞘
public chuqiao(){
this.pushACKMsg();
this.removeSelected();
}
/**
* 去除选中效果
*/
removeSelected(){
this.magicPointPool.node.getComponentsInChildren('MagicPointItem').forEach( (_item) => {
console.log(_item)
_item.getComponent(cc.Graphics).clear()
_item.node.getChildByName('helpPoint').position = cc.v2(0,0)
_item.isSelected = false;
})
}
// 点击发送
pushACKMsg(){
// 发送攻击
let messageservice = MyGame.SingletonFactory.getInstance(MessageService);
let playData = this.gameDataManager.getPlayData()
// 根据怪物当前的前行速度计算出,出鞘后打击到的怪物们的坐标
let lineACKArray:Array<msg.LineACK> = []
let delayACKArray:Array<msg.DelayACK> = []

let pre_index = -1

let first_point = playData.needPushMsgMagicPointArray[0];
let last_point = playData.needPushMsgMagicPointArray[playData.needPushMsgMagicPointArray.length-1]

let power_pre = 1;

if(first_point.Index == last_point.Index){
if(playData.needPushMsgMagicPointArray.length >= 3){
console.log('=======范围攻击时,直接攻击的攻击力变小====');
power_pre = 0.5 // 范围攻击时,直接攻击的攻击力变小
}
}

playData.needPushMsgMagicPointArray.forEach( (item,index) => {
if(index > 0){
let p_a = {'x':Math.sin( (pre_index*30 - 5)*Math.PI/180)*300,'y':Math.cos( (pre_index*30 - 5)*Math.PI/180)*300};
let p_b = {'x':Math.sin( (pre_index*30 + 5)*Math.PI/180)*300,'y':Math.cos( (pre_index*30 + 5)*Math.PI/180)*300};
let p_c = {'x':Math.sin( (item.Index*30 - 5)*Math.PI/180)*300,'y':Math.cos( (item.Index*30 - 5)*Math.PI/180)*300};
let p_d = {'x':Math.sin( (item.Index*30 + 5)*Math.PI/180)*300,'y':Math.cos( (item.Index*30 + 5)*Math.PI/180)*300};
let spriteEventArray:Array<msg.SpriteEvent> = []
let allSpriteArray = this.spritePool.getSpriteArray()
allSpriteArray.forEach((sprite)=>{
let p_p = {'x':sprite.node.x,'y':sprite.node.y}
let is_pz = pInQuadrangle(p_a,p_b,p_c,p_d,p_p);
if(is_pz){
// console.log('在精灵index:'+sprite.msgSprite.SpriteIndex+'在第'+(index)+'直线攻击中有碰撞')
// console.log('sprite',sprite)
let spriteEvent = new msg.SpriteEvent
spriteEvent.SpriteIndex = sprite.msgSpriteItem.SpriteIndex
spriteEvent.SpriteEventType = msg.SpriteEventType.SUB_HP
spriteEvent.Value = 50*power_pre
spriteEventArray.push(spriteEvent)
}
})
let lineACK = new msg.LineACK
lineACK.FromIndex = pre_index
lineACK.ToIndex = item.Index
lineACK.SpriteEventArray = spriteEventArray

lineACKArray.push(lineACK)
pre_index = item.Index

}else{
pre_index = item.Index
}
});

// 范围攻击
if(first_point.Index == last_point.Index){
console.log('=======范围攻击1====');
if(playData.needPushMsgMagicPointArray.length >= 3){
console.log('=======范围攻击2====');
let spriteEvent = new msg.SpriteEvent
spriteEvent.SpriteIndex = 0 // todo
spriteEvent.SpriteEventType = msg.SpriteEventType.SUB_HP
spriteEvent.Value = 25

let delayACK = new msg.DelayACK
delayACK.SpriteEventArray = [spriteEvent]

delayACKArray.push(delayACK)
}
}

messageservice.sendFireREQByMagicPointArray(playData,playData.needPushMsgMagicPointArray,lineACKArray,delayACKArray)
this.gameDataManager.resetATK() // 清空攻击数据
}


/**
* 按点的顺序点亮各个点,并用虚线连接
*/
public flyswordMovetoMagicPoints(event:Event,message:msg.IFireACK){
console.log('====flyswordMovetoMagicPoints ====')
let magicPointArray:Array<msg.IMagicPoint> = message.MagicPointArray
let LineACKArray:Array<msg.ILineACK> = message.LineACKArray
let DelayACKArray:Array<msg.IDelayACK> = message.DelayACKArray

let index_arr = [] // point的index数组

let all_item = this.magicPointPool.node.getComponentsInChildren('MagicPointItem')
let need_light_magicPoint = [];
magicPointArray.forEach( (_item,_index) => {
index_arr.push(_item.Index)
need_light_magicPoint[_index] = all_item[_item.Index]
})
// this.smallflyingsword.active = true
let actionOrActionArray = []

need_light_magicPoint.forEach( (_item,_index) =>{
console.log(_item)
let _time = 0;
if(_index == 0){
_time = 0.5
}else{
let p1 = cc.v2(need_light_magicPoint[_index - 1].node.x,need_light_magicPoint[_index - 1].node.y)
let p2 = cc.v2(_item.node.x, _item.node.y)
let distance = p1.sub(p2).mag()
_time = distance*0.0005 // 根据距离算出延迟播放效果的时间
}
actionOrActionArray.push(cc.moveTo(_time,_item.node.x,_item.node.y))
if(_index > 0){
actionOrActionArray.push(cc.callFunc(this.spriteEventPlayLineACK,this,LineACKArray[_index - 1]))
}
})
if(DelayACKArray.length > 0){
DelayACKArray.forEach( (item) => {
actionOrActionArray.push(cc.callFunc(this.spriteEventPlayDelayACK,this,item))
})
}
actionOrActionArray.push(cc.callFunc(this.flyingswordMoveOver,this))


this.smallflyingsword.runAction(
cc.sequence(actionOrActionArray)
);
}
flyingswordMoveOver(){
console.log('flyingswordMoveOver')
// 发送
let messageservice = MyGame.SingletonFactory.getInstance(MessageService);
let playData = this.gameDataManager.getPlayData()
messageservice.sendSpriteDataREQ(playData.playMissionId,playData.RoomId)

setTimeout(()=>{
this.updateGamePanel()
this.spritePool.showSpritePosition() // 显示 精灵
},1000);
console.log('sendSpriteDataREQ')
console.log(playData.playMissionId)
console.log(playData.RoomId)
}

spriteEventPlayLineACK(node,LineACK:msg.LineACK){
console.log('spriteEventPlayLineACK')
if(LineACK != null){
this.spritePool.playSpriteLineACK(LineACK)
}
}
spriteEventPlayDelayACK(node,DelayACK:msg.DelayACK){
console.log('spriteEventPlayDelayACK')
if(DelayACK != null){
this.spritePool.playSpriteDelayACK(DelayACK)
}
}

}

4.3 面板模块

是却别与一般UI的,是同主页在同一个场景内的UI

4.3.1 灵宠面板

panle/HeroPanle.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { BasePanle }  from "./BasePanle";
// import goodsItem from "../goodsItem";
const {ccclass, property} = cc._decorator;
@ccclass
export default class HeroPanle extends BasePanle {

static UIName = "HeroPanle";
static ResourcePath = "ui/panle/heroPanle";

// Prefab资源层
heroPanlePrefab:cc.Prefab = null

onLoad(){

}


start () {
this.animationManager.playAnimation('HeroPowerMc01','label',this.node.getChildByName('heroMain').getChildByName('heroPowerMc'))
this.animationManager.playAnimation('HeroBig001','label',this.node.getChildByName('heroMain').getChildByName('heroBody'))
}

onClosePanleBtnClick(){
this.uiManager.closeUI('HeroPanle')
}

// update (dt) {}
}

4.3.2 包裹面板

panle/PackagePanle.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

import { BasePanle } from "./BasePanle";
// import goodsItem from "../goodsItem";
const {ccclass, property} = cc._decorator;
@ccclass
export default class PackagePanle extends BasePanle {

static UIName = "PackagePanle";
static ResourcePath = "ui/panle/packagePanle";

onLoad(){
}

start () {

}

onClosePanleBtnClick(){
this.uiManager.closeUI('PackagePanle')
}

}

游戏UI说明

1.主界面

-w379

说明:
主界面主要是记录当前关卡所在位置
用户可以在已解锁的关卡中自由选择关卡进行游戏

2.包裹界面

-w378
说明:
包裹内的物品列表
有四种主要的物品
道具:主要是一些特殊的物品,一般可直接使用
灵宠:未上场的灵宠
灵草:特殊物品,可喂食灵宠增加灵力
魔晶: 特殊物品,可锻造注入仙剑之中

3.灵宠界面

-w376
说明:
顶部为已上场的灵宠
中间为当前选中的灵宠
每个灵宠可装备四种魂骨来增强攻击输出
喂食灵草类可增加灵宠的属性

4.仙剑界面

-w372
说明:
顶部为已上场的仙剑
中间为当前选中的仙剑
六阶以上仙剑拥有剑灵
通过注入魔晶可增加仙剑的属性

5.桃花岛界面

-w379
说明:
岛内是玩家的一片独立领域
在岛内的仙草谷,可种植收获仙草类植物。
在岛内的神秘山脉,可获得灵宠

6.战斗界面

-w374
说明:
玩家的角色和六边形点阵是固定在屏幕中
网格为游戏内的地图坐标,每个网格内可同时存在2个单位
玩家可移动网格,使目标宠物进入仙剑攻击范围内
在点阵上划出攻击轨迹,接着点击仙剑,达到“仙剑出鞘”效果

策划案

1.游戏概述

1.1游戏简介

  • 本游戏是一款休闲闯关游戏,通过在六边形点阵上划出不同的轨迹来控制飞剑,清除来袭的敌方魔兽,也可通过灵宠出战对抗敌方魔兽,在规定回合内完成关卡任务或击败所有敌方魔兽即获胜。

1.2操作说明

  • 游戏中玩家在前方点阵上划出不同的仙剑轨迹(称之为“结节”),结节完成后会注入当前的仙剑中
  • 注入仙剑后,玩家可通过上划“剑鞘”或点击“仙剑”,达到“出鞘”效果。仙剑将按预定的飞行轨迹运行并给敌方造成一定的伤害
  • 仙剑沿飞行轨迹后,玩家可通过下滑“剑鞘”,达到“入鞘”效果,(第一次)入鞘后可生成一些记忆指令
  • 记忆指令可使玩家无需在点阵上手动划出轨迹,直接点击“仙剑”,就可达到“出鞘”效果(运行轨迹为记忆轨迹)
  • 记忆指令造成伤害后,玩家下滑“剑鞘”,无“入鞘”效果,不产生新的记忆指令
  • 如果点阵的布局不符合当前局势,玩家可通过左右滑动“仙剑”进行切换

2.游戏流程

未命名文件 -1-

3.游戏核心

3.1 结节

  • 定义
    • 由玩家在剑阵中划出的剑阵轨迹
  • 普通结节
    • 由玩家划出的非闭合结节
    • 仙剑经过的范围内,给敌方造成伤害
  • 范围结节
    • 由玩家划出的闭合结节
    • 仙剑经过后,给范围内敌方造成伤害
  • 记忆结节
    • “入鞘”后生成的结节,攻击力为手动结节的50~90%
    • 记忆结节可被存储为道具
  • 剑灵结节
    • 六阶以上仙剑的剑灵天生拥有的结节
    • 玩家划出的结节与剑灵结节相同可触发新的攻击属性

3.2 五灵

  • 定义
    • 雷(金色)风(绿色)水(蓝色)火(红色)土
  • 相克
    • 土克水,水克火,火克雷,雷克风,风克土
  • 仙剑
    • 可拥有多个灵力
  • 灵宠
    • 一般拥有单灵力,boss级灵宠拥有多个灵力
  • 仙草
    • 只拥有单灵力
  • 魔晶
    • 只拥有单灵力

3.3 锻造

  • 定义
    • 提升仙剑等级及灵力
  • 消耗
    • 金币和材料
  • 注入灵力
    • 仙剑阶位越高,仙剑上的灵力孔越多,可注入的灵力更多
  • 剑灵学习
    • 六阶以上的仙剑都有一个剑灵
    • 剑灵都有一个天生的剑灵结节
  • 熔炼
    • 熔炼仙剑一定概率可获得,剑灵记忆结节
    • 相应的魔晶和金币

3.4 游戏结算

  • 胜利
    • 关卡模式
      • 完成击杀任务
      • 完成守护任务
    • 对抗模式
      • 出战宠物靠近敌方的底线,并击中对方主公
  • 失败
    • 关卡模式
      • 未完成击杀任务
      • 未完成守护任务
      • 中途退出
    • 对抗模式
      • 主公被袭击
      • 中途退出

4.游戏主体

4.1 仙剑

  • 定义
    • 玩家的主要输出对象
  • 获得来源
    • 主线副本
    • 锻造合成
  • 战斗
    • 最多可同时携带3把
  • 品质
    • 普通,珍贵,稀有
  • 属性
    • 仙剑阶位越高,仙剑上的灵力孔越多
    • 六阶以上的仙剑都有一个剑灵
    • 同名仙剑的灵力孔数量固定
    • 剑灵都有一个天生的剑灵结节
    • 仙剑阶位范围3-12
  • 升级
    • 通过锻造给剑阵注入“魔晶”,提升灵力
    • 剑灵学习学习新的精灵结节

3.2 灵宠

  • 定义
    • 辅助玩家进行战斗的宠物
  • 获得来源
    • 主线副本
  • 战斗
    • 每种灵宠都有召唤分身功能
    • 最多可出战5个宠物
  • 品质
    • 普通,珍贵,稀有
  • 属性
    • 输出分远程,近战
    • 宠物血量=基础血量+等级+*成长血量
    • 宠物攻击=基础攻击+等级*成长攻击
    • 宠物防御=基础防御+等级*成长防御
  • 升级
    • 通过喂食“仙草”

5. 游戏道具

5.1 魔晶类

  • 获得来源
    • 主线副本
    • 其他副本
  • 用途
    • 升级剑阵的灵力

5.2 仙草类

  • 获得来源
    • 主线副本
    • 其他副本
  • 用途
    • 升级宠物的灵力

5.3 记忆结节类

  • 获得来源
    • 手动结节并“入鞘”后生成
  • 用途
    • 快速发动攻击

5.4 普通道具

  • 获得来源
    • 活动
    • 副本
  • 用途
    • 间接获得,魔晶,仙草,结节等
    • 某些副本的必要道具

6.游戏数值

6.1 仙剑数值

6.1.1 五灵相克

  • 土克水,水克火,火克雷,雷克风,风克土
  • 相克属性攻击伤害+10%
  • 相同属性攻击伤害-10%

    6.1.2 属性增加

  • 攻击力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100
  • 生命力升级配置
    • 1~10级每级+100
    • 11~20级每级+200
    • 21~30级每级+300
    • 31~40级每级+400
    • 41~50级每级+500
    • 51~60级每级+600
    • 61~70级每级+700
    • 71~80级每级+800
    • 81~90级每级+900
    • 91~100级每级+1000

6.1.3 伤害结算

  • 伤害公式
    • 伤害值 = (当前攻击力暴击倍数-0.5(对方防御力))* (1+是否相克)

6.1.4 品质

  • 普通(白)
    • 基础攻击力 100~200
    • 基础生命力 200~400
  • 精良(绿)
    • 基础攻击力 150~300
    • 基础生命力 300~600
  • 稀有(蓝)
    • 基础攻击力 200~400
    • 基础生命力 400~800
  • 史诗(紫)
    • 基础攻击力 250~500
    • 基础生命力 500~1000
  • 传说 (金)
    • 基础攻击力 300~600
    • 基础生命力 600~1200

6.2 灵宠数值

6.2.1 五灵相克

  • 土克水,水克火,火克雷,雷克风,风克土
  • 相克属性攻击伤害+10%
  • 相同属性攻击伤害-10%

6.2.2 属性增加

  • 攻击力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100
  • 防御力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100
  • 生命力升级配置
    • 1~10级每级+100
    • 11~20级每级+200
    • 21~30级每级+300
    • 31~40级每级+400
    • 41~50级每级+500
    • 51~60级每级+600
    • 61~70级每级+700
    • 71~80级每级+800
    • 81~90级每级+900
    • 91~100级每级+1000
  • 行动力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100

6.2.3 品质

  • 普通(白)
    • 基础攻击力 100~200
    • 基础防御力 100~200
    • 基础行动力 100~200
    • 基础生命力 200~400
  • 精良(绿)
    • 基础攻击力 150~300
    • 基础防御力 150~300
    • 基础行动力 150~300
    • 基础生命力 300~600
  • 稀有(蓝)
    • 基础攻击力 200~400
    • 基础防御力 200~400
    • 基础行动力 200~400
    • 基础生命力 400~800
  • 史诗(紫)
    • 基础攻击力 250~500
    • 基础防御力 250~500
    • 基础行动力 250~500
    • 基础生命力 500~1000
  • 传说 (金)
    • 基础攻击力 300~600
    • 基础防御力 300~600
    • 基础行动力 300~600
    • 基础生命力 600~1200

6.3 装备数值

6.3.1 属性增加

  • 攻击力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100
  • 防御力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100
  • 生命力升级配置
    • 1~10级每级+100
    • 11~20级每级+200
    • 21~30级每级+300
    • 31~40级每级+400
    • 41~50级每级+500
    • 51~60级每级+600
    • 61~70级每级+700
    • 71~80级每级+800
    • 81~90级每级+900
    • 91~100级每级+1000
  • 行动力升级配置
    • 1~10级每级+10
    • 11~20级每级+20
    • 21~30级每级+30
    • 31~40级每级+40
    • 41~50级每级+50
    • 51~60级每级+60
    • 61~70级每级+70
    • 71~80级每级+80
    • 81~90级每级+90
    • 91~100级每级+100