最近學習使用了一款HTML5遊戲引擎(青瓷引擎),並用它嘗試作了一個鬥地主的遊戲,簡單實現了單機對戰和網絡對戰,代碼可已放到github上,在此談談本身如何經過引擎來開發這款遊戲的。html
客戶端代碼前端
服務端代碼java
(點擊圖片進入遊戲體驗)git
本篇文章爲第一部分,主要包括單機模式的開始佈局設計準備。主要內容以下:github
鬥地主遊戲對於你們應該是算耳熟能詳的遊戲了,我就簡單說明下本身理解的整個遊戲的流程。網絡
遊戲的主體,如圖所示:編輯器
使用青瓷引擎的編輯器很容易幫我完成整個遊戲界面的佈局。作這個遊戲以前我從事java開發,也作過一段時間的前端開發,對js和前端佈局算是比較熟悉。結合官方的手冊、demo,我大概花了兩三天時間把這些過了遍,嘗試修改完善一些demo的內容,接下來我就開始本身嘗試作這個遊戲了。我的感受有強大的編輯器,佈局能夠比較迅速的完成,而後專一地進行邏輯編寫。ide
個人想法是把單機和多人對戰分紅兩個場景,建立項目工程landlord和主場景,其中main是主場景,single是單機版的場景,online是在線對戰的場景。函數
在Scripts下建立start.js,並配置爲入口腳本,此腳本首先定義了名字空間,將全局的數據都記錄在這裏
。代碼以下:工具
1 // 定義本工程的名字空間
2 qc.landlord = {}; 3
4 // 用來存放全部的全局數據(函數、變量等)
5 window.G = qc.landlord.G = {}; 6
7 // 初始化邏輯
8 qc.initGame = function(game) { 9 game.log.trace('Start the game landlord.'); 10 // 將game實例的引用記錄下來,方便在其餘邏輯腳本模塊中訪問
11 G.game = game; 12 //遊戲規則
13 G.gameRule = new qc.landlord.GameRule(); 14
15 G.online = new qc.landlord.Online(); 16 //AI邏輯
17 //G.AILogic = new qc.landlord.AILogic();
18 String.prototype.trim = function() { return this.replace(/(^\s*)|(\s*$)/g,''); }; 19 };
點擊start.js腳本,在右側的Inspector面板下點擊edit,把start.js拖入並放置到第一的位置就成爲入口腳本了。
主場景設計入如圖:
在Scripts/ui下建立MainUI.js腳本並掛載在lock節點下掛載(直接拖動腳本防置到節點),主要實現了兩個按鈕的事件,代碼以下:
1 /** 2 * 主場景腳本 3 * @method defineBehaviour 4 */
5 var MainUI = qc.defineBehaviour('qc.engine.MainUI', qc.Behaviour, function() { 6 this.singleBtn = null; 7 this.multiBtn = null; 8 this.singleScene = null; 9 }, { 10 singleBtn : qc.Serializer.NODE, 11 multiBtn: qc.Serializer.NODE, 12 enterNameBtn : qc.Serializer.NODE, 13 backBtn: qc.Serializer.NODE, 14 nameField: qc.Serializer.NODE, 15 enterNamePanel: qc.Serializer.NODE, 16 message: qc.Serializer.NODE, 17 singleScene: qc.Serializer.STRING, 18 multiScene: qc.Serializer.STRING 19 }); 20
21 // Called when the script instance is being loaded.
22 MainUI.prototype.awake = function() { 23 var self = this; 24 //單機按鈕事件
25 self.addListener(self.singleBtn.onClick, function(){ 26 self.game.state.load(self.singleScene, false, function() { 27 //牌組管理
28 G.cardMgr = new qc.landlord.Card(); 29 //玩家本人
30 G.ownPlayer = new qc.landlord.Player('QCPlayer'); 31 G.ownPlayer.isAI = false; 32 var storage = G.game.storage; 33 var ownScore = storage.get('QCPlayer'); 34 G.ownPlayer.score = ownScore ? ownScore : 500; 35 //電腦玩家(左)
36 G.leftPlayer = new qc.landlord.Player('AI_Left'); 37 var leftScore = storage.get('AI_Left'); 38 G.leftPlayer.score = leftScore ? leftScore : 500; 39 //電腦玩家(右)
40 G.rightPlayer = new qc.landlord.Player('AI_Right'); 41 var rightScore = storage.get('AI_Right'); 42 G.rightPlayer.score = rightScore ? rightScore : 500; 43
44 //指定玩家順序
45 G.ownPlayer.nextPlayer = G.rightPlayer; 46 G.rightPlayer.nextPlayer = G.leftPlayer; 47 G.leftPlayer.nextPlayer = G.ownPlayer; 48
49 //底牌
50 G.hiddenCards = []; 51 //當前手牌
52 G.currentCards = []; 53 }, function() { 54 console.log(self.singleScene + '場景加載完畢。'); 55 }); 56 }, self); 57
58 //多人對戰按鈕事件
59 self.addListener(self.multiBtn.onClick, function(){ 60
61 var uid = G.game.storage.get('uid'); 62 if(uid){ 63 self.showMessage(self.MSG_WAITING); 64 self.enterGame(uid); 65 } else { 66 var np = self.enterNamePanel.getScript('qc.TweenAlpha'); 67 np.from = 0; 68 np.to = 1; 69 np.resetToBeginning(); 70 self.enterNamePanel.visible = true; 71 np.playForward(); 72 } 73
74 }, self); 75
76 //確認按鈕事件
77 self.addListener(self.enterNameBtn.onClick, function(){ 78 var nickname = self.nameField.text.trim(); 79 if(nickname){ 80 self.showMessage(self.MSG_WAITING); 81 var result = G.online.register(nickname); 82 result.then(function(data){ 83 if(data.uid){ 84 G.game.storage.set('uid', data.uid); 85 G.game.storage.save(); 86 self.enterGame(data.uid); 87 } else if(err === G.online.ERR_EXIST_NAME){ 88 self.showMessage(self.MSG_EXIST_NAME); 89 } 90 }).catch(function(err){ 91 if(err === G.online.ERR_EXIST_NAME){ 92 self.showMessage(self.MSG_EXIST_NAME); 93 } 94 }); 95 } 96 }, self); 97
98 //返回按鈕事件
99 self.addListener(self.backBtn.onClick, function(){ 100 var np = self.enterNamePanel.getScript('qc.TweenAlpha'); 101 np.from = 1; 102 np.to = 0; 103 np.resetToBeginning(); 104 //self.enterNamePanel.visible = true;
105 np.playForward(); 106 np.onFinished.addOnce(function (){ 107 self.enterNamePanel.visible = false; 108 }, self); 109 window.onbeforeunload = undefined; 110 }, self); 111 }; 112
113 MainUI.prototype.enterGame = function(uid) { 114 var self = this; 115 G.ownPlayer = new qc.landlord.Player(null); 116 G.ownPlayer.uid = uid; 117 window.onbeforeunload = function (){ 118 var warning="確認退出遊戲?"; 119 return warning; 120 }; 121 G.online.joinGame(G.ownPlayer.uid).then(function(data){ 122 var player = data.seats ? data.seats[data.ownSeatNo] : data.desk.seats[data.ownSeatNo]; 123 G.ownPlayer.deskNo = player.deskNo; 124 G.ownPlayer.name = player.name; 125 G.ownPlayer.seatNo = data.ownSeatNo; 126 G.netInitData = data; 127 self.game.state.load(self.multiScene, false, function() { 128 }, function() { 129 console.log(self.multiScene + '場景加載完畢。'); 130 }); 131 }); 132 }; 133
134 MainUI.prototype.showMessage = function(m) { 135 var self = this; 136 if(m){ 137 self.message.text = m; 138 self.message.visible = true; 139 } else { 140 self.message.visible = false; 141 } 142 }; 143
144 MainUI.prototype.MSG_WAITING = '請稍等'; 145 MainUI.prototype.MSG_EXIST_NAME = '您輸入的暱稱已被使用,請重試';
鬥地主主要的主體就是玩家、紙牌,製做主場景以前,先編寫好這兩個實體類
玩家類設計:在Scripts/logic建立player.js代碼以下:
1 // define a user behaviour
2 var Player = qc.landlord.Player = function (n){ 3 var self = this; 4 // 玩家名
5 self.name = n ? n : 'Player'; 6 //是不是地主
7 self.isLandlord = false; 8 //是不是AI玩家
9 self.isAI = true; 10 //牌組
11 self.cardList = []; 12 //下一家
13 self.nextPlayer = null; 14 //如下屬性用於多人對戰
15 self.uid = null; 16 self.deskNo = null; 17 self.seatNo = null; 18 //是否已經準備
19 self.isReady = false; 20 //是否已經離開
21 self.isLeave = false; 22 }; 23 Object.defineProperties(Player.prototype, { 24 score: { 25 get: function(){ 26 return this._score; 27 }, 28 set: function(v){ 29 this._score = v; 30 var storage = G.game.storage; 31 storage.set(this.name, v); 32 storage.save(); 33 } 34 } 35 });
牌組類設計:在Scripts/logic/clone建立card.js代碼以下:
1 // define a user behaviour
2 var Card = qc.landlord.Card = function (){ 3 this.data = [ 4 {icon: 'j1.jpg', type: '0', val: 17}, 5 {icon: 'j2.jpg', type: '0', val: 16}, 6 {icon: 't1.jpg', type: '1', val: 14}, 7 {icon: 't2.jpg', type: '1', val: 15}, 8 {icon: 't3.jpg', type: '1', val: 3}, 9 {icon: 't4.jpg', type: '1', val: 4}, 10 {icon: 't5.jpg', type: '1', val: 5}, 11 {icon: 't6.jpg', type: '1', val: 6}, 12 {icon: 't7.jpg', type: '1', val: 7}, 13 {icon: 't8.jpg', type: '1', val: 8}, 14 {icon: 't9.jpg', type: '1', val: 9}, 15 {icon: 't10.jpg', type: '1', val: 10}, 16 {icon: 't11.jpg', type: '1', val: 11}, 17 {icon: 't12.jpg', type: '1', val: 12}, 18 {icon: 't13.jpg', type: '1', val: 13}, 19 {icon: 'x1.jpg', type: '2', val: 14}, 20 {icon: 'x2.jpg', type: '2', val: 15}, 21 {icon: 'x3.jpg', type: '2', val: 3}, 22 {icon: 'x4.jpg', type: '2', val: 4}, 23 {icon: 'x5.jpg', type: '2', val: 5}, 24 {icon: 'x6.jpg', type: '2', val: 6}, 25 {icon: 'x7.jpg', type: '2', val: 7}, 26 {icon: 'x8.jpg', type: '2', val: 8}, 27 {icon: 'x9.jpg', type: '2', val: 9}, 28 {icon: 'x10.jpg', type: '2', val: 10}, 29 {icon: 'x11.jpg', type: '2', val: 11}, 30 {icon: 'x12.jpg', type: '2', val: 12}, 31 {icon: 'x13.jpg', type: '2', val: 13}, 32 {icon: 'h1.jpg', type: '3', val: 14}, 33 {icon: 'h2.jpg', type: '3', val: 15}, 34 {icon: 'h3.jpg', type: '3', val: 3}, 35 {icon: 'h4.jpg', type: '3', val: 4}, 36 {icon: 'h5.jpg', type: '3', val: 5}, 37 {icon: 'h6.jpg', type: '3', val: 6}, 38 {icon: 'h7.jpg', type: '3', val: 7}, 39 {icon: 'h8.jpg', type: '3', val: 8}, 40 {icon: 'h9.jpg', type: '3', val: 9}, 41 {icon: 'h10.jpg', type: '3', val: 10}, 42 {icon: 'h11.jpg', type: '3', val: 11}, 43 {icon: 'h12.jpg', type: '3', val: 12}, 44 {icon: 'h13.jpg', type: '3', val: 13}, 45 {icon: 'k1.jpg', type: '4', val: 14}, 46 {icon: 'k2.jpg', type: '4', val: 15}, 47 {icon: 'k3.jpg', type: '4', val: 3}, 48 {icon: 'k4.jpg', type: '4', val: 4}, 49 {icon: 'k5.jpg', type: '4', val: 5}, 50 {icon: 'k6.jpg', type: '4', val: 6}, 51 {icon: 'k7.jpg', type: '4', val: 7}, 52 {icon: 'k8.jpg', type: '4', val: 8}, 53 {icon: 'k9.jpg', type: '4', val: 9}, 54 {icon: 'k10.jpg', type: '4', val: 10}, 55 {icon: 'k11.jpg', type: '4', val: 11}, 56 {icon: 'k12.jpg', type: '4', val: 12}, 57 {icon: 'k13.jpg', type: '4', val: 13} 58 ]; 59 }; 60 //拷貝牌組,返回一組新的牌組
61 Card.prototype.getNewCards = function () { 62 return this.data.slice(0); 63 };
在開始作主場景以前,須要作一些預製(點擊我看文檔),方便後面調用。主要仍是圍繞玩家、紙牌進行,引擎在這方面也給咱們提供了很強大的支持:
1 /** 2 * 卡牌規則 3 */
4 var CardUI = qc.defineBehaviour('qc.engine.CardUI', qc.Behaviour, function() { 5 this.isSelected = false; 6 this.info = null; 7 }, { 8 // fields need to be serialized
9 }); 10
11 /** 12 *顯示紙牌, 13 *@property info 卡牌信息, 14 *@property isSelect 是否選中 15 */
16 CardUI.prototype.show = function (info, isSelect){ 17 var self = this, 18 o =self.gameObject; 19 if(info){ 20 o.frame = info.icon; 21 o.resetNativeSize(); 22 o.visible = true; 23 self.info = info; 24 } 25 }; 26
27 /** 28 * 選中紙牌,紙牌上移 29 */
30 CardUI.prototype.onClick = function (){ 31 var self = this; 32 if(self.isSelected){ 33 this.gameObject.anchoredY = 0; 34 } else { 35 this.gameObject.anchoredY = -28; 36 } 37 self.isSelected = !self.isSelected; 38 var ui = window.landlordUI ? window.landlordUI : window.olLandlordUI; 39 if(ui.getReadyCardsKind()){ 40 ui.playBtn.state = qc.UIState.NORMAL; 41 } else { 42 ui.playBtn.state = qc.UIState.DISABLED; 43 } 44 };
二、玩家預製:完成以後發現這個並非很必要,由於是一開始都固定的,遊戲中不會添加改動。好比右邊玩家,以下圖:
每一個玩家下都要掛載一個腳本,主要是方便咱們後面去查找它的內容,這個好比修改分數,修改手牌數之類的操做,代碼以下:
1 // define a user behaviour
2 var PlayerUI = qc.defineBehaviour('qc.engine.PlayerUI', qc.Behaviour, function() { 3 // need this behaviour be scheduled in editor
4 //this.runInEditor = true;
5 this.player = null; 6 }, { 7 headPic : qc.Serializer.NODE, 8 playerScore : qc.Serializer.NODE, 9 cardContainer : qc.Serializer.NODE 10 });
三、信息預製:這個預製只是個文字,作這個預製主要用於叫分出牌的時候,好比給某個玩家顯示 2分 、不叫、 不出之類的信息,這樣直接加入不須要再去設置字體大小的信息。
單機模式場景,我大體劃分爲如下幾個模塊,具體能夠使用青瓷引擎編輯器打開,這種所見即所得的工具應該很快就能瞭解整個場景界面的佈局設計,如圖:
導入圖形資源並打包圖集(點擊我看文檔),
將全部的撲克牌圖形資源到Assets/atlas/poker@atlas下,身份頭像圖形資源到Assets/atlas/landlord@atlas,按鈕圖形資源到Assets/atlas/btn@atlas。