由於前段時間比較忙所以拖更了,這裏也十分抱歉,加上我這個人平時不經常寫博客,所以會慢一點,以後這種連續的博客我不會拖更了。
那麼進入正題,今天我們實現一個簡單的小插件,其實呢這個小插件也沒什麼卵用,只是可以給更多需要的人提供一個思路,同樣對我來說也是一個筆記,如果各位朋友感興趣的可以繼續往下看。
首先爲什麼要寫編輯器呢?我個人的理解不過於就是方便我們在平時開發,寫一些和引擎交互的東西,可以實現一鍵搞定重複的操作,比如說你要對一個文件夾下的所有預製體都添加一個腳本,那麼我們一個一個添加也可以,但肯定會對於這個重複的操作感到無聊同樣也會浪費開發時間,要是一鍵就能搞定,那會多麼方便是吧,但是轉念一想可能寫的時間還不如自己一個一個拖拽了,所以這個問題的取捨當然也看項目大小,進度等等情況來看,找到一個合適的解決辦法纔是最好的解決辦法。然後開始我們今天的這個小插件。
首先說下這個寫這個插件的意義,也就是需求,假設需求是這樣的:我想弄一個簡單的比如關卡的存儲管理,把場景下的物體在我搭完之後,一鍵可以把場景的裏的物體的位置保存到一個json的配置表中,然後我在加載某一個關卡的時候,根據表中的數據信息把場景還原回來,當然有人也會問那直接做個預支體不好麼?當然也可以,所以也根據項目來看。我突然想到這個也可以用來做一下單機遊戲存檔是吧。
好了,我們給自己的需求定好了,那麼我們自己看下要怎麼去實現這個插件,也就是邏輯。首先分爲幾個部分,1.也就是我們的保存的數據位置,我們需要一個固定的文件夾來存放每一個關卡的配置數據,對資源文件的操作部分2.我們需要一個面板也就是我們場景搭建完成後需要一鍵保存的一個面板,而面板中有什麼功能呢,要有一個一鍵生成的存儲路徑,還要有根據路徑生成的按鈕,以及我刪除這個配置數據的按鈕,還可能有刷新這個資源下配置表的數據的按鈕,當然最好還要有一個換下一關時,直接一鍵把之前搭建好的場景一鍵清空,換下一關的按鈕,至少我覺得這些都要有吧,3.我們需要把場景的物體怎麼通過面板上一鍵存到json配置表中呢,這裏涉及到場景和麪板的進程交互。所以總結下來這三點,那麼按照邏輯我們一步一步來吧,開始前還是先看張效果圖吧:
其實也並不是什麼完整的關卡配置數據表,只是我取的一個名字因爲不知道叫什麼,看下操作流程:我在root節點下比如搭建好了一個關卡,我想把信息保存起來,點擊tool下的按鈕會彈出這個面板,如果路徑不想改的話就用默認的,因爲到時候還原數據的時候也要load加載,所以最好是在resources或者它的子文件夾下,點擊生成配置表按鈕會一鍵把root下面所有的子節點的信息保存到這個配置表中也就是level1那個json裏,至於想保存什麼信息可以自己在代碼中設定,因爲我之前生成了,所以在生成就是覆蓋之前的文件了信息了,有的時候可能生成的數據在引擎沒有沒有及時刷新,點下刷新就好了,如果這一關不想要了,也可點擊刪除,完成一關後點擊重置,會把root場景下的節點全部刪除,好進行下一關配置。
接下來我們在工程的根目錄下的packages文件夾下建一個名爲level-configuration的文件夾,也可以自己換成自己想要的名字,我以我的舉例子。至於爲什麼在packages下操作我前面博客有說。裏面的文件結構如下:
main.js是這個插件的入口,panel下index.js是面板的代碼,scene-obtain.js是操作場景的腳本,那package.json就不用說了,path暫時先沒用到。看下工程裏:
先看下package.json裏的配置內容吧:
{
"name": "level-configuration",
"version": "0.0.1",
"description": "關卡配置表生成工具",
"author": "tm",
"main": "main.js",
"scene-script": "scene-obtain.js",
"main-menu": {
"Tool/LevelConfiguration": {
"message": "level-configuration:open-panel"
}
},
"panel": {
"main": "panel/index.js",
"type": "dockable",
"title": "LevelConfiguration",
"width": 400,
"height": 300
}
}
接下來是main.js裏的內容,引擎啓動後如果沒有resources文件夾會自動創建一個文件夾,後面是一些對文件的操作在主進程中操作:
'use strict';
let fs = require('fs');
let path = require('path');
//默認關卡路徑
let defaultLevelPath='assets/resources/';
module.exports={
load(){
this.init();
},
unload(){
Editor.log('卸載執行');
},
//初始化
init(){
this.createDirectory();
},
//創建初始文件夾
createDirectory(){
if(fs.existsSync(path.join(Editor.Project.path, defaultLevelPath))){
Editor.log('resources exists!');
}else {
// 插件加載後在項目根目錄自動創建指定文件夾
fs.mkdirSync(path.join(Editor.Project.path, defaultLevelPath));
Editor.success('resources created!');
}
},
//創建關卡文件
createLevelFile(filePath){
let levelPath=filePath+".json";
if(fs.existsSync(path.join(Editor.Project.path, levelPath))){
//覆蓋源文件
this.coverTargetFile(levelPath);
}else {
//新建文件
this.createNewFile(levelPath);
}
},
//覆蓋源文件
coverTargetFile(filePath){
Editor.Scene.callSceneScript('level-configuration', 'get-scene-info', function (err, json) {
Editor.assetdb.saveExists( 'db://'+filePath, json, function ( err, meta ) {
if(err){
Editor.log("覆蓋文件失敗!!!");
return;
}
Editor.log("覆蓋文件成功!!!");
});
});
},
//創建一個新的文件
createNewFile(filePath){
Editor.Scene.callSceneScript('level-configuration', 'get-scene-info', function (err, json) {
Editor.assetdb.create( 'db://'+filePath, json, function ( err, results ) {
if(err){
Editor.log("創建文件失敗!!!");
return;
}
Editor.log("創建文件成功!!!");
});
});
},
//刪除文件
deleteLevelFile(filePath){
let levelPath=filePath+".json";
if(fs.existsSync(path.join(Editor.Project.path, levelPath))){
Editor.assetdb.delete(['db://'+levelPath], function (err, results) {
results.forEach(function (result) {
if(err){
Editor.log("刪除文件失敗!!!");
return;
}
Editor.log("刪除文件成功!!!");
});
});
}else {
Editor.log("沒有可刪除的文件!!!");
}
},
messages: {
//打開面板
'open-panel'() {
Editor.Panel.open('level-configuration');
},
//保存按鈕點擊
'save-click'(event, ...args){
const self=this;
self.createLevelFile(args[0]);
},
//設置路徑
'set-path'(event,...args){
if(args[0] && args!=''){
defaultLevelPath=args[0];
this.createDirectory();
}
},
//面板加載完成
'panel-load-finish'(evnet,...args){
Editor.Ipc.sendToPanel('level-configuration','setDefaultPath', defaultLevelPath);
},
//刪除按鈕點擊
'delete-click'(event,...args){
this.deleteLevelFile(args[0]);
}
},
}
然後看下面板index.js的代碼,無非就是界面以及那幾個按鈕綁定的方法,和主進程的通信
let defaultLevelPath='assets/resources/';
let levelNameHeader="level_";
let levelName="";
Editor.Panel.extend({
style: `
:host { margin: 5px; }
h2 { color: #f90; }
.bottom {
height: 30px;
}
`,
template: `
<h2 style="text-align:center">關卡配置表生成器</h2>
<hr />
<div>
FilePath: <span id="label"></span>
<ui-input value="path" id="inputPath"}></ui-input>
</div>
<hr />
<div>
LevelName: <span id="levelLabel"></span>
<ui-num-input style="width: 130px;" step=1 min=1 id="changeLevel"></ui-num-input>
</div>
<hr />
<div style="text-align:right">
<ui-button id="btn" class="green">生成配置表</ui-button>
<ui-button id="deleteBtn" class="red">刪除配置表</ui-button>
<ui-button id="updateBtn" class="blue">刷新資源</ui-button>
<ui-button id="resetBtn">重置場景</ui-button>
</div>
`,
$: {
//保存按鈕
btn: '#btn',
//固定路徑前label
label: '#label',
//關卡路徑前label
levelLabel:'#levelLabel',
//固定路徑
inputPath:'#inputPath',
//更改關卡
changeLevel:'#changeLevel',
//刪除按鈕
deleteBtn:'#deleteBtn',
//刷新按鈕
updateBtn:'#updateBtn',
//重置
resetBtn:'#resetBtn'
},
ready () {
Editor.Ipc.sendToMain('level-configuration:panel-load-finish');
this.init();
this.saveBtnClick();
this.setLevelConfigurationDefaultPath();
this.changeLevelEvent();
this.deleteBtnClick();
this.updateBtnClick();
this.resetBtnClick();
},
init(){
this.$label.innerText = '(默認文件路徑)';
this.$levelLabel.innerText='(關卡名稱)'+levelNameHeader;
this.$changeLevel.value=1;
levelName=levelNameHeader+this.$changeLevel.value;
},
//更改關卡事件
changeLevelEvent(){
this.$changeLevel.addEventListener('change',()=>{
levelName=levelNameHeader+this.$changeLevel.value;
})
},
//重置場景按鈕點擊
resetBtnClick(){
this.$resetBtn.addEventListener('confirm',()=>{
Editor.Scene.callSceneScript('level-configuration', 'reset-scene', function (err, res) {
});
})
},
//刷新按鈕點擊事件
updateBtnClick(){
this.$updateBtn.addEventListener('confirm',()=>{
Editor.assetdb.refresh('db://'+defaultLevelPath, function (err, results) {
if(err){
Editor.log("刷新文件目錄失敗!!!")
return;
}
Editor.log("刷新文件目錄成功!!!")
});
})
},
//刪除按鈕點擊事件
deleteBtnClick(){
this.$deleteBtn.addEventListener('confirm',()=>{
Editor.Ipc.sendToMain('level-configuration:delete-click',defaultLevelPath+levelName);
})
},
//保存按鈕點擊事件
saveBtnClick(){
this.$btn.addEventListener('confirm', () => {
Editor.Ipc.sendToMain('level-configuration:save-click',defaultLevelPath+levelName);
});
},
//設置默認的配置路徑
setLevelConfigurationDefaultPath(){
this.$inputPath.addEventListener('confirm',()=>{
defaultLevelPath=this.$inputPath.value;
Editor.Ipc.sendToMain('level-configuration:set-path',defaultLevelPath);
})
},
messages : {
'setDefaultPath':function (event,...agrs) {
if(agrs[0] && agrs[0]!=""){
defaultLevelPath=agrs[0];
this.$inputPath.value=agrs[0];
}
if (event.reply) {
//if no error, the first argument should be null
event.reply(null, 'Fine, thank you!');
}
}
}
});
然後看下scene-obtain.js裏的內容吧,想保存什麼信息可以自己對應修改setObjInfo這個方法裏的內容
'use strict';
module.exports = {
//獲取場景root節點下的配置信息
'get-scene-info': function (event) {
var can=cc.find('Canvas');
var root = cc.find('Canvas/root');
let objArray=[];
if(root){
for(let i=0;i<parseInt(root.children.length) ;i++){
let singleInfo= this.setObjInfo(root.children[i],i,can);
objArray.push(singleInfo);
}
}else {
Editor.log("沒有root節點");
}
let json=JSON.stringify(objArray);
if (event.reply) {
event.reply(null, json);
}
},
//重置場景
'reset-scene':function (event) {
var root = cc.find('Canvas/root');
if(root){
for(let i=0;i<root.children.length;i++){
root.children[i].destroy();
}
}
if (event.reply) {
event.reply(null, "");
}
},
//設置每個json信息
setObjInfo(gameObject,tag,can){
let name="obj"+tag;
let posX=gameObject.position.x;
let posY=gameObject.position.y;
let scaleX=gameObject.scaleX;
let scaleY=gameObject.scaleY;
let rotation=gameObject.rotation;
let sprite=gameObject.getComponent(cc.Sprite);
let imgName="";
if(sprite && sprite.spriteFrame){
imgName=sprite.spriteFrame.name;
}
var objInfo={
name:name,
posX:posX,
posY:posY,
scaleX:scaleX,
scaleY:scaleY,
rotation:rotation,
canvas_w:can.width,
canvas_h:can.height,
imgName:imgName
}
return objInfo;
}
};
結尾想測試的話,千萬別忘記了在你當前場景下的canvas下新建一個root節點哈,當然如果不想在root下可以自己把代碼裏的部分關於root部分自行去掉,然後就可以測試了呀,代碼裏的內容我就不詳細說了,大概邏輯如開頭所說,其實無非就是寫各自的部分,操作場景的部分,面板的部分,操作資源文件的部分,然後把各個部分通過進程通信連接到一起。若喜歡的小夥伴或有想更深入研究的話,最後還是老樣子,文檔貼在下面,文檔是個學習好東西啊,多看https://docs.cocos.com/creator/manual/zh/extension/。