去吧!皮卡丘!
小時候擁有一臺任天堂是多少熊孩子的夢想,每一個夜晚被窩裏透出的微弱光線,把小小的童年帶入另外一個世界,家門口的鳥和狗,森林裏的蟲和瀑布,山洞裏的超音蝠,帶着小小的夢,走過一個個城市,一路冒險,飛天潛水,攀瀑碎巖,所向披靡。
每一個醒來的清晨,都恍如出門冒險的那天~css
基於開放地圖二次開發,完成簡易像素版PokemonGohtml
一、用戶體系
二、揹包
三、圖鑑
四、人物定位
五、精靈分佈
六、精靈捕捉
七、排行榜
八、移動隨機事件
九、新手引導前端
一、地圖增長道館挑戰
二、平常任務系統css3
一、精靈交易
二、玩家對戰
三、AR捕捉場景web
*目前只完成第一階段的功能
功能 / 廠商 | 百度地圖 | 騰訊地圖 | 高德地圖 |
---|---|---|---|
自定義皮膚 | 支持 | 不支持 | 支持 |
實時定位 | 不支持 | 支持 | 支持 |
開發文檔 | 差 | 通常 | 友好 |
對比三個地圖廠商,咱們選擇高德地圖進行二次開發ajax
登陸http://lbs.amap.com/
控制檯-應用管理-建立新應用-添加新KEY數據庫
具體參考微信公衆平臺開發者文檔
https://mp.weixin.qq.com/wiki...api
咱們須要一些接口來保存用戶數據,因此須要找一個服務端的同窗配合完成幾個簡單的接口
一、api/login 判斷登陸狀態,獲取用戶基本信息
二、api/getGlassPokemon 獲取草地精靈
三、api/getMyPokemon 獲取揹包精靈
四、api/catchPokemon 捕捉精靈
五、api/getRank 獲取排行榜信息數組
一、簡單設計主界面UI,肯定功能佈局、地圖的配色方案:微信
二、準備150只精靈的素材圖片(大小各一套)
在<head></head>
中引入高德地圖js-sdk
<head> <script src="http://webapi.amap.com/maps?v=1.3&key=464e2c3addc64c5894994afe0bbdca21"> </head>
key的值爲高德地圖開發者中心建立應用後得到的key
在html中建立地圖容器
<div id="gomap"></div>
在js中初始化地圖
var gomap; gomap = new AMap.Map('gomap', { zoomEnable : false, //不容許縮放 zoom:18, //默認縮放等級18 center: [118.18088, 24.4896], //初始定位座標 });
默認的地圖樣式不能知足咱們的需求,高德地圖提供了地圖皮膚編輯器:高德地圖皮膚編輯器
在編輯器中修改道路,陸地,建築,水域,綠地等顏色,同時在配置中隱藏了一些道路、建築與標記,簡化地圖。
編輯完成後點擊發布,得到地圖樣式ID:e6fa21422698f8a28585158d9d075f1d
在地圖初始化中引入地圖樣式便可
var gomap; gomap = new AMap.Map('gomap', { zoomEnable : false, zoom:18, center: [118.18088, 24.4896], mapStyle : 'amap://styles/e6fa21422698f8a28585158d9d075f1d' });
這樣看起來就有點遊戲的樣子了
咱們須要把地圖和主角定位在當前位置,而且在移動時實時更新定位,這就須要藉助AMap的geolocation插件
gomap.plugin('AMap.Geolocation',function(){ var geo = new AMap.Geolocation({ showButton: false, showCircle: false, showMarker : true, //顯示定位圖標 markerOptions : { content : '<div class="Symbol hero"></div>', //設置marker自定義節點內容 } }); gomap.addControl(geo); geo.watchPosition(); //實時獲取定位 AMap.event.addListener(geolocation, 'complete', onComplete);//返回定位成功信息 AMap.event.addListener(geolocation, 'error', onError); //返回定位出錯信息 })
小智一我的站在地圖上有點孤單,咱們給他加一個光環放大的效果,看起來像是在發出檢測信號:
.Symbol.hero:after{ -webkit-animation:heroWave 2s ease infinite; background:rgba(255,255,181,0.1); content:''; width:100px; height:100px; display:block; position:absolute; left:-30px; top:-30px; border-radius:100%; box-shadow:0 0 0 1px rgba(255,255,181,0.7); opacity:0.7; } @-webkit-keyframes heroWave{ 0%{ -webkit-transform:scale(0.2);opacity:0} 50%{ opacity:1} 100%{ -webkit-transform:scale(1);opacity:0} }
有了定位,咱們還須要知道本身移動的方向,方便接近目標,因此咱們在界面右上角放置了一個虛擬羅盤
經過監聽HTML5的deviceorientation獲取指南針角度信息,改變羅盤旋轉方向:
if (window.DeviceOrientationEvent) { window.addEventListener("deviceorientation", function(event){ var dir = event.webkitCompassHeading; $("#J_pin").css("-webkit-transform",'rotate('+ (360-dir) +'deg)'); }, false); }
查看DEMO (羅盤只在移動端生效,掃碼查看)
因爲精靈的編號,屬性,星級等數據是固定的,在前端建立一個保存精靈圖鑑數據的JSON文件,以減小服務端返回數據的複雜度,經過編號在圖鑑中索引對應精靈的相關數據
var Pokedex = [ { 'number':'001', 'name' :'妙蛙種子', 'name_jp' : 'フシギダネ', 'name_en' : 'Bulbasaur', 'properties' : ['草','毒'], 'star' : 4, }, { 'number':'002', 'name' :'妙蛙草', 'name_jp' : 'フシギソウ', 'name_en' : 'Ivysaur', 'properties' : ['草','毒'], 'star' : 4, }, ... ]; //精靈屬性顏色配置 var Pokedexcolor = { '草' : '#1ba50e,#2ec920', '冰' : '#13c6db,#57e9ff', '超能力' :'#dd045b,#f7478d', '蟲' : '#889610,#b5b214', '地面' : '#af8a19,#d8b343', '電' : '#b28200,#ffd621', '毒' : '#752464,#9e448c', '飛行' : '#4381ff,#72aefc', '鋼' : '#6d6d8a,#aaaabb', '格鬥' : '#902918,#bb5544', '火' : '#c72500,#f05526', '龍' : '#2b1aa6,#7766ee', '水' : '#2b1aa6,#3088e1', '岩石' : '#907d2f,#a89755', '通常' : '#969685,#bbbbaa', '幽靈' : '#3d3d7c,#5f52a7', '妖精' : '#3d3d7c,#5f52a7', }
主角誕生了,如今開始在周圍生成一些隨機的精靈,調用getGlassPokemon接口,傳遞當前位置座標,服務端在座標半徑1千米內生成必定個數的精靈,前端經過返回的座標和精靈編號將對應精靈添加到地圖上。
因爲後續接口須要驗證微信受權信息,爲了便於DEMO查看請先訪問一次模擬登錄接口:http://www.guowc.cc/api/sysUs...
getGlassPokemon
接口返回數據格式:
data : { { id : 231, //精靈惟一標識,用於捕捉成功後從數據庫中精準刪除地圖對應精靈 number: "77", //精靈編號,用於圖鑑中獲取更多精靈信息 lng : 118.094561807441 //精靈經度 lat : 24.4805797983452 //精靈緯度 }, ... }
首先在前面的geolocation插件中調用getCurrentPosition()
方法,獲取一次初始定位座標,
將座標傳給getPokemons
接口拉取草地精靈數據
var self = this gomap.plugin('AMap.Geolocation',function(){ var geo = new AMap.Geolocation({ ... }); ... //首次定位 geo.getCurrentPosition(function( status, result ){ heroPoint.lng = result.position.lng; heroPoint.lat = result.position.lat; self.getPokemon(heroPoint) }); })
請求接口數據:
getPokemon : function(point) { var self = this Method.fetch(Api.getGlassPokemons,{ lng:point.lng,lat:point.lat },function(data){ var res = data.data; for(var i = 0; i < res.length; i++){ self.addPokemon(res[i]); } }); },
Method.fetch爲封裝的ajax方法,只貼出關鍵流程代碼,具體詳見DEMO
獲取到數據後,循環調用addPokemon方法,將精靈添加到地圖上:
addPokemon : function(data) { var pid = Method.getPid(data.number); //獲取精靈編號(格式化'12'=>'012') var marker = new AMap.Marker({ map: Common.gomap, position: [data.position_x, data.position_y], icon: new AMap.Icon({ size: new AMap.Size(40, 40), imageSize : new AMap.Size(40, 40), image: "images/pokemon/PM_icon_"+ pid +".png", }), }); },
如今咱們就能在地圖上看到精靈了~
這一步咱們先把已捕捉到的精靈列表保存起來,以便後續使用:
getMyPokemons : function(){ Method.fetch(API.getMyPokemons,{},function(data){ for(var i = 0 ;i < data.data.length; i++){ State.bag.push(Method.getPid(data.data[i].number)); //將已得到精靈編號保存在全局State.bag數組中 } }); }
精靈收集是整個遊戲的核心功能,原版pokemonGo精靈捕捉過程爲AR實景捕捉形式,咱們把精靈的捕捉形式簡化了,保留街機時代的像素風格。最先在實現這個功能時,採用的策略是當玩家座標與地圖精靈小於必定距離時,自動進入精靈捕捉場景,這種方式存在幾個問題:
通過優化,將捕捉規則修改成:直接點擊地圖精靈便可捕捉,半徑500米外提示用戶超過捕捉範圍。
優化後的方案下降了捕捉門檻,也鼓勵用戶走動去發現和捕捉更多精靈。
上一步的addPokemon方法中,咱們已經向地圖中添加了精靈點標記(marker),但此時地圖上的精靈惟一區分只是圖片不一樣而已,咱們還需爲每一個marker綁定對應的精靈信息,併爲每一個marker綁定點擊事件,下面完善一下addPokemon方法:
addPokemon : function(data) { var self = this; var nid = parseInt(data.number); var pid = Method.getPid(data.number); //獲取精靈編號(格式化'12'=>'012'); var id = data.id; var marker = new AMap.Marker({ map: Common.gomap, position: [data.position_x, data.position_y], icon: new AMap.Icon({ size: new AMap.Size(40, 40), imageSize : new AMap.Size(40, 40), image: "images/pokemon/PM_icon_"+ pid +".png", }), extData : { nid : nid, id : id, } }); marker.on('click',function(e){ self.clickPokemon(e) }) },
因爲地圖上顯示精靈圖標太小,沒法展現更多信息,因此點擊精靈後,不直接進入戰鬥,先彈出對應精靈的卡牌:
clickPokemon : function(e){ var self = this; var data = e.target.getExtData(); self.initPokecard(data.nid,e.target); },
初始化卡牌彈窗:
<div class="Modal pokedex" id="js-modal-pokedex"> <div class="card"> <div class="namebox"> <p class="name_cn"></p> <p class="name_jp"></p> </div> <div class="pokebox"> <img class="pokeimg" src="" alt=""> <div class="Widget stars"> <div class="star"></div> </div> </div> <div class="propbox"> <div class="num"></div> <div class="prop"></div> </div> <p class="Widget timer tips"><b class="t">TIPS </b>捕捉半徑<span class="t">500</span>米 當前距離<span class="t dist">0</span>米</p> <span id="js-btn-catch" class="catchbtn">捕捉</span> </div> </div>
initPokecard : function(nid, target) { var $card = Element.$pokedex, $catch = Element.$catch; var data = Pokedex[nid - 1]; //從圖鑑JSON中獲取對應精靈詳細圖鑑數據 var props = '', dist = 0; var imgUrl = 'images/pokemon_big/PM_animation_'+ data.number +'.png'; //拼接屬性節點 for(var i = 0; i< data.properties.length; i++){ var color = Pokedexcolor[data.properties[i]].split(','); props += '<div class="item" style="background:'+ color[1] +';border-color:'+ color[0] +'">'+ data.properties[i] +'</div>'; } $card.find('.name_cn').text(data.name); //精靈中文名 $card.find('.name_jp').text(data.name_jp + data.name_en); //精靈外文名 $card.find('.star')[0].className = 'star star_' + data.star; //精靈星級 $card.find('.num').text('No.' + data.number); //精靈編號 $card.find('.prop').html(props); //精靈屬性 //預加載精靈大圖 $card.find('.pokebox').removeClass('loaded'); Method.loadImg(imgUrl, function() { $card.find('.pokeimg').attr('src',imgUrl); $card.find('.pokebox').addClass('loaded'); }); $card.addClass('show'); }
到這裏就完成了精靈卡片的初始化(與揹包圖鑑共用),然而卡片只帶有固定數據,須要跟單純的圖鑑查看器作區分,咱們在卡片下方加上操做區,操做區有3種狀態:精靈可捕捉,精靈已得到,精靈超出捕捉範圍:
initPokecard : function(nid, target) { ... //判斷點擊精靈行爲來自地圖仍是圖鑑 if(target) { var pa = target.getPosition(), pb = [State.heroPoint.lng,State.heroPoint.lat]; var dist = parseInt(pa.distance(pb)); //計算主角與點擊精靈距離 $card.addClass('catch'); $card.off().on('touchend',function(e){ $(this).removeClass('show'); e.preventDefault(); }) if( dist < 500 ) { $card.find('.tips').hide(); if(State.bag.indexOf(data.number) != -1){ $catch.removeClass().addClass('ownbtn').text('已得到'); }else{ $catch.removeClass().addClass('catchbtn').text('捕捉'); } }else{ $card.find('.dist').text(dist); $card.find('.tips').show(); $catch.removeClass().addClass('overbtn').text('走近點啊親'); } }else{ $card.removeClass('catch'); $card.find('.tips').hide(); } }
首先爲卡片下方的捕捉按鈕綁定事件,將點擊的精靈數據傳遞到meetPokemon方法中(初始化精靈捕捉場景方法):
initPokecard : function(nid, target) { ... if(target) { var ext = target.getExtData() $catch.off(); //解除捕捉按鈕綁定事件 ... if( dist < 500 ){ if(State.bag.indexOf('data.number) != -1){ ... }else{ $catch.on('touchend', self.meetPokemon(ext.nid,ext.id)); target.setMap(null); //開始捕捉後將地圖上對應小精靈移除(捕捉成功or失敗小精靈都會消失) } } }else{ } }
而後初始化捕捉場景:
meetPokemon : function(nid,id){ Element.$modal.removeClass('show'); Element.$body.addClass('State catching'); //進入捕捉場景須要控制界面多處UI,因此把狀態class放到body上 Element.$catchBox.find('.texture').attr('src','images/pokemon_big/PM_animation_' + Pokedex[nid].number + '.png'); Element.$catchBox.find('.pname').text(Pokedex[nid].name); Element.$catchBox.find('.star')[0].className = 'star star_' + Pokedex[nid].star; },
如今點擊丟出精靈球開始捕捉精靈,首先在meetPokemon中綁定精靈球的點擊事件:
meetPokemon : function(nid,id){ ... Element.$catchBox.find('.ballbox').off().on('touchend',function(){ self.catchPokemon(nid,id) }) },
接下來實現catchPokemon方法:
catchPokemon : function(nid,id){ var self = this; var name = Pokedex[nid].name, pid = Pokedex[nid].number, star = Pokedex[nid].star, rate = 0.8 - star / 10; //根據星級決定捕捉成功機率 if(State.catching) return; if(Method.random(rate)){ //捕捉成功 Element.$catchBox.addClass('catchwin'); //在catchbox上增長catchwin控制捕捉成功動畫(動畫具體實現參照DEMO) Method.fetch(API.catchPokemon,{ id : id },function(){ console.log('捕捉成功!') //向服務端發送捕捉成功請求,移除getGlassPokemon返回的對應位置精靈,同時加入用戶揹包 }); setTimeout(function(){ //動畫播放結束後調用捕捉成功彈窗 self.awardBox('images/pokemon_big/PM_animation_' + pid + '.png',name,function(){ //彈窗關閉後回到主界面 $body.removeClass(); Element.$catchBox.removeClass('catchwin'); State.catching = false; }); },3200); }else{ //捕捉失敗 Element.$catchBox.addClass('catchfail'); //在catchbox上增長catchfail控制捕捉失敗動畫 Method.fetch(API.catchPokemon,{ id : id , flag : true},function(){ console.log('捕捉失敗!') //向服務端發送捕捉失敗請求,僅移除getGlassPokemon返回的對應位置精靈 }); setTimeout(function(){ //動畫播放失敗後調用捕捉失敗彈窗 self.alertBox(name + ' 逃跑了!',function(){ //彈窗關閉後回到主界面 Element.$body.removeClass(); Element.$catchBox.removeClass('catchfail'); State.catching = false; }); },1500); } State.catching = true; },
捕捉成功與失敗:
awardBox : function(img,name,callback){ var cbk = callback || function(){}; Element.$award.addClass('show'); Element.$award.find('.img').attr('src',img); Element.$award.find('.pname').text(name); Element.$award.find('.confirm').off().on('tap',function(){ Element.$award.removeClass('show'); cbk(); }); }, alertBox : function(title,callback){ var cbk = callback || function(){}; Element.$alert.addClass('show'); Element.$alert.find('.heading').text(title); Element.$alert.find('.confirm').off().on('tap',function(){ Element.$alert.removeClass('show'); cbk(); }); },
捕捉失敗過程:
捕捉成功過程:
還原經典,咱們加入一個簡單的新手引導,經過與大木博士的對話,肯定玩家性別,第一個夥伴,和簡單的遊戲玩法介紹。
首先實現一個簡單的打字效果:
typing(char,delay){ var chars = char.split(''); var index = 0, delay = 0; Element.$typeText.html(''); Element.$typeNext.hide(); //打字過程隱藏下一步箭頭 State.typeOver = false; var timer = setInterval(function(){ if(delay == delay){ if(index == chars.length){ clearInterval(timer); State.typeOver = true; Element.$typeNext.show(); return; } if(chars[index] == '/'){ Element.$typeText.append('<br>'); //判斷換行位置 }else{ Element.$typeText.append(chars[index]); } index ++; }else{ delay ++ ; } },50); },
而後初始化新手引導:
initGuide : function(){ var self = this; var sex = 1, pokenum = '001', typeIndex = 0; Method.typing('歡迎來到精靈世界/我是大木博士',10); Element.$modal.removeClass('show'); //隱藏全部彈窗 Element.$modalGuide.addClass('show'); //顯示新手引導彈窗 Element.$body.addClass('blur'); //背景模糊 Element.$typeBox.bind('tap',function(){ //開始對話 if(!State.typeOver) return; typeIndex ++; switch (typeIndex) { case 1: Method.typing('這個世界處處都有精靈的存在/許多人把精靈當作夥伴',0); break; case 2: Method.typing('那麼你是男孩仍是女孩?/(選擇角色)',0); Element.$modalGuide.addClass('setrole'); //進入選擇角色界面 Element.$setRole.bind('tap',function(){ $(this).addClass('selected').siblings().removeClass('selected'); sex = $(this).data('sex'); }); break; case 3: Method.typing('選擇一隻精靈做爲你的夥伴吧/(選擇精靈)',0); Element.$modalGuide.removeClass('setrole').addClass('setpoke'); //進入選擇精靈界面 Element.$setPoke.bind('tap',function(){ $(this).addClass('selected').siblings().removeClass('selected'); pokenum = $(this).data('number'); Element.$setFigure[0].className = 'img-' + pokenum; }); Method.setItem('sex',sex); break; case 4: Method.typing('點擊周圍的小精靈便可抓捕!/(操做方式)',0); Element.$modalGuide.removeClass('setpoke'); break; case 5: Method.typing('移動可能遇到隨機出現的稀有精靈哦!/(隨機事件)',0); break; case 6: Method.typing('請帶上你的夥伴去冒險吧!',0); break; case 7: Element.$modalGuide.removeClass('show'); Element.$body.removeClass('blur'); self.ready(); break; default: } }); },
這個項目是去年6月份開始的,從零開始策劃、設計、到程序實現,斷斷續續寫了2個月,期間深度分析對比了幾個地圖廠商的開發文檔,推翻了數次方案,不斷對遊戲細節,性能,交互,功能取捨作優化,總結了LBS遊戲開發的一些經驗與建議:
一、文檔與社區活躍度很重要(這點高德地圖作的更好)
二、做爲練手,不依賴框架庫開發遊戲,更能鍛鍊邏輯能力,瞭解底層實現
三、做爲落地項目,輕量休閒遊戲開發仍然須要藉助成熟的遊戲開發框架(Phaser、Pixel),或Vue、React(大量狀態管理)
四、交互動畫部分儘量交給css3,js負責數據與狀態控制
五、用戶體驗、性能、遊戲性同等重要
最後再附上完整版:(完整版仍爲百度地圖開發,體驗相差不大)
have a fun!