PokemonGo:LBS遊戲開發

寫在前面

去吧!皮卡丘!
小時候擁有一臺任天堂是多少熊孩子的夢想,每一個夜晚被窩裏透出的微弱光線,把小小的童年帶入另外一個世界,家門口的鳥和狗,森林裏的蟲和瀑布,山洞裏的超音蝠,帶着小小的夢,走過一個個城市,一路冒險,飛天潛水,攀瀑碎巖,所向披靡。
每一個醒來的清晨,都恍如出門冒險的那天~css

要作什麼

基於開放地圖二次開發,完成簡易像素版PokemonGohtml

clipboard.png

準備工做

1、肯定功能需求

第一階段

一、用戶體系
二、揹包
三、圖鑑
四、人物定位
五、精靈分佈
六、精靈捕捉
七、排行榜
八、移動隨機事件
九、新手引導前端

第二階段

一、地圖增長道館挑戰
二、平常任務系統css3

第三階段

一、精靈交易
二、玩家對戰
三、AR捕捉場景web

*目前只完成第一階段的功能

2、開放地圖選擇

功能 / 廠商 百度地圖 騰訊地圖 高德地圖
自定義皮膚 支持 不支持 支持
實時定位 不支持 支持 支持
開發文檔 通常 友好

對比三個地圖廠商,咱們選擇高德地圖進行二次開發ajax

3、申請高德地圖SDK

登陸http://lbs.amap.com/
控制檯-應用管理-建立新應用-添加新KEY數據庫

4、接入微信受權

具體參考微信公衆平臺開發者文檔
https://mp.weixin.qq.com/wiki...api

5、服務端接口

咱們須要一些接口來保存用戶數據,因此須要找一個服務端的同窗配合完成幾個簡單的接口
一、api/login 判斷登陸狀態,獲取用戶基本信息
二、api/getGlassPokemon 獲取草地精靈
三、api/getMyPokemon 獲取揹包精靈
四、api/catchPokemon 捕捉精靈
五、api/getRank 獲取排行榜信息數組

6、素材準備

一、簡單設計主界面UI,肯定功能佈局、地圖的配色方案:微信

clipboard.png

二、準備150只精靈的素材圖片(大小各一套)

clipboard.png

clipboard.png

如今開始

1、接入高德地圖

<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],    //初始定位座標
});

查看DEMO

clipboard.png

2、地圖美化

默認的地圖樣式不能知足咱們的需求,高德地圖提供了地圖皮膚編輯器:高德地圖皮膚編輯器

clipboard.png

在編輯器中修改道路,陸地,建築,水域,綠地等顏色,同時在配置中隱藏了一些道路、建築與標記,簡化地圖。
編輯完成後點擊發布,得到地圖樣式ID:e6fa21422698f8a28585158d9d075f1d
在地圖初始化中引入地圖樣式便可

var gomap;
gomap = new AMap.Map('gomap', {
    zoomEnable : false,
    zoom:18,
    center: [118.18088, 24.4896],
    mapStyle : 'amap://styles/e6fa21422698f8a28585158d9d075f1d'
});

查看DEMO

clipboard.png

這樣看起來就有點遊戲的樣子了

3、地圖定位

咱們須要把地圖和主角定位在當前位置,而且在移動時實時更新定位,這就須要藉助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);      //返回定位出錯信息
})

clipboard.png

小智一我的站在地圖上有點孤單,咱們給他加一個光環放大的效果,看起來像是在發出檢測信號:

.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}
}

圖片描述

查看DEMO

4、羅盤

有了定位,咱們還須要知道本身移動的方向,方便接近目標,因此咱們在界面右上角放置了一個虛擬羅盤

clipboard.png

經過監聽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 (羅盤只在移動端生效,掃碼查看)

clipboard.png

5、精靈數據

因爲精靈的編號,屬性,星級等數據是固定的,在前端建立一個保存精靈圖鑑數據的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',
}

6、在地圖上添加精靈

主角誕生了,如今開始在周圍生成一些隨機的精靈,調用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",
        }),
    });
},

查看DEMO

clipboard.png

如今咱們就能在地圖上看到精靈了~

7、獲取揹包精靈

這一步咱們先把已捕捉到的精靈列表保存起來,以便後續使用:

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數組中
        }
    });
}

8、精靈收集

操做優化

精靈收集是整個遊戲的核心功能,原版pokemonGo精靈捕捉過程爲AR實景捕捉形式,咱們把精靈的捕捉形式簡化了,保留街機時代的像素風格。最先在實現這個功能時,採用的策略是當玩家座標與地圖精靈小於必定距離時,自動進入精靈捕捉場景,這種方式存在幾個問題:

  1. 用戶位置發生變化時,須要不斷計算用戶座標與地圖上全部精靈的距離,計算量較大
  2. 可能存在同時與兩個精靈距離符合捕捉條件,而一次只能捕捉一隻精靈
  3. 用戶若是不移動,基本很難捕捉到精靈

通過優化,將捕捉規則修改成:直接點擊地圖精靈便可捕捉,半徑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);
},

clipboard.png

初始化卡牌彈窗:

<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種狀態:精靈可捕捉,精靈已得到,精靈超出捕捉範圍:

clipboard.png

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();
    }
}

clipboard.png

clipboard.png

查看DEMO

9、精靈捕捉場景

首先爲卡片下方的捕捉按鈕綁定事件,將點擊的精靈數據傳遞到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;
},

clipboard.png

查看DEMO

10、去吧精靈球!

如今點擊丟出精靈球開始捕捉精靈,首先在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();
    });
},

捕捉失敗過程:
clipboard.png

捕捉成功過程:
clipboard.png

查看DEMO

11、大木博士

還原經典,咱們加入一個簡單的新手引導,經過與大木博士的對話,肯定玩家性別,第一個夥伴,和簡單的遊戲玩法介紹。

clipboard.png

首先實現一個簡單的打字效果:

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:
        }
    });
},

查看DEMO

12、完善細節

頁面加載

clipboard.png

精靈刷新倒計時

clipboard.png

系統公告

clipboard.png

Toast提示

clipboard.png

訓練師信息

clipboard.png

精靈揹包

clipboard.png

精靈圖鑑

clipboard.png

精靈大師榜

clipboard.png

總結

這個項目是去年6月份開始的,從零開始策劃、設計、到程序實現,斷斷續續寫了2個月,期間深度分析對比了幾個地圖廠商的開發文檔,推翻了數次方案,不斷對遊戲細節,性能,交互,功能取捨作優化,總結了LBS遊戲開發的一些經驗與建議:
一、文檔與社區活躍度很重要(這點高德地圖作的更好)
二、做爲練手,不依賴框架庫開發遊戲,更能鍛鍊邏輯能力,瞭解底層實現
三、做爲落地項目,輕量休閒遊戲開發仍然須要藉助成熟的遊戲開發框架(Phaser、Pixel),或Vue、React(大量狀態管理)
四、交互動畫部分儘量交給css3,js負責數據與狀態控制
五、用戶體驗、性能、遊戲性同等重要

最後再附上完整版:(完整版仍爲百度地圖開發,體驗相差不大)
clipboard.png

have a fun!

相關文章
相關標籤/搜索