[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.通讯接口设计 与后端通讯主要是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 ( ) { let ProtocolData = MessageService.ProtocolData this .msgidNameArray = [] for (let k in ProtocolData) { let value = ProtocolData[k]; 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); } 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 , 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() }) 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) }) } 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 ; string NickName = 2 ; string PicNo = 3 ; } message Test { string Test = 1 ; } enum MissionOverType { LOSS=0 ; WIN=1 ; } enum SpriteEventType { SUB_HP=0 ; ADD_HP=1 ; ADD_BUFF=2 ; SUB_BUFF=3 ; } 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 ; } message Buff { string BuffNo=1 ; string BuffName=2 ; uint32 BuffType=3 ; uint32 IsGood=4 ; } message SpriteDataREQ { uint32 MissionId=1 ; uint32 RoomId=2 ; } message SpriteDataACK { uint32 MissionId=1 ; uint32 RoomId=2 ; uint32 Round=3 ; repeated SpriteItem SpriteItemArray=4 ; repeated Player PlayerArray=5 ; } 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 ; 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 ; uint32 RoomId=4 ; uint32 MissionId=5 ; repeated LineACK LineACKArray=6 ; repeated DelayACK DelayACKArray=7 ; } 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 ; } 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 ; uint32 Num=3 ; } enum ChatToType { WORLD=0 ; UNION=1 ; USER =2 ; } 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 ; }
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 音效管理 封装了声音资源的加载及音量的整体控制的功能。
主要方法:
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)
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 SpritePool from "../pool/SpritePool" import MagicPointPool from "../pool/MagicPointPool" import { pInQuadrangle } from "../../base/utils/OtherUtil" ;@ccclass export default class GameMainUI extends BaseComponent { @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() } 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 ) { 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 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 = [] 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] }) 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" ;const {ccclass, property} = cc._decorator;@ccclass export default class HeroPanle extends BasePanle { static UIName = "HeroPanle" ; static ResourcePath = "ui/panle/heroPanle" ; 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' ) } }
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" ;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.主界面
说明: 主界面主要是记录当前关卡所在位置 用户可以在已解锁的关卡中自由选择关卡进行游戏
2.包裹界面 说明: 包裹内的物品列表 有四种主要的物品 道具:主要是一些特殊的物品,一般可直接使用 灵宠:未上场的灵宠 灵草:特殊物品,可喂食灵宠增加灵力 魔晶: 特殊物品,可锻造注入仙剑之中
3.灵宠界面 说明: 顶部为已上场的灵宠 中间为当前选中的灵宠 每个灵宠可装备四种魂骨来增强攻击输出 喂食灵草类可增加灵宠的属性
4.仙剑界面 说明: 顶部为已上场的仙剑 中间为当前选中的仙剑 六阶以上仙剑拥有剑灵 通过注入魔晶可增加仙剑的属性
5.桃花岛界面 说明: 岛内是玩家的一片独立领域 在岛内的仙草谷,可种植收获仙草类植物。 在岛内的神秘山脉,可获得灵宠
6.战斗界面 说明: 玩家的角色和六边形点阵是固定在屏幕中 网格为游戏内的地图坐标,每个网格内可同时存在2个单位 玩家可移动网格,使目标宠物进入仙剑攻击范围内 在点阵上划出攻击轨迹,接着点击仙剑,达到“仙剑出鞘”效果
策划案 1.游戏概述 1.1游戏简介
本游戏是一款休闲闯关游戏,通过在六边形点阵上划出不同的轨迹来控制飞剑,清除来袭的敌方魔兽,也可通过灵宠出战对抗敌方魔兽,在规定回合内完成关卡任务或击败所有敌方魔兽即获胜。
1.2操作说明
游戏中玩家在前方点阵上划出不同的仙剑轨迹(称之为“结节”),结节完成后会注入当前的仙剑中
注入仙剑后,玩家可通过上划“剑鞘”或点击“仙剑”,达到“出鞘”效果。仙剑将按预定的飞行轨迹运行并给敌方造成一定的伤害
仙剑沿飞行轨迹后,玩家可通过下滑“剑鞘”,达到“入鞘”效果,(第一次)入鞘后可生成一些记忆指令
记忆指令可使玩家无需在点阵上手动划出轨迹,直接点击“仙剑”,就可达到“出鞘”效果(运行轨迹为记忆轨迹)
记忆指令造成伤害后,玩家下滑“剑鞘”,无“入鞘”效果,不产生新的记忆指令
如果点阵的布局不符合当前局势,玩家可通过左右滑动“仙剑”进行切换
2.游戏流程
3.游戏核心 3.1 结节
定义
普通结节
由玩家划出的非闭合结节
仙剑经过的范围内,给敌方造成伤害
范围结节
由玩家划出的闭合结节
仙剑经过后,给范围内敌方造成伤害
记忆结节
“入鞘”后生成的结节,攻击力为手动结节的50~90%
记忆结节可被存储为道具
剑灵结节
六阶以上仙剑的剑灵天生拥有的结节
玩家划出的结节与剑灵结节相同可触发新的攻击属性
3.2 五灵
3.3 锻造
定义
消耗
注入灵力
仙剑阶位越高,仙剑上的灵力孔越多,可注入的灵力更多
剑灵学习
六阶以上的仙剑都有一个剑灵
剑灵都有一个天生的剑灵结节
熔炼
熔炼仙剑一定概率可获得,剑灵记忆结节
相应的魔晶和金币
3.4 游戏结算
4.游戏主体 4.1 仙剑
定义
获得来源
战斗
品质
属性
仙剑阶位越高,仙剑上的灵力孔越多
六阶以上的仙剑都有一个剑灵
同名仙剑的灵力孔数量固定
剑灵都有一个天生的剑灵结节
仙剑阶位范围3-12
升级
通过锻造给剑阵注入“魔晶”,提升灵力
剑灵学习学习新的精灵结节
3.2 灵宠
定义
获得来源
战斗
品质
属性
输出分远程,近战
宠物血量=基础血量+等级+*成长血量
宠物攻击=基础攻击+等级*成长攻击
宠物防御=基础防御+等级*成长防御
升级
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