昨天參加了hack day的一個比賽,賽制大致是:24小時,自由組隊2~4人,任意發揮。運氣比較好,拿了第三名和最佳創意獎。
建議先看看這個demo,bug是有的,chrome下玩玩,測試測試就行,O(∩_∩)O~
DEMO:http://qianduannotes.sinaapp.com/3dtank/html/index.html
基本效果:
關於
懶得去SAE上折騰,沒把那另外一半的功能補上,不過我還是介紹下這幾個沒補上功能吧。
1. 音效。開始音樂是比較古老的坦克大戰開機音樂。
① 開始音效 點擊播放
② 發子彈 點擊播放
③ 擊中坦克 點擊播放
④ 爆炸 點擊播放
②和③是自己錄制的,呵呵,DIY的東西才好玩。
2. 登錄驗證
采用的是解鎖,這個創意應該是非常不錯的,當登錄的時候,A、B玩家下方會生成一個如上圖的canvas解鎖塊,當然這個解鎖卡也會通過socket傳送到手機遙控端,手機解鎖成功后方可登錄。
3. 坦克360°旋轉
由於鍵盤控制只能上下左右,所以360°是轉不出來的..剛想截一張手機控制游戲的圖,總是報錯...囧(后台用的是php,socket控制信號傳輸,剛打開手機端網頁的時候php socket報錯)。
手機遙控端視圖:
這里主要利用的是手機多點觸控,touchstart,touchmove,touchend這三個事件。
function canvasAddListener() { canvas.addEventListener('touchstart', onTouchStart, false); canvas.addEventListener('touchmove', onTouchMove, false); canvas.addEventListener('touchend', onTouchEnd, false); }
4. 重新開始游戲和打死坦克添加效果等
以上都是沒有公開顯示出來的效果,下次弄好了再上傳吧,嘻嘻。
先說說前台
前台主要采用的是css3和js(這是廢話)。
1. css3構建一個3D游戲場地
效果:
.box { width:500px; height:500px; position:relative; -webkit-transform-style: preserve-3d; /*-webkit-transform: rotateY(40deg);*/ -webkit-transition:all 1s ease-in-out; } .inBox { width:300px; height:300px; overflow: hidden; text-align: center; box-shadow: 0px 0px 2px white; background:rgba(255,255,255,.2); /*background:#779443;*/ position:absolute; top:100px; left:100px; color:white; } .box-forward { -webkit-transform: rotateY(0deg) translateZ(150px); } .box-back { -webkit-transform: rotateY(180deg) translateZ(150px); } .box-left { -webkit-transform: rotateY(270deg) translateZ(150px); } .box-right { -webkit-transform: rotateY(90deg) translateZ(150px); } .box-top { -webkit-transform:rotateX(90deg) translateZ(150px); } .box-bottom { -webkit-transform:rotateX(-90deg) translateZ(150px); }
上面是css部分,比賽過程中,參看了下張鑫旭大哥的文章(之前這塊還不是很了解的),文章鏈接。就不細說了。
下面是HTML部分:
<div class="w"> <div class="box"> <div class="inBox box-forward" data-num="upF rightF downF leftF" data-v="1"><div></div></div> <div class="inBox box-back" data-num="upF leftF downF rightF" data-v="2"><div></div></div> <div class="inBox box-left" data-num="upF forwardF downF backF" data-v="3"><div></div></div> <div class="inBox box-right" data-num="upF backF downF forwardF" data-v="4"><div></div></div> <div class="inBox box-top" data-num="backF rightF forwardF leftF" data-v="5"><div></div></div> <div class="inBox box-bottom" data-num="backF leftF forwardF rightF" data-v="6"><div></div></div> </div> </div>
2. JS這塊,寫了比較多。

var $ = document.querySelectorAll.bind(document); /** * @description core part * @author hustskyking */ var faces = $(".inBox"), bloodA = $(".bloodA div")[0], bloodB = $(".bloodB div")[0], box = $(".box")[0], upF = $(".box-top")[0], downF = $(".box-bottom")[0], leftF = $(".box-left")[0], rightF = $(".box-right")[0], forwardF = $(".box-forward")[0], backF = $(".box-back")[0], cSize = upF.clientWidth, datas = { "upF": upF, "downF": downF, "leftF": leftF, "rightF": rightF, "forwardF": forwardF, "backF": backF }, stopN = 0, tanks = {}, randomTank = 2, tankLimit = 20, //a w d s j ← ↑ → ↓ p debug_keySet = [[65, 87, 68, 83, 74], [37, 38, 39, 40, 80]]; /** * @Class Tank * @attrs width, height, currentX, currentY, speedX, speedY, angleX, angleY, plusy, plusx * container, dataset, stopN, tank, tankId, life, bulletBox, color, timerLimit, timer, gap, sTime */ var Tank = function(setting){ var opts = setting || {}; this.container = opts.container || forwardF; this.dataset = this.container.getAttribute('data-num').split(" "); this.belong = 0; this.grade = 0; this.debug = false; this.width = opts.width || 14; this.height = opts.height || 18; this.stopN = "keyup" + (window.stopN++); this.gap = opts.gap || 30; this.speed = opts.speed || 3; this.ax = 0; this.ay = 0; this.cx = opts.cx || (cSize - this.width) / 2; this.cy = opts.cy || (cSize - this.height) / 2; this.sx = 0; this.sy = 0; this.tank = null; this.tankId = ""; this.life = 100; this.color = opts.color || "#0047B3"; this.bulletBox = []; this.bColor = opts.bColor || "#0047B3"; this.timerLimit= 400; this.gunAngle = 0; this.timer = null; this.sTime = 0; } Tank.prototype = { init: function(tankId, debug){ if(tankId){ this.tankId = tankId; this.belong = (Number(tankId) > 2 ? 2 :Number(tankId)); }else{ throw new Error("必須設置tank ID"); } var this_ = this; //繪制坦克 this.paintTank(); if(debug){ this.debug = true; this.keySet = debug.keySet; window.addEventListener("keydown", function(){ this_.move_debug(); }, false); } window.addEventListener(this.stopN, function(){ clearInterval(this_.timer); }, false); }, paintTank: function(){ var tank = document.createElement("span"); var circle = document.createElement("span"); var gun = document.createElement("span"); gun.setAttribute("class", "gun"); tank.setAttribute("class", "tank"); circle.setAttribute("class", "circle"); tank.setAttribute("id", "tank" + this.tankId); gun.style.borderColor = this.color; circle.style.borderColor = this.color; tank.style.cssText = "width:" + this.width + "px;height:" + this.height + "px;top:" + this.cy + "px;left:" + this.cx + "px;border-color:" + this.color; tank.appendChild(gun); tank.appendChild(circle); this.container.getElementsByTagName("div")[0].appendChild(tank); this.tank = tank; return tank; }, switchPainter: function(N){ //N 1->up 2->right 3->down 4->down this.ay %= 360; var r = false, a = 0; if (this.ay > 180) { this.ay -= 360; r = true; } if (this.ay < -180) this.ay += 360; this.container = datas[this.dataset[N]]; this.dataset = this.container.getAttribute('data-num').split(" "); this.container.getElementsByTagName("div")[0].appendChild(this.tank); if(this.belong >= 2) return; box.style.cssText = "-webkit-transform: rotateY(" + (this.ay + (r ? -a : a)) + "deg);"; }, setAngle: function(x, y, ang){ this.sx = x; this.sy = y; this.gunAngle = 90 - ang / Math.PI * 180; $("#tank" + this.tankId)[0].style.cssText += "-webkit-transform: rotate(" + this.gunAngle + "deg);"; }, move: function(ang){ this.setAngle(Math.cos(ang), -Math.sin(ang), ang); this.stopAni(); this.ani(); }, direction_debug: function(){ switch(event.keyCode) { case this.keySet[0]: return "left"; case this.keySet[1]: return "up"; case this.keySet[2]: return "right"; case this.keySet[3]: return "down"; case this.keySet[4]: return "shooter"; } }, move_debug: function(){ this.stopAni(); switch(this.direction_debug()) { case "up": console.log("up"); this.setAngle(0, -1, Math.PI / 2); this.gunAngle_debug = 0; break; case "down": console.log("down"); this.setAngle(0, 1, Math.PI / 2 * 3); this.gunAngle_debug = 180; break; case "left": console.log("left"); this.setAngle(-1, 0, Math.PI); this.gunAngle_debug = 90; break; case "right": console.log("right"); this.setAngle(1, 0, Math.PI * 2); this.gunAngle_debug = -90; break; case "shooter": console.log("shooter"); this.shooter(); default: return; } this.ani(); }, ani: function(){ var this_ = this; this.timer = setInterval(function(){ this_.detective(); // this_.detectiveTank(); this_.cx += this_.sx * this_.speed; this_.cy += this_.sy * this_.speed; this_.detective(); this_.tank.style.top = this_.cy + "px"; this_.tank.style.left = this_.cx + "px"; }, this.gap); }, stopAni: function(){ var event = document.createEvent('HTMLEvents'); event.initEvent(this.stopN, true, true); event.eventName = this.stopN; window.dispatchEvent(event); }, detective: function(){ if(this.cx - this.width >= cSize){ //right this.ay += -90; this.ax += 0; this.switchPainter(1); this.cx = 1; } if(this.cx + this.width <= 0){ //left this.ay += 90; this.ax += 0; this.switchPainter(3); this.cx = cSize + this.width - 1; } if(this.cy <= 0){ //top this.sy = 0; this.cy = 4; } if(this.cy + this.width >= cSize){ //bottom this.sy = 0; this.cy = cSize - this.width - 8; } }, detectiveTank: function(){ if(this.cx - this.width >= cSize){ //right this.ay += -90; this.ax += 0; this.switchPainter(1); this.cx = 1; } if(this.cx + this.width <= 0){ //left this.ay += 90; this.ax += 0; this.switchPainter(3); this.cx = cSize + this.width - 1; } if(this.cy <= 0){ //top this.sy = 0; this.cy = 4; } if(this.cy + this.width >= cSize){ //bottom this.sy = 0; this.cy = cSize - this.width - 8; } }, shooter: function(){ if((new Date()).getTime() - this.sTime < this.timerLimit) return; var bullet = new Bullet(this.gunAngle, this.cx, this.cy, this.tankId); this.container.getElementsByTagName("div")[0].appendChild(bullet.bullet); bullet.move(); this.sTime = (new Date()).getTime(); }, destroy: function(){ this.stopAni(); this.shooterTimer && clearInterval(this.shooterTimer); this.moveTimer && clearInterval(this.moveTimer); if(this.tankId >= 2 && randomTank <= tankLimit) createRandomTank(); delete tanks[this.tankId]; this.tank.parentNode && this.tank.parentNode.removeChild(this.tank); delete this; } } /** * @Bullet Tank * @attrs bullet, timer, gap, x, yadsjsw */ var Bullet = function(gA, x, y, tankId){ this.bullet = document.createElement("span"); this.timer = null; this.gap = 30; this.passed = 0; var a = Math.cos(gA / 180 * Math.PI - Math.PI / 2), b = Math.sin(gA / 180 * Math.PI - Math.PI / 2); if(Math.abs(a) > 1) a = a > 0 ? Math.floor(a) : Math.ceil(a); if(Math.abs(b) > 1) b = b > 0 ? Math.floor(b) : Math.ceil(b); this.a = a; this.b = b; this.x = x + 6; this.y = y + 7; this.bullet.setAttribute("class", "bullet"); this.bullet.style.top = -10 + "px"; this.bullet.style.left = -10 + "px"; this.bullet.style.borderColor = tanks[tankId].bColor; this.tankId = tankId; this.container = $("#tank" + this.tankId)[0].parentNode.parentNode; this.belong = tanks[this.tankId].belong; } Bullet.prototype = { bSwitchPainter: function(N){ this.container = this.bullet.parentNode.parentNode; this.dataset = this.container.getAttribute('data-num').split(" "); this.container = datas[this.dataset[N]]; this.dataset = this.container.getAttribute('data-num').split(" "); this.container.getElementsByTagName("div")[0].appendChild(this.bullet); this.passed++; if(this.passed == 4){ this.destroy(); } }, detective: function(){ if(this.x - 2 >= cSize){ //right this.bSwitchPainter(1); this.x = 1; } if(this.y - 2 >= cSize){ //bottom this.destroy(); } if(this.x + 2 <= 0){ //left this.bSwitchPainter(3); this.x = cSize + 3; } if(this.y + 2 <= 0){ //top this.destroy(); } }, move: function(){ var this_ = this; this.timer = setInterval(function(){ this_.detective(); this_.x += this_.a * 5; this_.y += this_.b * 5; this_.checkHit(); this_.bullet.style.top = this_.y + "px"; this_.bullet.style.left = this_.x + "px"; }, this.gap); }, checkHit: function(){ for (var i in tanks) { if (i == this.tankId) continue; if (tanks[i].belong == this.belong) continue; var tx = tanks[i].cx, ty = tanks[i].cy, bc = this.container.getAttribute("data-v"), tc = tanks[i].container.getAttribute("data-v"), w = 14; if ((bc == tc) && (this.x < tx + w) && (this.x > tx - w) && (this.y < ty + w) && (this.y > ty - w) ) { console.log(i, tanks[i].life); this.hit(tanks[i]); this.destroy(); } } }, hit: function(tTank){ tTank.life -= 20; if(tTank.belong == 0){ console.log("1掉血"); bloodA.style.bottom = 300 * tTank.life / 100 + "px"; if(tTank.life == 0){ setTimeout(function(){ $(".pujieL")[0].style.display = "block"; }, 2000); } }else if(tTank.belong == 1){ console.log("2掉血"); bloodB.style.bottom = 300 * tTank.life / 100 + "px"; if(tTank.life == 0){ setTimeout(function(){ $(".pujieR")[0].style.display = "block"; }, 2000); } }else { var add = 5; if(tanks[this.tankId].belong == 0){ if(tTank.life == 0){ add = 20; //window.addTankGrade(0); } tanks[this.tankId].grade += add; $("#gradeA")[0].innerHTML = tanks[this.tankId].grade; }else{ if(tTank.life == 0){ add = 20; //window.addTankGrade(1); } tanks[this.tankId].grade += add; $("#gradeB")[0].innerHTML = tanks[this.tankId].grade; } } if(tTank.life <= 0){ tTank.destroy(); console.log("destroyed"); } }, destroy: function(){ clearInterval(this.timer); if (typeof tanks[this.tankId] != 'undefined') { var tankArr = tanks[this.tankId].bulletBox; tankArr.splice(tankArr.indexOf(this),1); } this.bullet.parentNode && this.bullet.parentNode.removeChild(this.bullet); } } /* function createTank(color){ var tank = document.createElement("span"); var circle = document.createElement("span"); var gun = document.createElement("span"); gun.setAttribute("class", "gun"); tank.setAttribute("class", "tank"); circle.setAttribute("class", "circle"); gun.style.borderColor = color; circle.style.borderColor = color; tank.style.cssText = "width:14px;height:18px;position:relative;border-color:" + color; tank.appendChild(gun); tank.appendChild(circle); return tank; } function addTankGrade(N){ var box, color, tank; if(N == 0) { box = $("#tankBoxA")[0]; color = "#8500FF"; }else{ box = $("#tankBoxB")[0]; color = "#6D6D27"; } tank = createTank(color); box.appendChild(tank); } */ function createRandomTank(obj) { var obj = obj || {cx: 150, cy: 150, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}; var rTank = new Tank(obj); rTank.init(String(randomTank)); rTank.shooterTimer = null; rTank.shooterTimer = setInterval(function(){ if(Math.random() > 0.8){ rTank.shooter(); } }, 200); rTank.moveTimer = null; rTank.moveTimer = setInterval(function(){ if(Math.random() > 0.4){ var ang = Math.PI * 2 * Math.random(); rTank.move.call(rTank, ang); } }, 1200); tanks[randomTank] = rTank; randomTank++; } function run_debug(){ tanks[0] = new Tank({cx: 50, cy: 220, color:"red", bColor:"red"}); tanks[0].init('0', { keySet:debug_keySet[0] }); tanks[1] = new Tank(); tanks[1].init('1', { keySet:debug_keySet[1] }); createRandomTank({cx: 250, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); createRandomTank({cx: 220, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); createRandomTank({cx: 190, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); createRandomTank({cx: 160, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); createRandomTank({cx: 130, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); createRandomTank({cx: 100, cy: 220, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}); } /* window.onload = function(){ var layer = document.createElement("div"); layer.setAttribute("id", "layer"); var stripe = document.createElement("div"); stripe.setAttribute("id", "stripe"); stripe.style.top = (document.clientHeight - 250) / 2 + "px"; document.body.appendChild(layer); document.body.appendChild(stripe); }*/ $("#gradeA")[0].innerHTML = $("#gradeB")[0].innerHTML = 0; //run_debug(); function showMsg(msgContent){ var box = document.createElement("div"); var msg = document.createElement("div"); box.setAttribute("id", "showMsgBox"); msg.setAttribute("id", "msgBox"); with(box.style){ position = "absolute"; top = 0; left = 0; bottom = 0; right = 0; } with(msg.style){ margin = "0 auto"; width = "500px"; height = "400px"; marginTop = "100px"; padding = "55px"; fontSize = "30px"; fontFamily = "微軟雅黑"; lineHeight = "40px"; } msg.innerHTML = msgContent || "空"; box.appendChild(msg); document.body.appendChild(box); } showMsg("<b style='color:white'>[測試版本]</b>本版本只實現了一半的功能,最終版本是手機控制,並且有音效、登錄驗證、坦克可以360°旋轉,因為需要配置環境,沒有公開。左右邊界可以穿過,為3D效果。5槍可以搞定一個坦克。" + "<p style='color:yellow;text-align:center'>按鍵 ctrl+alt+J 開始</p><p>①:WSAD 控制上下左右,J發子彈</p><p>②:↑↓←→ 控制上下左右,p發子彈</p>") window.onkeydown = function(){ if(event.ctrlKey && event.altKey && event.keyCode == 74){ $("#showMsgBox")[0].style.display = "none"; $("#msgBox")[0].style.display = "none"; run_debug(); } }
拆開分析下:
① Tank對象
DIY坦克(還行,哈哈哈~):
var Tank = function(setting){ var opts = setting || {}; this.container = opts.container || forwardF; this.dataset = this.container.getAttribute('data-num').split(" "); //.... } Tank.prototype = { init: function(tankId, debug){ //.... if(debug){ this.debug = true; this.keySet = debug.keySet; window.addEventListener("keydown", function(){ this_.move_debug(); }, false); } window.addEventListener(this.stopN, function(){ clearInterval(this_.timer); }, false); }, paintTank: function(){ //.... }, switchPainter: function(N){ //.... }, direction_debug: function(){ //.... }, move_debug: function(){ //.... }, ani: function(){ //.... }, stopAni: function(){ var event = document.createEvent('HTMLEvents'); event.initEvent(this.stopN, true, true); event.eventName = this.stopN; window.dispatchEvent(event); }, detective: function(){ //.... }, detectiveTank: function(){ //.... }, shooter: function(){ //.... }, destroy: function(){ //.... } }
難點在於一些邊界的判斷,但是好好考慮下也不算什么難點咯~這些代碼中應該看到了很多debug之類的變量和函數,因為我寫了兩種模式,一種是手機端玩,一中是電腦鍵盤控制(debug模式)。
②子彈對象
/** * @Bullet Tank * @attrs bullet, timer, gap, x, yadsjsw */ var Bullet = function(gA, x, y, tankId){ //.... };
Bullet.prototype = { move: function(){ //.... }, checkHit: function(){ //.... }, hit: function(tTank){ //.... }, destroy: function(){ clearInterval(this.timer); if (typeof tanks[this.tankId] != 'undefined') { var tankArr = tanks[this.tankId].bulletBox; tankArr.splice(tankArr.indexOf(this),1); } this.bullet.parentNode && this.bullet.parentNode.removeChild(this.bullet); } }
和坦克一樣,都有一個destroy函數,銷毀對象。
③ 構建AI對象
function createRandomTank(obj) { var obj = obj || {cx: 150, cy: 150, color:"#ECFF0B", bColor:"#ECFF0B", speed: 1, container: backF}; var rTank = new Tank(obj); rTank.init(String(randomTank)); rTank.shooterTimer = null; rTank.shooterTimer = setInterval(function(){ if(Math.random() > 0.8){ rTank.shooter(); } }, 200); rTank.moveTimer = null; rTank.moveTimer = setInterval(function(){ if(Math.random() > 0.4){ var ang = Math.PI * 2 * Math.random(); rTank.move.call(rTank, ang); } }, 1200); tanks[randomTank] = rTank; randomTank++; }
機器人是個麻煩的東西,這塊雖然不難,銷毀他們的時候費了不少力氣~~~主要是那么timer要跟着一起銷毀。
④ 構建對象說明
tanks[0] = new Tank({cx: 50, cy: 220, color:"red", bColor:"red"}); tanks[0].init('0', { keySet:debug_keySet[0] });
這里需要說明一下,只要init后面加了第二個參數,就是調試模式,也就是說鍵盤是可以控制運行的。
設置了一個全局變量
//a w d s j ← ↑ → ↓ p debug_keySet = [[65, 87, 68, 83, 74], [37, 38, 39, 40, 80]];
3. socket這塊
整個平台信息的交互就是以他為核心,socket這個東西還算比較新,所以學習的時候也沒找到太多的資料,只能對着w3c的一些文檔邊試邊做。
var socket; function ws_init() { var host = "ws://192.168.86.1:1111/"; // var host = "ws://202.114.20.79:1111/"; try { socket = new WebSocket(host); logMsg('WebSocket - status '+socket.readyState); socket.onopen = function(msg) { logMsg("Welcome - status "+this.readyState); send('display'); }; socket.onclose = function(msg) { logMsg("Disconnected - status "+this.readyState); }; socket.onmessage = function(msg) { //.... }; } catch(ex) { logMsg(ex); } } function send(msg) { try { socket.send(msg + '='); } catch(ex) { logMsg(ex); } }
socket在前端部分是非常簡單的,就是三個事件onopen, onclose, onmessage來驅動,重點還是后台操作,真心麻煩!
后台部分
后台用的是php,本來打算使用nodeJS,不是十分熟練,24個小時的賽制花太長時間去學習也不現實,所以就用了比較熟悉的php來建立socket連接,還算成功吧。
這個部分以后有時間說。先碎覺~~
最后,別忘了這個DEMO哦, http://qianduannotes.sinaapp.com/3dtank/html/index.html