框架设计原则
- 游戏场景中只放置启动脚本,不放内容,否则合作和维护麻烦,运行的时候只有一个场景容器
- 手动关联资源会导致大型项目无法维护
- 节点和预制体不手动挂载任何代码,可以把美术视图和程序同时开发,降低了维护难度,所有东西都可以从代码搜索得到。
- 程序员维护业务逻辑,数据来自策划,视图来自美术。视图由美术做好之后放在固定的地方,程序员必要的时候调用即可。
目录:
AssetsPackage:用来存放2D和3D资源
Scenes:用来存放场景,只有一个Main场景
Script:分为框架代码(可以重用),业务逻辑代码
Script/FrameWork:框架代码
Script/FrameWork/Utils:通用函数
Script/FrameWork/Manager:管理器
Script/3rd:第三方代码
Script/Game:业务逻辑
启动代码
场景中手动挂载的唯一代码,只放置框架的初始化代码
import { _decorator, Component, Node, TextAsset, Prefab } from 'cc';
import { EventMgr } from './Framework/Managers/EventMgr';
import { NetMgr } from './Framework/Managers/Net/NetMgr';
import { ProtoMgr } from './Framework/Managers/Net/ProtoMgr';
import { ResMgr } from './Framework/Managers/ResMgr';
import { SoundMgr } from './Framework/Managers/SoundMgr';
import { TimerMgr } from './Framework/Managers/TimerMgr';
import { UIMgr } from './Framework/Managers/UIMgr';
import { GameApp } from './Game/GameApp';
const { ccclass, property } = _decorator;
@ccclass('GameLanch')
export class GameLanch extends Component {
public static Instance: GameLanch = null as unknown as GameLanch;
@property
public isNetMode: boolean = false;
@property
private wsUrl: string = "ws://127.0.0.1:6081/ws";
@property(TextAsset)
private pbTexAsset: TextAsset | null = null;
@property(Prefab)
public UILoading: Prefab = null as unknown as Prefab;
onLoad(): void {
if(GameLanch.Instance === null) {
GameLanch.Instance = this;
}
else {
this.destroy();
return;
}
console.log("Game Lanching......");
// 初始化框架逻辑: 资源管理,声音管理,网络管理
this.node.addComponent(ResMgr);
this.node.addComponent(SoundMgr);
this.node.addComponent(TimerMgr);
this.node.addComponent(EventMgr);
this.node.addComponent(UIMgr);
// 是否使用网络模块
if(this.isNetMode) {
this.node.addComponent(ProtoMgr).Init(this.pbTexAsset);
this.node.addComponent(NetMgr).Init(this.wsUrl);
}
// end
this.node.addComponent(GameApp);
// end
// 检查更新我们的资源
// end
// 进入游戏里面去,逻辑模块入口
GameApp.Instance.EnterGame();
// end
}
}
逻辑模块入口
游戏正式的逻辑从EnterGame开始,加载UI,进入场景
import { _decorator, Component, Node, AudioClip, AudioSource, Prefab } from 'cc';
import { EventMgr } from '../Framework/Managers/EventMgr';
import { ProtoMgr } from '../Framework/Managers/Net/ProtoMgr';
import { ResMgr } from '../Framework/Managers/ResMgr';
import { SoundMgr } from '../Framework/Managers/SoundMgr';
import { TimerMgr } from '../Framework/Managers/TimerMgr';
import { UIMgr } from '../Framework/Managers/UIMgr';
import { GameLanch } from '../GameLanch';
import { AuthProxy } from './ServerProxy/AuthProxy';
import { NetEventDispatcher } from './ServerProxy/NetEventDispatcher';
var resPkg = {
"Sounds": [
{ assetType: AudioClip, urls:
["CK_attack1",
"Qinbing_die"
]},
],
"GUI": [
{
assetType: Prefab,
urls: [
"UIPrefabs/LoginUI",
],
},
],
// "Sounds": AudioClip,
}
export class GameApp extends Component {
public static Instance: GameApp = null as unknown as GameApp
onLoad(): void {
if(GameApp.Instance === null) {
GameApp.Instance = this;
}
else {
this.destroy();
return;
}
// 实例化游戏网络事件模块
if(GameLanch.Instance.isNetMode) {
this.node.addComponent(NetEventDispatcher).Init();
AuthProxy.Insance.Init();
}
// end
}
// 游戏逻辑入口
public EnterGame(): void {
UIMgr.Instance.ShowUIPrefab(GameLanch.Instance.UILoading);
ResMgr.Instance.preloadResPkg(resPkg, (now: any, total: any)=>{
EventMgr.Instance.Emit("loadProgress", Math.floor(now * 100 / total));
}, ()=>{
this.EnterLoadingScene();
});
}
public EnterLoadingScene(): void {
console.log("EnterLoadingScene");
/*
// 释放测试
ResMgr.Instance.releaseResPkg(resPkg);
this.scheduleOnce(()=>{
console.log(ResMgr.Instance.getAsset("Sounds", "CK_attack1"));
}, 3)*/
// 释放游戏地图
// end
// 释放游戏角色
// end
// 释放我们的游戏UI
// end
// 播放声音
/*var as = this.node.addComponent(AudioSource);
as.clip = ResMgr.Instance.getAsset("Sounds", "CK_attack1");
as.loop = true;
as.play();*/
// var clip = ResMgr.Instance.getAsset("Sounds", "CK_attack1");
// SoundMgr.Instance.playSound(clip);
// SoundMgr.Instance.setMusicMute(false);
// SoundMgr.Instance.playBgMusic(clip, true);
// end
// 释放我们的UI视图了
UIMgr.Instance.ClearAll();
UIMgr.Instance.ShowUIView("LoginUI");
// end
// 监听事件
// end
}
}
资源管理模块
- 常用资源,进入游戏之前加载好
- 较大的资源,不常用,游戏中加载,加进度条
- 异步加载
- 加载场景的过程中,加载资源
- 提前配置好场景用的资源包,填好资源加载表(手写或代码生成)
- 准备好单个资源加载,释放的接口
- 提供获取资源的接口
资源加载表:var resPkg = { "Sound":[ {typeAsset:Prefab, urls: [路径1,路径2,路径3]}, {typeAsset:Prefab, urls: [路径1,路径2,路径3]}, ... ], "Common": Texture2D; ... };
/*
【注意】:
ab包对象没有释放,如果有必要可以考虑释放;
释放ab包对象,不会释放资源;
预加载资源使用接口: preleadResPkg/releaseResPkg, 常驻内存资源,可以不写入释放资源包;
游戏中较大的资源,可以采用preleadAsset/releaseAsset 单个使用释放
游戏中获取资源: getAsset();
*/
import { _decorator, Component, Node, assetManager, AssetManager, Asset } from 'cc';
export class ResMgr extends Component {
public static Instance: ResMgr = null as unknown as ResMgr;
private totalAb: number = 0;
private nowAb: number = 0;
private now: number = 0;
private total: number = 0;
onLoad(): void {
if(ResMgr.Instance === null) {
ResMgr.Instance = this;
}
else {
this.destroy();
return;
}
}
private loadAndRef(abBundle: AssetManager.Bundle, url: string,
typeAsset: any,
progress: Function, endFunc: Function): void {
abBundle.load(url, typeAsset, (err: any, asset: Asset)=>{
if (err) {
console.log("load assets: ", err);
return;
}
console.log("load asset success:", url);
asset.addRef(); // 增加一个引用技术;
this.now ++;
if(progress) {
progress(this.now, this.total);
}
if(this.now >= this.total) {
if(endFunc) {
endFunc();
}
}
});
}
private loadAssetsInUrls(abBundle: AssetManager.Bundle, typeAsset: any, urls: Array<string>, progress: Function, endFunc: Function): void {
for(let i = 0; i < urls.length; i ++) {
this.loadAndRef(abBundle, urls[i], typeAsset, progress, endFunc);
}
}
private releaseAssetsInUrls(abBundle: AssetManager.Bundle, typeAsset: any, urls: Array<string>): void {
for(let i = 0; i < urls.length; i ++) {
// console.log(urls[i]);
let asset: Asset = abBundle.get(urls[i]) as Asset;
if(!asset) {
continue;
}
// console.log(asset.refCount);
asset.decRef(true);
}
}
private preloadAssetsInAssetsBundles(resPkg: any, progress: Function, endFunc: Function): void {
for(var key in resPkg) {
var abBundle: AssetManager.Bundle = assetManager.getBundle(key) as AssetManager.Bundle;
if(!abBundle) {
continue;
}
if(resPkg[key] instanceof Array) {
for(let i = 0; i < resPkg[key].length; i ++) {
// let info: any = abBundle.getDirWithPath("/");
// console.log(info);
this.loadAssetsInUrls(abBundle, resPkg[key][i].typeAsset, resPkg[key][i].urls, progress, endFunc);
}
}
else {
let typeAsset = resPkg[key];
let infos = abBundle.getDirWithPath("/");
let urls: any = [];
for(let i = 0; i < infos.length; i ++) {
urls.push(infos[i].path);
}
this.loadAssetsInUrls(abBundle, typeAsset, urls, progress, endFunc);
}
}
}
/*
var resPkg = {
"Ab包名字": [
{ typeAsset: 资源类型, urls: []},
{ typeAsset: 资源类型, urls: []},
...
],
"Ab包名字": 资源类型, 表示整包ab包按照一个类型加载;
... ..
};
progress(now, total)
*/
public preloadResPkg(resPkg: any, progress: Function, endFunc: Function): void {
this.totalAb = 0;
this.nowAb = 0;
this.total = 0;
this.now = 0;
for(var key in resPkg) {
this.totalAb ++;
if(resPkg[key] instanceof Array) {
for(let i = 0; i < resPkg[key].length; i ++) {
this.total += resPkg[key][i].urls.length;
}
}
}
// 加载ab包
for(var key in resPkg) {
assetManager.loadBundle(key, (err, bundle: AssetManager.Bundle)=>{
if(err) {
console.log("load bundle erro: ", err);
return;
}
this.nowAb ++;
if(!(resPkg[key] instanceof Array)) {
let infos = bundle.getDirWithPath("/");
this.total += (infos.length);
}
if(this.nowAb >= this.totalAb) { // ab包加载完毕
this.preloadAssetsInAssetsBundles(resPkg, progress, endFunc);
}
});
}
// end
}
public releaseResPkg(resPkg: any): void {
for(var key in resPkg) {
let abBundle: AssetManager.Bundle = assetManager.getBundle(key) as AssetManager.Bundle;
if(!abBundle) {
continue;
}
if(resPkg[key] instanceof Array) {
for(let i = 0; i < resPkg[key].length; i ++) {
this.releaseAssetsInUrls(abBundle, resPkg[key][i].typeAsset, resPkg[key][i].urls);
}
}
else {
let typeAsset = resPkg[key];
let infos = abBundle.getDirWithPath("/");
let urls: any = [];
for(let i = 0; i < infos.length; i ++) {
urls.push(infos[i].path);
}
this.releaseAssetsInUrls(abBundle, typeAsset, urls);
}
}
}
//预加载单个资源
public preloadAsset(abName: string, url: string, typeClass: any, endFunc: Function): void {
assetManager.loadBundle(abName, (err, abBundle: AssetManager.Bundle)=>{
if(err) {
console.log(err);
return;
}
abBundle.load(url, typeClass, (err, asset: Asset)=>{
if(err) {
console.log(err);
return;
}
if(endFunc) {
endFunc();
}
});
});
}
public releaseAsset(abName: string, url: string): void {
var abBundle: AssetManager.Bundle = assetManager.getBundle(abName) as AssetManager.Bundle;
if(!abBundle) {
return;
}
abBundle.release(url);
}
// 同步接口, 前面已经加载好了的资源;使用资源
public getAsset(abName: string, url: string): any {
var abBundle: AssetManager.Bundle = assetManager.getBundle(abName) as AssetManager.Bundle;
if(!abBundle) {
return null;
}
return abBundle.get(url);
}
}
声音管理模块
- 添加8个AudioSource来播放声音,一个AudioSource播放背景音乐
- 每次播放声音,使用一个没有播放声音的AudioSource来播放
- 所有AudioSource被使用时,顶掉最前面一个AudioSource的声音
import { _decorator, Component, Node, AudioSource, AudioClip } from 'cc'; export class SoundMgr extends Component { public static Instance: SoundMgr = null as unknown as SoundMgr; private static MAX_SOUNDS: number = 8; // 最大音效的数目 private nowIndex: number = 0; private sounds: Array<AudioSource> = []; private bgMusic: AudioSource = null as unknown as AudioSource; private isMusicMute: boolean = false; private isSoundMute: boolean = false; onLoad(): void { if(SoundMgr.Instance === null) { SoundMgr.Instance = this; } else { this.destroy(); return; } for(let i = 0; i < SoundMgr.MAX_SOUNDS; i ++) { var as = this.node.addComponent(AudioSource); this.sounds.push(as); } this.bgMusic = this.node.addComponent(AudioSource) as AudioSource; // 从本地存储里面把设置读出来, 0, 1 var value = localStorage.getItem("GAME_MUSIC_MUTE"); if(value) { let v = parseInt(value); this.isMusicMute = (v === 1)? true : false; } value = localStorage.getItem("GAME_SOUND_MUTE"); if(value) { let v = parseInt(value); this.isSoundMute = (v === 1)? true : false; } } public playBgMusic(clip: AudioClip, isLoop: boolean): void { this.bgMusic.clip = clip; this.bgMusic.loop = isLoop; this.bgMusic.volume = (this.isMusicMute)? 0 : 1.0; this.bgMusic.play(); } public stopBgMusic(): void { this.bgMusic.stop(); } public playSound(clip: AudioClip): void { if(this.isSoundMute === true) { return; } var as = this.sounds[this.nowIndex]; this.nowIndex ++; if(this.nowIndex >= SoundMgr.MAX_SOUNDS) { this.nowIndex = 0; } as.clip = clip; as.loop = false; as.play(); } public playSoundOneShot(clip: AudioClip): void { var as = this.sounds[this.nowIndex]; this.nowIndex ++; if(this.nowIndex >= SoundMgr.MAX_SOUNDS) { this.nowIndex = 0; } as.clip = clip; as.loop = false; as.playOneShot(clip); } public setMusicMute(isMute: boolean): void { this.isMusicMute = isMute; this.bgMusic.volume = (this.isMusicMute)? 0 : 1.0; // localStorage let value = (isMute)? 1 : 0; localStorage.setItem("GAME_MUSIC_MUTE", value.toString()); // end } public setSoundsMute(isMute: boolean): void { this.isSoundMute = isMute; // localStorage let value = (isMute)? 1 : 0; localStorage.setItem("GAME_SOUND_MUTE", value.toString()); } }
定时器管理
import { _decorator, Component, Node } from 'cc';
class TimerNode {
public callback: Function = null as unknown as Function;
public duration: number = 0; // 定时器触发的时间间隔;
public delay: number = 0; // 第一次触发要隔多少时间;
public repeat: number = 0; // 你要触发的次数;
public passedTime: number = 0; // 这个Timer过去的时间;
public param: any = null; // // 用户要传的参数
public isRemoved: boolean = false; // 是否已经删除了
public timerId: number = 0; // 标识这个timer的唯一Id号;
}
export class TimerMgr extends Component {
public static Instance: TimerMgr = null as unknown as TimerMgr;
private autoIncId: number = 1; // 自增长的id, 表示唯一的timerId;
private timers: any = {}; // 这个timerId--->Timer对象隐映射
private removeTimers: Array<TimerNode> = [];
private newAddTimers: Array<TimerNode> = [];
onLoad(): void {
if(TimerMgr.Instance === null) {
TimerMgr.Instance = this;
}
else {
this.destroy();
return;
}
}
update(dt: number): void {
// 把新加进来的放入到我们的表里面来
for (let i = 0; i < this.newAddTimers.length; i++) {
this.timers[this.newAddTimers[i].timerId] = this.newAddTimers[i];
}
this.newAddTimers.length = 0;
// end
for (let key in this.timers) {
var timer = this.timers[key];
if (timer.isRemoved) {
this.removeTimers.push(timer);
continue;
}
timer.passedTime += dt; // 更新一下timer时间
if (timer.passedTime >= (timer.delay + timer.duration)) {
// 做一次触发
timer.callback(timer.param);
timer.repeat--;
timer.passedTime -= (timer.delay + timer.duration);
timer.delay = 0; // 很重要;
if (timer.repeat == 0)
{ // 触发次数结束,我们是不是要删除这个Timer;
timer.isRemoved = true;
this.removeTimers.push(timer);
}
// end
}
}
// 结束以后,清理掉要删除的Timer;
for (let i = 0; i < this.removeTimers.length; i++) {
// this.timers.delete(this.removeTimers[i]);
delete this.timers[this.removeTimers[i].timerId];
}
this.removeTimers.length = 0;
// end
}
// [repeat < 0 or repeat == 0 表示的是无限触发]
public ScheduleWithParams(func: Function, param: any, repeat: number, duration: number, delay: number = 0): number
{
let timer: TimerNode = new TimerNode();
timer.callback = func;
timer.param = param;
timer.repeat = repeat;
timer.duration = duration;
timer.delay = delay;
timer.passedTime = timer.duration;
timer.isRemoved = false;
timer.timerId = this.autoIncId;
this.autoIncId ++;
// this.timers.Add(timer.timerId, timer);
this.newAddTimers.push(timer);
return timer.timerId;
}
public Once(func: Function, delay: number): number
{
return this.Schedule(func, 1, 0, delay);
}
public ScheduleOnce(func: Function, param: any, delay: number): number {
return this.ScheduleWithParams(func, param, 1, 0, delay);
}
// [repeat < 0 or repeat == 0 表示的是无限触发]
public Schedule(func: Function, repeat: number, duration: number, delay: number = 0): number
{
return this.ScheduleWithParams(func, null, repeat, duration, delay);
}
public Unschedule(timerId: number) {
if (!this.timers[timerId]) {
return;
}
let timer: TimerNode = this.timers[timerId];
timer.isRemoved = true;
}
}
UI管理
原则:
- 每个UI视图,只有一个控制代码
- 控制代码里面的有一个数据成员表,能方便访问每个需要控制的UI节点
- 控制代码要提供常用的用户输入型组件的监听(button,滑动条)
- 控制代码规范命名 UI视图名字_Ctrl
UI类的基类:
UI管理器:import { _decorator, Component, Node, Button } from 'cc'; export class UICtrl extends Component { protected view: any = {}; // 路径--->节点; this.view["路径"] --->获得节点; // 把UI中所有的节点都加载到一个表中,方便通过路径访问 private loadAllNodeInView(root: any, path: string) { for(let i = 0; i < root.children.length; i ++) { this.view[path + root.children[i].name] = root.children[i]; this.loadAllNodeInView(root.children[i], path + root.children[i].name + "/"); } } onLoad(): void { this.loadAllNodeInView(this.node, ""); } // 为按钮添加事件 public AddButtonListener(viewName: string, caller: any, func: any) { var view_node = this.view[viewName]; if (!view_node) { return; } var button = view_node.getComponent(Button); if (!button) { return; } view_node.on("click", func, caller); } // 其他UI事件, .... }
import { _decorator, Component, Node, find, instantiate, Prefab } from 'cc'; import { ResMgr } from './ResMgr'; export class UIMgr extends Component { public static Instance: UIMgr = null as unknown as UIMgr; private canvas: Node = null as unknown as Node; private uiMap: any = {}; onLoad(): void { if(UIMgr.Instance === null) { UIMgr.Instance = this; } else { this.destroy(); return; } // 挂我们的UI视图的一个根节点; this.canvas = find("Canvas") as Node; // 特殊的挂载点, .... // end } // 传入prefab显示UI public ShowUIPrefab(uiPrefab: Prefab, parent?: Node): void { var uiView: Node = instantiate(uiPrefab) as Node; parent = (!parent)? this.canvas : parent; parent.addChild(uiView); //往根节点上挂下UI视图脚本; console.log(uiPrefab, uiPrefab.data.name); uiView.addComponent(uiPrefab.data.name + "_Ctrl"); this.uiMap[uiPrefab.data.name] = uiView; } // 传入string显示UI public ShowUIView(viewName: string, parent?: Node): void { // 实例化UI视图出来; var uiPrefab = ResMgr.Instance.getAsset("GUI", "UIPrefabs/" + viewName); if(!uiPrefab) { console.log("cannot find ui Prefab: ", viewName); return; } var uiView: Node = instantiate(uiPrefab) as Node; parent = (!parent)? this.canvas : parent; parent.addChild(uiView); this.uiMap[viewName] = uiView; // console.log(uiView); //往根节点上挂下UI视图脚本; uiView.addComponent(uiPrefab.data.name + "_Ctrl"); } public RemoveUI(ui_name: string) { if (this.uiMap[ui_name]) { this.uiMap[ui_name].destroy(); this.uiMap[ui_name] = null; } } public ClearAll() { for (var key in this.uiMap) { if (this.uiMap[key]) { this.uiMap[key].destroy(); this.uiMap[key] = null; } } } }
使用案例
import { _decorator, Component, Node, Label } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
const { ccclass, property } = _decorator;
import { UICtrl } from "./../../Framework/Managers/UICtrl";
@ccclass('UILoading_Ctrl')
export class UILoading_Ctrl extends UICtrl {
private progressValue: Label = null as unknown as Label;
onLoad(): void {
super.onLoad();
this.progressValue = this.view["Progress"].getComponent(Label);
this.progressValue.string = "0%";
EventMgr.Instance.AddEventListener("loadProgress", this, this.onProgressUpdate);
}
private onProgressUpdate(name: string, per: number): void {
this.progressValue.string = per + "%";
}
onDestroy(): void {
EventMgr.Instance.RemoveListenner("loadProgress", this, this.onProgressUpdate)
}
}
事件的订阅和发布
import { _decorator, Component, Node } from 'cc';
export class EventMgr extends Component {
public static Instance: EventMgr = null as unknown as EventMgr;
// xxxx事件名字 ----》 【监听者1(caller, func),监听者2...】
private events_map: any = {};
onLoad(): void {
if(EventMgr.Instance === null) {
EventMgr.Instance = this;
}
else {
this.destroy();
return;
}
}
// func(event_name: string, udata: any)
public AddEventListener(eventName: string, caller: any, func: Function) {
if (!this.events_map[eventName]) {
this.events_map[eventName] = [];
}
var event_queue = this.events_map[eventName];
event_queue.push({
caller: caller,
func: func
});
}
public RemoveListenner(eventName: string, caller: any, func: Function) {
if (!this.events_map[eventName]) {
return;
}
var event_queue = this.events_map[eventName];
for(var i = 0; i < event_queue.length; i ++) {
var obj = event_queue[i];
if (obj.caller == caller && obj.func == func) {
event_queue.splice(i, 1);
break;
}
}
if (event_queue.length <= 0) {
this.events_map[eventName] = null;
}
}
public Emit(eventName: string, udata: any) {
if (!this.events_map[eventName]) {
return;
}
var event_queue = this.events_map[eventName];
for(var i = 0; i < event_queue.length; i ++) {
var obj = event_queue[i];
obj.func.call(obj.caller, eventName, udata);
}
}
}
WebSocket网络模块
客户端网络模块需要做好三件事:
- 连接管理
- 接受数据
- 发送数据
import { _decorator, Component, Node } from 'cc'; import { EventMgr } from '../EventMgr'; enum State { Disconnected = 0, // 断开连接 Connecting = 1, // 正在连接 Connected = 2, // 已经连接; }; export class NetMgr extends Component { public static Instance: NetMgr = null as unknown as NetMgr; private url: string = "ws://127.0.0.1:6081/ws"; private state: number = State.Disconnected; private sock: WebSocket|null = null; onLoad(): void { if(NetMgr.Instance === null) { NetMgr.Instance = this; } else { this.destroy(); return; } this.state = State.Disconnected; } public Init(url: string): void { this.url = url; this.state = State.Disconnected; } // 调用这里发送打包好的数据 public send_data(data_arraybuf: ArrayBuffer) { if (this.state === State.Connected && this.sock) { this.sock.send(data_arraybuf); } } private connect_to_server(): void { if (this.state !== State.Disconnected) { return; } // 抛出一个正在重新连接的事件; EventMgr.Instance.Emit("net_connecting", null); this.state = State.Connecting; this.sock = new WebSocket(this.url); // H5标准,底层做好了; this.sock.binaryType = "arraybuffer"; // blob, 二进制; this.sock.onopen = this._on_opened.bind(this); this.sock.onmessage = this._on_recv_data.bind(this); this.sock.onclose = this._on_socket_close.bind(this); this.sock.onerror = this._on_socket_err.bind(this); } // 这里接收的数据会被事件发布出去,protoMgr会订阅这个事件 private _on_recv_data(event: any) { EventMgr.Instance.Emit("net_message", event.data); } private _on_socket_close(event: any) { this.close_socket(); } private _on_socket_err(event: any) { this.close_socket(); } public close_socket() { if (this.state === State.Connected) { if (this.sock !== null) { this.sock.close(); this.sock = null; } } EventMgr.Instance.Emit("net_disconnect", null); this.state = State.Disconnected; } // 连接成功了 private _on_opened(event: any) { this.state = State.Connected; console.log("connect to server: " + this.url + " sucess!"); EventMgr.Instance.Emit("net_connect", null); } update (dt: number) { if (this.state !== State.Disconnected) { return; } this.connect_to_server(); } }
ProMgr协议管理
配置动态解析描述文件:
- 项目中内置一个protobuf解释型runtime库,protobuf.js,这个文件有8723行,这里不展示,详见https://github.com/dcodeio/protobuf.js
- 资源文件中添加协议描述文件
- 实现ProMgr
import { _decorator, Component, Node, TextAsset } from 'cc';
declare const protobuf: any;
export class ProtoMgr extends Component {
public static Instance: ProtoMgr = null as unknown as ProtoMgr;
// 协议描述文件的文本对象
private pbTexAsset: TextAsset|null = null;
// 根据协议描述文本对象,我们生成一个动态解析的对象;
private pb: any = null;
public Init(pbTex: TextAsset|null): void {
this.pbTexAsset = pbTex;
this.pb = protobuf.parse(this.pbTexAsset);
}
onLoad(): void {
if(ProtoMgr.Instance === null) {
ProtoMgr.Instance = this;
}
else {
this.destroy();
return;
}
}
// 调用该函数对对应名字的协议进行序列化,返回二进制,可以直接通过websocket发送
public SerializeMsg(msgName: string, msgBody: any): Uint8Array {
let rs = this.pb.root.lookupType(msgName);
let msg = rs.create(msgBody);
let buf = rs.encode(msg).finish();
return buf;
}
// 二进制解码,生成对应的表
public DeserializeMsg(msgName: string, msgBuf: Uint8Array): Object {
let rs = this.pb.root.lookupType(msgName);
let msg = rs.decode(msgBuf)
return msg;
}
}
和服务器对接
双方规定一个发送数据的格式:
服务号2字节 | 命令号 2字节 | 用户标识 4字节(客户端不用写数据) | 数据本体
import { _decorator, Component, Node, Game } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
import { NetMgr } from '../../Framework/Managers/Net/NetMgr';
import { ProtoMgr } from '../../Framework/Managers/Net/ProtoMgr';
import { Cmd } from './Cmd';
import { Stype } from './Stype';
export class NetEventDispatcher extends Component {
public static Instance: NetEventDispatcher = null as unknown as NetEventDispatcher;
public onLoad(): void {
if(NetEventDispatcher.Instance === null) {
NetEventDispatcher.Instance = this;
}
else {
this.destroy();
return;
}
}
// 这里订阅了Websocket模块的接收数据
public Init(): void {
EventMgr.Instance.AddEventListener("net_message", this, this.onRecvMsg);
}
// 事件名字---》事件订阅传过来的 net_message
// udata: 网络收到数据;
private onRecvMsg(uname: string, udata: ArrayBuffer): void {
// 获取服务号,命令号;
var dataView = new DataView(udata);
var stype = dataView.getInt16(0, true);
var ctype = dataView.getInt16(2, true);
// 获取我们的序列化后的二进制数据;
var uint8Buf: Uint8Array = new Uint8Array(udata);
var msgBuf = uint8Buf.subarray(4 + 4);
// 反序列化二进制数据body 为一个对象
var msgBody = ProtoMgr.Instance.DeserializeMsg(Cmd[ctype], msgBuf);
// 根据服务号触发对应的事件,对应的服务模块根据命令号处理即可
EventMgr.Instance.Emit(Stype[stype], {ctype: ctype, body: msgBody});
}
// 发送数据
public sendMsg(stype: number, ctype: number, msg: any) {
// step1: 序列化一个msg--->buf;
// enum Cmd ---> {1: "eGuestLoginReq", "eGuestLoginReq": 1}
let msgBuf = ProtoMgr.Instance.SerializeMsg(Cmd[ctype], msg);
// end
// step2: 按照协议,封装号我们的二进制数据包;
var total_len = msgBuf.length + 2 + 2 + 4;
var buf = new ArrayBuffer(total_len); // 内存;
// DataView, 工具,buffer里面来写东西;
var dataview = new DataView(buf);
// [stype, ctype, 4, body Buf]
dataview.setInt16(0, stype, true); // offset, stype
dataview.setInt16(2, ctype, true); // offset = 2, ctype;
dataview.setInt32(4, 0, true);
// end
var uint8Buf = new Uint8Array(buf);
uint8Buf.set(msgBuf, 8);
// step3: WebSocket发送出去
NetMgr.Instance.send_data(buf);
// end
}
}
规定服务号和命令号内容
export enum Stype {
INVALIDI_STYPE = 0,
Auth = 1,
System = 2,
Logic = 3,
}
export enum Cmd {
INVALID_CMD = 0,
GuestLoginReq = 1,
GuestLoginRes = 2,
Relogin = 3,
UserLostConn = 4,
EditProfileReq = 5,
EditProfileRes = 6,
AccountUpgradeReq = 7,
AccountUpgradeRes = 8,
UnameLoginReq = 9,
UnameLoginRes = 10,
LoginOutReq = 11,
LoginOutRes = 12,
GetUgameInfoReq = 13,
GetUgameInfoRes = 14,
RecvLoginBonuesReq = 15,
RecvLoginBonuesRes = 16,
GetWorldRankUchipReq = 17,
GetWorldRankUchipRes = 18,
GetSysMsgReq = 19,
GetSysMsgRes = 20,
LoginLogicReq = 21,
LoginLogicRes = 22,
EnterZoneReq = 23,
EnterZoneRes = 24,
EnterMatch = 25,
UserArrived = 26,
ExitMatchReq = 27,
ExitMatchRes = 28,
UserExitMatch = 29,
GameStart = 30,
UdpTest = 31,
LogicFrame = 32,
NextFrameOpts = 33,
}
使用
import { EventMgr } from "../../Framework/Managers/EventMgr";
import { Cmd } from "./Cmd";
import { NetEventDispatcher } from "./NetEventDispatcher";
import { Stype } from "./Stype";
export class AuthProxy {
public static Insance: AuthProxy = new AuthProxy();
//该脚本专注于处理Auth服务
public Init(): void {
EventMgr.Instance.AddEventListener(Stype[Stype.Auth], this, this.onAuthServerReturn);
}
// 接收数据
private onAuthServerReturn(eventName: string, msg: any): void {
// 这里可以根据eventName进行不同的逻辑处理,这里可以使用case的写法
console.log(msg);
}
// 发送登录请求
public UserNameLogin(uname: string, upwd: string): void {
// var md5Pwd = hex_md5(upwd);
NetEventDispatcher.Instance.sendMsg(Stype.Auth, Cmd.UnameLoginReq, {uname: uname, upwd: upwd});
}
}
游戏框架的首屏画面
- 游戏开始的时候需要添加一个加载进度画面,这个画面的资源要预先加载,做法:把Loading界面留在编辑器场景中,或者使用预制体挂载在场景节点中,注意不要打到AB包中。
import { _decorator, Component, Node, Label } from 'cc';
import { EventMgr } from '../../Framework/Managers/EventMgr';
const { ccclass, property } = _decorator;
import { UICtrl } from "./../../Framework/Managers/UICtrl";
@ccclass('UILoading_Ctrl')
export class UILoading_Ctrl extends UICtrl {
private progressValue: Label = null as unknown as Label;
onLoad(): void {
super.onLoad();
this.progressValue = this.view["Progress"].getComponent(Label);
this.progressValue.string = "0%";
EventMgr.Instance.AddEventListener("loadProgress", this, this.onProgressUpdate);
}
private onProgressUpdate(name: string, per: number): void {
this.progressValue.string = per + "%";
}
onDestroy(): void {
EventMgr.Instance.RemoveListenner("loadProgress", this, this.onProgressUpdate)
}
}
Excel表格模块,加载CSV
- 策划使用Excel表格可以导出CSV文件给程序使用
- 程序把csv文件作为文本资源加载
- 解析CSV文件,每一行都是一个表{},存储键值对。总体是一个数组,存储各行的表
- 不仅提供数组访问一行的数据,还提供另外一个数据结构:表,通过主键访问,主键就是第一列的id号。
表格加载代码(直接放到工程中即可)
var CELL_DELIMITERS = [",", ";", "\t", "|", "^"];
var LINE_DELIMITERS = ["\r\n", "\r", "\n"];
var getter = function (index) {
return ("d[" + index + "]");
};
var getterCast = function(value, index, cast, d) {
if (cast instanceof Array) {
if (cast[index] === "number") {
return Number(d[index]);
} else if (cast[index] === "boolean") {
return d[index] === "true" || d[index] === "t" || d[index] === "1";
} else {
return d[index];
}
} else {
if (!isNaN(Number(value))) {
return Number(d[index]);
} else if (value == "false" || value == "true" || value == "t" || value == "f") {
return d[index] === "true" || d[index] === "t" || d[index] === "1";
} else {
return d[index];
}
}
};
var CSV = {
//
/* =========================================
* Constants ===============================
* ========================================= */
STANDARD_DECODE_OPTS: {
skip: 0,
limit: false,
header: false,
cast: false,
comment: ""
},
STANDARD_ENCODE_OPTS: {
delimiter: CELL_DELIMITERS[0],
newline: LINE_DELIMITERS[0],
skip: 0,
limit: false,
header: false
},
quoteMark: '"',
doubleQuoteMark: '""',
quoteRegex: /"/g,
/* =========================================
* Utility Functions =======================
* ========================================= */
assign: function () {
var args = Array.prototype.slice.call(arguments);
var base = args[0];
var rest = args.slice(1);
for (var i = 0, len = rest.length; i < len; i++) {
for (var attr in rest[i]) {
base[attr] = rest[i][attr];
}
}
return base;
},
map: function (collection, fn) {
var results = [];
for (var i = 0, len = collection.length; i < len; i++) {
results[i] = fn(collection[i], i);
}
return results;
},
getType: function (obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
},
getLimit: function (limit, len) {
return limit === false ? len : limit;
},
buildObjectConstructor: function(fields, sample, cast) {
return function(d) {
var object = new Object();
var setter = function(attr, value) {
return object[attr] = value;
};
if (cast) {
fields.forEach(function(attr, idx) {
setter(attr, getterCast(sample[idx], idx, cast, d));
});
} else {
fields.forEach(function(attr, idx) {
setter(attr, getterCast(sample[idx], idx, null, d));
});
}
// body.push("return object;");
// body.join(";\n");
return object;
};
},
buildArrayConstructor: function(fields, sample, cast) {
return function(d) {
var row = new Array(sample.length);
var setter = function(idx, value) {
return row[idx] = value;
};
if (cast) {
fields.forEach(function(attr, idx) {
setter(attr, getterCast(sample[idx], idx, cast, d));
});
} else {
fields.forEach(function(attr, idx) {
setter(attr, getterCast(sample[idx], idx, null, d));
});
}
return row;
};
},
frequency: function (coll, needle, limit) {
if (limit === void 0) limit = false;
var count = 0;
var lastIndex = 0;
var maxIndex = this.getLimit(limit, coll.length);
while (lastIndex < maxIndex) {
lastIndex = coll.indexOf(needle, lastIndex);
if (lastIndex === -1) break;
lastIndex += 1;
count++;
}
return count;
},
mostFrequent: function (coll, needles, limit) {
var max = 0;
var detected;
for (var cur = needles.length - 1; cur >= 0; cur--) {
if (this.frequency(coll, needles[cur], limit) > max) {
detected = needles[cur];
}
}
return detected || needles[0];
},
unsafeParse: function (text, opts, fn) {
var lines = text.split(opts.newline);
if (opts.skip > 0) {
lines.splice(opts.skip);
}
var fields;
var constructor;
function cells(lines) {
var line = lines.shift();
if (line.indexOf('"') >= 0) {// 含引号
// 找到这行完整的数据, 找到对称的双引号
var lastIndex = 0;
var findIndex = 0;
var count = 0;
while (lines.length > 0) {
lastIndex = line.indexOf('"', findIndex);
if (lastIndex === -1 && count % 2 === 0) break;
if (lastIndex !== -1) {
findIndex = lastIndex + 1;
count++;
} else {
line = line + opts.newline + lines.shift();
}
}
var list = [];
var item;
var quoteCount = 0;
var start = 0;
var end = 0;
var length = line.length;
for (var key in line) {
if (!line.hasOwnProperty(key)) {
continue;
}
let numKey = parseInt(key);
var value = line[key];
if (numKey === 0 && value === '"') {
quoteCount++;
start = 1;
}
if (value === '"') {
quoteCount++;
if (line[numKey - 1] === opts.delimiter && start === numKey) {
start++;
}
}
if (value === '"' && quoteCount % 2 === 0) {
if (line[numKey + 1] === opts.delimiter || numKey + 1 === length) {
end = numKey;
item = line.substring(start, end);
list.push(item);
start = end + 2;
end = start;
}
}
if (value === opts.delimiter && quoteCount % 2 === 0) {
end = numKey;
if (end > start) {
item = line.substring(start, end);
list.push(item);
start = end + 1;
end = start;
} else if (end === start) {
list.push("");
start = end + 1;
end = start;
}
}
}
end = length;
if (end >= start) {
item = line.substring(start, end);
list.push(item);
}
return list;
} else {
return line.split(opts.delimiter);
}
}
if (opts.header) {
if (opts.header === true) {
opts.comment = cells(lines); // 第一行是注释
opts.cast = cells(lines); // 第二行是数据类型
fields = cells(lines);
} else if (this.getType(opts.header) === "Array") {
fields = opts.header;
}
constructor = this.buildObjectConstructor(fields, lines[0].split(opts.delimiter), opts.cast);
} else {
constructor = this.buildArrayConstructor(fields, lines[0].split(opts.delimiter), opts.cast);
}
while (lines.length > 0) {
var row = cells(lines);
if (row.length > 1) {
fn(constructor(row), fields[0]);
}
}
return true;
},
safeParse: function (text, opts, fn) {
var delimiter = opts.delimiter;
var newline = opts.newline;
var lines = text.split(newline);
if (opts.skip > 0) {
lines.splice(opts.skip);
}
return true;
},
encodeCells: function (line, delimiter, newline) {
var row = line.slice(0);
for (var i = 0, len = row.length; i < len; i++) {
if (row[i].indexOf(this.quoteMark) !== -1) {
row[i] = row[i].replace(this.quoteRegex, this.doubleQuoteMark);
}
if (row[i].indexOf(delimiter) !== -1 || row[i].indexOf(newline) !== -1) {
row[i] = this.quoteMark + row[i] + this.quoteMark;
}
}
return row.join(delimiter);
},
encodeArrays: function(coll, opts, fn) {
var delimiter = opts.delimiter;
var newline = opts.newline;
if (opts.header && this.getType(opts.header) === "Array") {
fn(this.encodeCells(opts.header, delimiter, newline));
}
for (var cur = 0, lim = this.getLimit(opts.limit, coll.length); cur < lim; cur++) {
fn(this.encodeCells(coll[cur], delimiter, newline));
}
return true;
},
encodeObjects: function (coll, opts, fn) {
var delimiter = opts.delimiter;
var newline = opts.newline;
var header;
var row;
header = [];
row = [];
for (var key in coll[0]) {
header.push(key);
row.push(coll[0][key]);
}
if (opts.header === true) {
fn(this.encodeCells(header, delimiter, newline));
} else if (this.getType(opts.header) === "Array") {
fn(this.encodeCells(opts.header, delimiter, newline));
}
fn(this.encodeCells(row, delimiter));
for (var cur = 1, lim = this.getLimit(opts.limit, coll.length); cur < lim; cur++) {
row = [];
for (var key$1 = 0, len = header.length; key$1 < len; key$1++) {
row.push(coll[cur][header[key$1]]);
}
fn(this.encodeCells(row, delimiter, newline));
}
return true;
},
parse: function (text, opts, fn) {
var rows;
if (this.getType(opts) === "Function") {
fn = opts;
opts = {};
} else if (this.getType(fn) !== "Function") {
rows = [];
fn = rows.push.bind(rows);
} else {
rows = [];
}
opts = this.assign({}, this.STANDARD_DECODE_OPTS, opts);
this.opts = opts;
if (!opts.delimiter || !opts.newline) {
var limit = Math.min(48, Math.floor(text.length / 20), text.length);
opts.delimiter = opts.delimiter || this.mostFrequent(text, CELL_DELIMITERS, limit);
opts.newline = opts.newline || this.mostFrequent(text, LINE_DELIMITERS, limit);
}
// modify by jl 由表自行控制不要含有双引号.提高解析效率
return this.unsafeParse(text, opts, fn) &&
(rows.length > 0 ? rows : true);
},
encode: function (coll, opts, fn) {
var lines;
if (this.getType(opts) === "Function") {
fn = opts;
opts = {};
} else if (this.getType(fn) !== "Function") {
lines = [];
fn = lines.push.bind(lines);
}
opts = this.assign({}, this.STANDARD_ENCODE_OPTS, opts);
if (opts.skip > 0) {
coll = coll.slice(opts.skip);
}
return (this.getType(coll[0]) === "Array" ? this.encodeArrays : this.encodeObjects)(coll, opts, fn) &&
(lines.length > 0 ? lines.join(opts.newline) : true);
}
};
export default CSV;
CSV管理模块
import { _decorator, Component, Node } from 'cc';
import CSV from '../../3rd/CSVParser';
export class ExcelMgr extends Component {
public static Instance: ExcelMgr = null as unknown as ExcelMgr;
public onLoad(): void {
if(ExcelMgr.Instance === null) {
ExcelMgr.Instance = this;
}
else {
this.destroy();
return;
}
}
loadCallback: Function = null as unknown as Function;
cntLoad: number = 0;
curLoad: number = 0;
csvTablesLoaded: any = {};
csvTables:any = {};
csvTableForArr:any = {};
tableCast:any = {};
tableComment:any = {};
addTable (tableName:string, tableContent:string, force?:boolean) {
if (this.csvTables[tableName] && !force) {
return;
}
// 把id作为主键的表
var tableData = {};
// 每一行的内容
var tableArr = [];
// 表头
var opts = { header: true };
// 解析
CSV.parse(tableContent, opts, function (row, keyname) {
tableData[row[keyname]] = row;
tableArr.push(row);
});
// 表头和注释
this.tableCast[tableName] = (CSV as any).opts.cast;
this.tableComment[tableName] = (CSV as any).opts.comment;
// 添加到管理表的表中
this.csvTables[tableName] = tableData;
this.csvTableForArr[tableName] = tableArr;
//this.csvTables[tableName].initFromText(tableContent);
}
getTableArr (tableName:string) {
return this.csvTableForArr[tableName];
}
getTable (tableName:string) {
return this.csvTables[tableName];
}
// 查询一条数据,key=value
queryOne (tableName:string, key:string, value:any) {
var table = this.getTable(tableName);
if (!table) {
return null;
}
if (key) {
for (var tbItem in table) {
if (!table.hasOwnProperty(tbItem)) {
continue;
}
if (table[tbItem][key] === value) {
return table[tbItem];
}
}
} else {
return table[value];
}
}
// 通过id查询一条数据
queryByID (tableName:string, ID:string) {
return this.queryOne(tableName, null, ID);
}
// 查询符合条件 所有数据key=value
queryAll (tableName:string, key:string, value:any) {
var table = this.getTable(tableName);
if (!table || !key) {
return null;
}
var ret = {};
for (var tbItem in table) {
if (!table.hasOwnProperty(tbItem)) {
continue;
}
if (table[tbItem][key] === value) {
ret[tbItem] = table[tbItem];
}
}
return ret;
}
queryIn (tableName:string, key:string, values:Array<any>) {
var table = this.getTable(tableName);
if (!table || !key) {
return null;
}
var ret = {};
var keys = Object.keys(table);
var length = keys.length;
for (var i = 0; i < length; i++) {
var item = table[keys[i]];
if (values.indexOf(item[key]) > -1) {
ret[keys[i]] = item;
}
}
return ret;
}
queryByCondition (tableName:string, condition: any) {
if (condition.constructor !== Object) {
return null;
}
var table = this.getTable(tableName);
if (!table) {
return null;
}
var ret = {};
var tableKeys = Object.keys(table);
var tableKeysLength = tableKeys.length;
var keys = Object.keys(condition);
var keysLength = keys.length;
for (var i = 0; i < tableKeysLength; i++) {
var item = table[tableKeys[i]];
var fit = true;
for (var j = 0; j < keysLength; j++) {
var key = keys[j];
fit = fit && (condition[key] === item[key]) && !ret[tableKeys[i]];
}
if (fit) {
ret[tableKeys[i]] = item;
}
}
return ret;
}
queryOneByCondition (tableName:string, condition: any) {
if (condition.constructor !== Object) {
return null;
}
var table = this.getTable(tableName);
if (!table) {
return null;
}
var keys = Object.keys(condition);
var keysLength = keys.length;
for (let keyName in table) {
var item = table[keyName];
var fit = true;
for (var j = 0; j < keysLength; j++) {
var key = keys[j];
fit = fit && (condition[key] === item[key]);
}
if (fit) {
return item;
}
}
return null;
}
}
使用:
// 游戏开始的逻辑
public initGame(csvText: string): void {
// 加载一个表格数据到我们的Excel管理里面了;
ExcelMgr.Instance.addTable("map", csvText);
var excel = ExcelMgr.Instance.getTableArr("map");
console.log(excel);
// end
var ret = ExcelMgr.Instance.queryOne("map", "mapName", "map008");
console.log(ret);
}
战斗系统的三重设计
- 功能机制层:只提供功能,不提供策略。例如:行走组件、动画播放、攻击组件
- 策略层:基础角色控制、玩家角色控制、NPC策略、Boss策略,调用功能机制层,实现对应的动作:例如:移动,攻击,被攻击
- 行为决策层:玩家输入,NPC固定策略,网络同步策略,AI策略等,主要功能是在什么时机调用策略层的函数。
功能机制层
动画模块示例
import { _decorator, Component, Node, SkeletalAnimation } from 'cc';
export class AnimAction extends Component {
public anim: SkeletalAnimation = null!;
private state: number = 0;
public static AnimState = {
Invalid: -1,
Idle: 0,
Walk: 1,
Skill: 2,
};
static animNames = ["free", "walk", "skill1"];
public init(): void {
this.anim = this.node.getComponentInChildren(SkeletalAnimation);
this.state = AnimAction.AnimState.Invalid;
this.setState(AnimAction.AnimState.Idle);
}
public setState(state: number) {
if(this.state === state) {
return;
}
this.state = state;
// console.log(AnimAction.animNames[this.state], "####");
this.anim.crossFade(AnimAction.animNames[this.state]);
}
}
攻击模块示例
import { _decorator, Component, Node } from 'cc';
export class AttackAction extends Component {
private isAttack: boolean = false;
private hurtTime: number = 0;
private endTime: number = 0;
private onHurt: Function = null!;
private onEndAttack: Function = null!;
private nowTime: number = 0;
public init(): void {
this.isAttack = false;
this.hurtTime = 0;
this.endTime = 0;
this.onHurt = null;
this.onEndAttack = null;
this.nowTime = 0;
}
public doAttack(hurtTime: number,
endTime: number,
onHurt: Function, endAttack: Function): boolean {
if(this.isAttack === true) {
return false; // 失败,返回foce;
}
this.nowTime = 0;
this.isAttack = true;
this.hurtTime = hurtTime;
this.endTime = endTime;
this.onHurt = onHurt;
this.onEndAttack = endAttack;
return true;
}
update(dt: number): void {
if(this.isAttack === false) {
return;
}
this.nowTime += dt;
if(this.nowTime >= this.hurtTime) { // 计算伤害
if(this.onHurt) {
this.onHurt();
this.onHurt = null;
}
}
if(this.nowTime >= this.endTime) {
if(this.onEndAttack) {
this.onEndAttack();
this.onEndAttack = null;
}
this.isAttack = false;
}
}
}
策略层
基础角色控制模块示例
import { _decorator, Component, Node } from 'cc';
import { AnimAction } from './AnimAction';
import { AttackAction } from './AttackAction';
import { FightMgr } from './FightMgr';
export class CharactorCtrl extends Component {
protected animAction: AnimAction = null!;
protected attackAction: AttackAction = null!;
// 战斗属性
protected HP: number = 3;
protected attack: number = 2;
protected define: number = 1;
protected isDead: boolean = false;
// ....
// end
protected skillParams: any = {};
public init(params: any): void {
this.HP = params.HP;
this.attack = params.attack;
this.define = params.define;
this.isDead = false;
if(params.skill) {
this.skillParams.endTime = params.skill.endTime;
this.skillParams.hurtTime = params.skill.hurtTime;
this.skillParams.skillAttack = params.skill.skillAttack;
this.skillParams.attackR = params.skill.attackR;
}
// 功能组件我们要实例化
this.animAction = this.node.addComponent(AnimAction);
this.animAction.init();
this.attackAction = this.node.addComponent(AttackAction);
this.attackAction.init();
// ...
// end
}
public onDoSkillAction() {
if(this.attackAction.doAttack(this.skillParams.hurtTime, this.skillParams.endTime, this.onComputeSkillHurt.bind(this), this.onSkillActionEnd.bind(this))) {
this.animAction.setState(AnimAction.AnimState.Skill);
}
}
public onHurt(attack: number): void {
console.log(attack, this.define);
var lost: number = attack - this.define;
if(lost <= 0) {
return;
}
this.HP -= lost;
if(this.HP <= 0) {
// this.node.removeFromParent();
this.isDead = true;
// 播放死亡动画
console.log("FightMgr.Instance.destroyEnemy");
FightMgr.Instance.destroyEnemy(this.node);
// end
}
}
public onComputeSkillHurt(): void {
// 我就要从地图里面找到我所有的敌人;
var enmeies = FightMgr.Instance.findObjectByRadius(this.node.getWorldPosition(), this.skillParams.attackR);
for(var i = 0; i < enmeies.length; i ++) {
var ctrl: CharactorCtrl = enmeies[i].getComponent(CharactorCtrl);
ctrl.onHurt(this.skillParams.skillAttack);
}
// end
}
public onSkillActionEnd(): void {
this.animAction.setState(AnimAction.AnimState.Idle);
}
}
行为决策层
玩家操作模块示例
import { _decorator, Component, Node, systemEvent, SystemEvent } from 'cc';
import { CharactorCtrl } from './CharactorCtrl';
export class PlayerOpt extends Component {
private ctrl: CharactorCtrl = null;
public init(): void {
this.ctrl = this.node.getComponent(CharactorCtrl);
systemEvent.on(SystemEvent.EventType.TOUCH_START, this.onTouchStart, this);
}
private onTouchStart(): void {
this.ctrl.onDoSkillAction();
}
}
怪物AI模块示例
import { _decorator, Component, Node } from 'cc';
import { CharactorCtrl } from './CharactorCtrl';
export class AIOpt extends Component {
private ctrl: CharactorCtrl = null;
public init(): void {
this.ctrl = this.node.getComponent(CharactorCtrl);
}
private doAI(): void {
// 发一个技能
// this.ctrl.onDoSkillAction();
// end
// 行走
// end
}
update(dt: number): void {
this.doAI();
}
}
加载远程图片
this.img = this.node.getComponent(Sprite);
var str = "http://lf9-sf-be-pack-sign.pglstatp-toutiao.com/ad.union.api/b5dd2924197bf4428751ceb9782e00da?x-expires=1942070400&x-signature=QhZdl6SPjxb4FCx4MGFj2RA4Fps%3D";
assetManager.loadRemote(str, {ext:".jpg"}, (err, imgAsset: any)=>{
if(err) {
console.log("load error");
return;
}
// console.log(imgAsset.data);
var s = SpriteFrame.createWithImage(imgAsset);
// console.log(s, imgAsset);
this.img.spriteFrame = s;
});
}