JS开发HTML5游戏《神奇的六边形》(三)

来源:互联网 发布:国内经济数据 编辑:程序博客网 时间:2024/04/28 02:59

近期出现一款魔性的消除类HTML5游戏《神奇的六边形》,今天我们一起来看看如何通过开源免费的青瓷引擎(www.zuoyouxi.com)来实现这款游戏。

(点击图片可进入游戏体验)

因内容太多,为方便大家阅读,所以分成部分来讲解。

本文为第三部分,主要包括:

11.显示出3个形状

12.形状的拖放处理

13.形状放入棋盘的实现

14.界面管理

15.消除行

若要一次性查看所有文档,也可点击这里。

 

十一. 显示出3个形状

1. 在Scripts/ui创建文件:Pool.js,绘制3个形状。

var s = qc.Serializer; /**  * 3个形状的绘制  */ var Pool = qc.defineBehaviour('qc.tetris.Pool', qc.Behaviour, function() {     var self = this;     /**      * 形状的预置      */      self.blocksPrefab = null;     /**      * 记录下面3个形状的实例      */     self.shapes = []; }, {     blocksPrefab: s.PREFAB }); /**  * 初始化处理  */ Pool.prototype.awake = function() {     var self = this;     self.redraw(); }; /**  * 绘制3个形状  */ Pool.prototype.redraw = function() {     var self = this;     // 先干掉旧的形状数据     for (var i = 0; i < self.shapes.length; i++) {         self.shapes[i].destroy();     }     self.shapes = [];     // 创建3个新的形状     for (i = 0; i < 3; i++) {         self.add(i);     }     self.resize(); }; /**  * 调整位置  */ Pool.prototype.resize = function() {     var self = this, o = self.gameObject;     // 计算X方向的偏移     var offset = o.width * (0.5 - 0.165);     for (var i = 0; i < 3; i++) {         var child = self.shapes[i];         if (!child) return;         child.anchoredX = offset * (i - 1);         child.anchoredY = 0;     } }; /**  * 添加一个形状  */ Pool.prototype.add = function(index) {     var self = this;     var o = self.game.add.clone(self.blocksPrefab, self.gameObject);     var c = o.getScript('qc.tetris.BlocksUI');     c.data = qc.Tetris.Shapes.pool[index];     self.shapes[index] = o; }; /**  * 删除一个形状  */ Pool.prototype.remove = function(index) {     var o = this.shapes[index];     o.destroyImmediately();     this.shapes.splice(index, 1); };


整个代码逻辑比较简单,根据3个形状的数据进行绘制。请参考注释进行理解。

 

2. 将此脚本挂载到UIRoot/pool节点,关联blocksPrefab属性:

3. 运行测试下效果,3个形状正确显示了:

 

十二. 形状的拖放处理

形状在被按下时,需要变大,如果是手机上需要向上做一定的位置偏移。拖拽时形状应该跟着鼠标或手指进行移动。

修改脚本Scripts/ui/BlocksUI.js,添加如下代码:

1. 修改reset函数,增加放大区块的逻辑:

BlocksUI.prototype.reset = function(fixToBoard) {     var self = this, o = self.gameObject;     for (var pos in self._blocks) {         var p = qc.Tetris.readPos(pos);         var pt = qc.Tetris.board.toWorld(p, fixToBoard ? qc.Tetris.BLOCK_H : qc.Tetris.POOL_DISTANCE_NORMAL);         var block = self._blocks[pos];         block.anchoredX = pt.x;         block.anchoredY = pt.y;         var scale = fixToBoard ? 1.13 : 1;         block.find('shadow').scaleX = scale;         block.find('shadow').scaleY = scale;         block.find('block').scaleX = scale;         block.find('block').scaleY = scale;     } };


2. 添加按下的逻辑处理,放大区块:

 /**  * 鼠标按下:放大区块  */ BlocksUI.prototype.onDown = function(e) {     var self = this, o = self.gameObject;     self.drop = false;     self.reset(true);     // 在手机下,需要往上做点偏移     o.y -= self.offsetY; };


  • drop标记当前区块是否被放到棋盘了,刚开始按下清理下环境
  • 按下时需要向上做偏移offsetY

3. 添加鼠标松开或触摸结束的处理,还原区块的位置和大小: 

/**  * 鼠标松开:重置区块大小  */ BlocksUI.prototype.onUp = function() {     var self = this;     self.reset(); };

4. 添加开始拖拽的处理:

/**  * 拖拽开始  */ BlocksUI.prototype.onDragStart = function(e) {     var self = this;     self.drop = false;     self.drag = true;     self.lastPos = '';     self.game.input.nativeMode = true;     self.reset(true);     self.game.log.trace('Start drag:{0}', self.index);     // 复制出可放入标记     var ob = self.flagBlocks = self.game.add.clone(self.gameObject, qc.Tetris.boardUI.gameObject);     ob.children.forEach(function(block) {         block.find('shadow').visible = false;         var b = block.find('block');         b.width = qc.Tetris.BLOCK_W;         b.height = qc.Tetris.BLOCK_H;         b.scaleX = 1;         b.scaleY = 1;         b.frame = 'dark' + b.frame;     });     ob.scaleX = 1;     ob.scaleY = 1;     ob.interactive = false;     self.hideFlag(); };


  • 初始时,标记正在拖拽(drag = true),并且没有被放下(drop = false)
  • 当拖拽到棋盘时,需要实时指示是否可以放下本形状。拖拽开始先清理下最近一次检测的逻辑坐标点(last = '')
  • 设置输入模式nativeMode = true。确保输入事件能被实时处理(默认情况下延后一帧处理,运行效率比较高),本游戏对拖拽的实时响应比较重要。
  • 拖拽开始时,放大并偏移形状(和鼠标按下的逻辑一样)
  • 后续的逻辑:另外复制出本形状,并隐藏掉。这个形状在后续拖拽中,会在棋盘显示出来以指示当前是否可以放入。这个指示的格子图片,使用暗色的图片。

5. 添加拖拽的处理,每帧都会进行调度:

 /**  * 拖拽中  */ BlocksUI.prototype.onDrag = function(e) {     var self = this,         o = self.gameObject;     if (self.drag) {         // 改变节点的目标位置         var p = o.getWorldPosition();         p.x += e.source.deltaX;         p.y += e.source.deltaY;         var lp = o.parent.toLocal(p);         o.x = lp.x;         o.y = lp.y;         // 计算当前对应棋盘中心点的偏移         var board = qc.Tetris.boardUI.gameObject;         p = board.toLocal(p);         p.y += board.height * 0.5;         // 反算出对应的归一化坐标         var xy = qc.Tetris.board.toLocal(p);         var x = Math.round(xy.x),             y = Math.round(xy.y),             pos = qc.Tetris.makePos(x, y);         if (self.lastPos !== pos) {             self.lastPos = pos;             if (qc.Tetris.board.data[pos] &&                 qc.Tetris.board.checkPutIn(pos, self.data.list)) {                 self.showFlag(pos);             }             else {                 self.hideFlag();             }         }     } };

· 在拖拽的事件e中,会指明本帧到上一帧的移动偏移量(屏幕坐标),本形状加上屏幕坐标偏移,这样就移动起来了

· 然后计算本形状的中心点,对应到棋盘的逻辑坐标。并检查目标是否可以放入,如果可以就需要显示指示

· 最近一次检测的逻辑坐标需要记录下来,防止每帧都对同一逻辑坐标检查是否可以放入(白耗CPU)

6. 打开脚本Scripts/logic/Board.js,实现checkPutIn方法:

 Board.prototype.checkPutIn = function(pos, list) {     var self = this;     var pt = qc.Tetris.readPos(pos),         x = pt.x,         y = pt.y;     for (var i = 0; i < list.length; i++) {         var x0 = x + list[i][0],             y0 = y + list[i][1];         // 这个点应该是空的         var block = self.data[qc.Tetris.makePos(x0, y0)];         if (!block) return false;         if (block.value !== 0) return false;     }     return true; };

7. 继续打开Scripts/ui/Blocks.js,继续实现拖拽结束的逻辑:

/**  * 拖拽结束  */ BlocksUI.prototype.onDragEnd = function(e) {     var self = this,         o = self.gameObject;     self.drag = false;     if (self.flagBlocks.visible && self.lastPos) {         // 放到这个位置中去         self.drop = true;         qc.Tetris.operation.putIn(self.index, self.lastPos, self.data);     }     else {         self.reset();         o.parent.getScript('qc.tetris.Pool').resize();     }     // 显示标记可以干掉了     self.flagBlocks.destroy();     delete self.flagBlocks; }; /**  * 隐藏指示标记  */ BlocksUI.prototype.hideFlag = function() {     this.flagBlocks.visible = false; }; /**  * 显示指示标记  */ BlocksUI.prototype.showFlag = function(pos) {     this.flagBlocks.visible = true;     var pt = qc.Tetris.board.data[pos];     this.flagBlocks.anchoredX = pt.x;     this.flagBlocks.anchoredY = pt.y; };

· 拖拽结束后,需要判定形状是否被放入目标节点

· 如果可以放入,则调用指令:qc.Tetris.operation.putIn(下步骤实现)

· 如果不能放入,则需要将位置和大小等还原

· 最后,指示对象需要被析构

8. 在Scripts/operation创建文件PutIn.js,实现放入形状指令:

/**  * 请求放入指定格子,如果成功放入返回true,否则返回false  */ qc.Tetris.operation.putIn = function(index, pos) {     // TODO: 逻辑待实现 };


9. 在Blocks.js中,我们使用到了棋盘对象:qc.Tetris.boardUI.gameObject,但目前这个值(BoardUI)尚未被赋值。
打开BoardUI.js,在构造函数中加入代码赋值:

var BoardUI = qc.defineBehaviour('qc.tetris.BoardUI', qc.Behaviour, function() {     var self = this;     // 登记下本对象     qc.Tetris.boardUI = self;     /**      * 棋盘的棋子元素      */     self.pieces = {};     ...


10. 运行测试下,形状可以随意拖拽了,并且可以反弹回原来位置。不过还无法放入(因为PutIn我们还没实现),请继续后面教程。

十三. 形状放入棋盘的实现

处理流程如下图:

打开文件Scripts/operation/PutIn.js,实现上述代码:

/**     * 请求放入指定格子,如果成功放入返回true,否则返回false     */    qc.Tetris.operation.putIn = function(index, pos) {        var shape = qc.Tetris.Shapes.pool[index],            board = qc.Tetris.board,            ui = qc.Tetris.game.ui,            log = qc.Tetris.game.log;        log.trace('尝试将({0})放入({1})', index, pos);        if (!board.checkPutIn(pos, shape.list)) {            // 禁止放入            return false;        }        log.trace('放入格子:({0})', pos);        // 更新棋盘信息        board.putIn(pos, shape.list, shape.value);        // 计算可以消除的行,并同时消除掉        var lines = board.getFullLines();        lines.forEach(function(flag) {            var children = ui.killLineEffect.find(flag).gameObject.children;            var pts = [];            children.forEach(function(child) { pts.push(child.name); })            board.clearLine(pts);        });        // 计算分数明细,并添加之        var scoreDetail = qc.Tetris.operation.calcScore(lines);        qc.Tetris.score.current += scoreDetail.total;        // 替换为新的形状        qc.Tetris.Shapes.pool.splice(index, 1);        qc.Tetris.Shapes.pool.push(qc.Tetris.Shapes.random());        // 重新绘制棋盘        ui.board.redraw();        // 行消除与分数增加的动画表现        if (lines.length > 0) {            for (var i = 0; i < lines.length; i++) {                ui.killLineEffect.play(i, lines[i], scoreDetail.lines[i]);            }        }        else {            ui.board.getScript('qc.tetris.FlyScore').play(pos, scoreDetail.total);        }        // 当前分数的动画表现        ui.currentScore.setScore();        // 形状飞入的动画表现,并将旧的形状删除掉        ui.pool.remove(index);        ui.pool.add(2);        ui.pool.flyIn(index);        // 死亡检测        if (board.die) {            // 延迟显示死亡界面            log.trace('Game Over!');            qc.Tetris.game.timer.add(3000, function() {                ui.onDie();            });        }        // 放入成功了        return true;    };    /**     * 计算分数明细     * total: 总分数     * lines: [各行的分数]     */    qc.Tetris.operation.calcScore = function(lines) {        var scores = {            total: 40,            lines: []        };        if (lines.length < 1) return scores;        // 计算加成        var append = Math.max(0, lines.length - 1 * 10);        for (var i = 0; i < lines.length; i++) {            var flag = lines[i];            var line = qc.Tetris.game.ui.killLineEffect.find(flag);            var len = line.gameObject.children.length;            scores.lines[i] = len * 20 + append * len;            scores.total += scores.lines[i];            // 40合并到第一行去做表现            if (i === 0) {                scores.lines[i] += 40;            }        }        return scores;    };


  • calcScore方法为计算分数的逻辑
  • 代码中出现了qc.Tetris.game.ui(即UIManager),在下文中陆续实现
  • 另外,本逻辑中加入了一些动画表现,在下文中也陆续实现之
  • 先大致理解下处理流程,细节可以后续章节中逐一理解

十四. 界面管理

1. 在Scripts/ui新建UIManager.js:

/**  * 负责管理所有的游戏界面  */ var UIManager = qc.defineBehaviour('qc.tetris.UIManager', qc.Behaviour, function() {     var self = this;     self.game.ui = self;     self.runInEditor = true; }, {     bestScoreNode: qc.Serializer.NODE,     currentScoreNode: qc.Serializer.NODE,     boardNode: qc.Serializer.NODE,     poolNode: qc.Serializer.NODE,     killLineEffectNode: qc.Serializer.NODE,     uiRoot: qc.Serializer.NODE,     gameOverPrefab: qc.Serializer.PREFAB }); /**  * 初始化管理  */ UIManager.prototype.awake = function() {     var self = this;     /**      * bestScore: BestScore组件      */     if (self.bestScoreNode)         self.bestScore = self.bestScoreNode.getScript('qc.tetris.BestScore');     /**      * currentScore: CurrentScore组件      */     if (self.currentScoreNode)         self.currentScore = self.currentScoreNode.getScript('qc.tetris.CurrentScore');     /**      * board: 棋盘绘制组件      */     if (self.boardNode)         self.board = self.boardNode.getScript('qc.tetris.BoardUI');     /**      * pool: 3个形状的方块      */     if (self.poolNode)         self.pool = self.poolNode.getScript('qc.tetris.Pool');     /**      * killLineEffect: 方块消除的动画组件      */     if (self.killLineEffectNode)         self.killLineEffect = self.killLineEffectNode.getScript('qc.tetris.KillLineEffect'); }; /**  * 游戏重新开始的界面处理  */ UIManager.prototype.restart = function() {     var self = this;     // 重新生成3个新的形状     self.pool.redraw();     // 棋盘重绘制     self.board.redraw();     // 重绘当前分数     self.currentScore.setScore(); }; /**  * 死亡的处理  */ UIManager.prototype.onDie = function() {     // 显示失败页面     this.game.add.clone(this.gameOverPrefab, this.uiRoot); };


  • UIManager引用了几个界面逻辑,其中KillLineEffect脚本下章节再实现
  • 同时,加入了死亡处理接口、重新开始游戏接口,具体的逻辑在后续章节中逐一实现

2. 将脚本挂载到UIRoot,并关联各属性:

部分属性先留空

 

十五. 消除行

以下的行是可以被消除的:

  

逻辑实现

1. 打开Scripts/logic/board.js,将上述3类型的行建立数据结构:

var Board = qc.Tetris.Board = function() {     // 省略一堆代码     ...     // 左斜的9条线,指明起始点坐标     self.xyLines = [         [0, -4],         [1, -4],         [2, -4],         [3, -4],         [4, -4],         [4, -3],         [4, -2],         [4, -1],         [4, 0]     ];     // 横向9条线,指明起始点坐标和长度     self.yLines = [         [0, -4, 5],         [-1, -3, 6],         [-2, -2, 7],         [-3, -1, 8],         [-4, 0, 9],         [-4, 1, 8],         [-4, 2, 7],         [-4, 3, 6],         [-4, 4, 5]     ];     // 右斜9条线,指明起始点坐标和长度     self.xLines = [         [-4, 0, 5],         [-3, -1, 6],         [-2, -2, 7],         [-1, -3, 8],         [0, -4, 9],         [1, -4, 8],         [2, -4, 7],         [3, -4, 6],         [4, -4, 5]     ]; };

2. 实现putIn接口:

Board.prototype.putIn = function(pos, list, value) {     var self = this;     var pt = qc.Tetris.readPos(pos),         x = pt.x,         y = pt.y;     for (var i = 0; i < list.length; i++) {         var x0 = x + list[i][0],             y0 = y + list[i][1];         // 这个点应该是空的         var block = self.data[qc.Tetris.makePos(x0, y0)];         block.value = value;     } };


3. 实现clearLine接口,干掉一行数据:

// 干掉一行 Board.prototype.clearLine = function(pts) {     var self = this;     pts.forEach(function(pos) {         self.data[pos].value = 0;     }); };


4. 实现getFullLines接口,将所有可以消除的行返回:

}     // 右斜9条线     var pts = self.xLines;     for (var i = 0; i < pts.length; i++) {         var start = pts[i], end = [start[0], start[1] + start[2] - 1];         var ok = true;         for (var x = start[0], y = start[1]; y <= end[1];) {             var pos = qc.Tetris.makePos(x, y);             if (self.data[pos].value === 0) {                 // 不符合,不能消除                 ok = false; break;             }             // 下一个点             y++;         }         if (ok) {             // 这条线可以消除,添加进来             lines.push('x' + qc.Tetris.makePos(start[0], start[1]));         }     }     // 左斜的9条线     var pts = self.xyLines;     for (var i = 0; i < pts.length; i++) {         var start = pts[i], end = [start[1], start[0]];         var ok = true;         for (var x = start[0], y = start[1]; true;) {             var pos = qc.Tetris.makePos(x, y);             if (self.data[pos].value === 0) {                 // 不符合,不能消除                 ok = false; break;             }             // 下一个点             if (end[0] > start[0]) {                 x++, y--;                 if (x > end[0]) break;             }             else {                 x--, y++;                 if (x < end[0]) break;             }         }         if (ok) {             // 这条线可以消除,添加进来             lines.push('xy' + qc.Tetris.makePos(start[0], start[1]));         }     }     return lines; };


界面实现

预先将所有的行创建出来,当行被删除时直接显示出来做动画表现。以下流程中,我们首先创建一个格子的预制,再创建一个行的预置。

1. 在board节点下,创建Image对象,设置属性如下图:

2.将新创建的block节点拖入Assets/prefab目录,创建预制。然后从场景中删除。

3. 在board节点下,创建Node对象,设置属性如下图:

4. 为节点挂载TweenAlpha动画组件,消失时需要淡出:

  • 透明度从1变化到0
  • 耗时0.5秒
  • 变化的曲线是:先平缓的做变化,然后在快速变化为0
  • 图片中from和to值设置反了,请手工设置下from=1,to=0

5. 在Scripts/ui下创建脚本Line.js,控制行的绘制和表现:

/**  * 消除一行的表现界面  */ var LineUI = qc.defineBehaviour('qc.tetris.LineUI', qc.Behaviour, function() {     var self = this;     // 描述行的信息     self.flag = 'xy';     self.x = 0;     self.y = 0; }, {     blockPrefab: qc.Serializer.PREFAB }); Object.defineProperties(LineUI.prototype, {     /**      * 取得行标记      */     key: {         get: function() {             return this.flag + qc.Tetris.makePos(this.x, this.y);         }     },     /**      * 取得本行的格子数量      */     count: {         get: function() {             return this.gameObject.children.length;         }     } }); /**  * 初始化行  */ LineUI.prototype.init = function(flag, start, end) {     var self = this;     self.flag = flag;     self.x = start[0];     self.y = start[1];     // 创建一个格子     var createBlock = function(pos) {         var block = self.game.add.clone(self.blockPrefab, self.gameObject);         block.frame = 'white.png';         block.anchoredX = qc.Tetris.board.data[pos].x;         block.anchoredY = qc.Tetris.board.data[pos].y;         block.name = pos;         return block;     };     switch (flag) {     case 'xy':         for (var x = self.x, y = self.y; true;) {             createBlock(qc.Tetris.makePos(x, y));             // 下一个点             if (end[0] > start[0]) {                 x++, y--;                 if (x > end[0]) break;             }             else {                 x--, y++;                 if (x < end[0]) break;             }         }         break;     case 'y':         for (var x = start[0], y = start[1]; x <= end[0];) {             createBlock(qc.Tetris.makePos(x, y));             x++;         }         break;     case 'x':         for (var x = start[0], y = start[1]; y <= end[1];) {             createBlock(qc.Tetris.makePos(x, y));             y++;         }     }     // 初始时隐藏掉     self.gameObject.name = self.key;     self.gameObject.visible = false; }; /**  * 播放消失的动画  */ LineUI.prototype.playDisappear = function(index) {     var self = this,         o = self.gameObject,         ta = self.getScript('qc.TweenAlpha');     o.visible = true;     o.alpha = 1;     ta.delay = 0;     ta.resetToBeginning();     ta.onFinished.addOnce(function() {         // 隐藏掉         o.visible = false;     });     ta.playForward(); };


  • flag和x、y属性描述了行的信息(左斜行、水平行还是右斜行,起始点的坐标)

6. 将此脚本挂载到Line节点,并设置blockPrefab为第一步骤创建的格子预置:

7. 将line拖进Assets/prefab目录,创建预制。然后从场景中删除。

8. 在Scripts/ui创建脚本KillLineEffect.js,处理行消失表现的逻辑

/**  * 行消除的动画表现  */ var KillLineEffect = qc.defineBehaviour('qc.tetris.KillLineEffect', qc.Behaviour, function() {     var self = this;     /**      * 所有的行      */     self.lines = {};     /**      * 两行之间的播放延迟      */     self.delay = 300; }, {     delay: qc.Serializer.NUMBER,     linePrefab: qc.Serializer.PREFAB }); /**  * 初始化:将用于表现的行全部创建出来放着  */ KillLineEffect.prototype.awake = function() {     var self = this;     // 创建用于消除表现的格子行     var createLine = function(flag, start, end) {         var ob = self.game.add.clone(self.linePrefab, self.gameObject);         var line = ob.getScript('qc.tetris.LineUI');         line.init(flag, start, end);         self.lines[line.key] = line;     };     var pts = qc.Tetris.board.xyLines;     for (var i = 0; i < pts.length; i++) {         var start = pts[i], end = [start[1], start[0]];         createLine('xy', start, end);     }     var pts = qc.Tetris.board.yLines;     for (var i = 0; i < pts.length; i++) {         var start = pts[i], end = [start[0] + start[2] - 1, start[1]];         createLine('y', start, end);     }     var pts = qc.Tetris.board.xLines;     for (var i = 0; i < pts.length; i++) {         var start = pts[i], end = [start[0], start[1] + start[2] - 1];         createLine('x', start, end);     } }; KillLineEffect.prototype.find = function(flag) {     return this.lines[flag]; }; KillLineEffect.prototype.play = function(index, flag, score) {     var self = this;     var line = self.find(flag);     var delay = index * self.delay;     var playFunc = function() {         // 冒出分数         var children = line.gameObject.children;         var pos = children[Math.round(children.length/2) - 1].name;         self.getScript('qc.tetris.FlyScore').play(pos, score);         // 消失动画         line.playDisappear();     };     if (delay <= 0) {         playFunc();     }     else {         self.game.timer.add(delay, playFunc);     } };

· 在脚本初始化时,将所有行的数据构建出来,并隐藏掉

· delay表示在多行消失时,其动画的间隔时间

· 在动画表现时,有分数表现,FlyScore下一章再补充 

9. 将KillLineEffect挂载到board节点(棋盘),并设置linePrefab:

10. 运行工程,就可以看到这些“行”了:

11. 选中UIRoot节点,设置UIManager的Kill Line Effect Node属性(board节点,因为board挂载了KillLineEffect脚本):

 

 

上一篇:JS开发HTML5游戏《神奇的六边形》(二) 

下一篇:JS开发HTML5游戏《神奇的六边形》(四)

1 0
原创粉丝点击