摘要:优化后的方案降低了捕捉门槛,也鼓励用户走动去发现和捕捉更多精灵。
写在前面
去吧!皮卡丘!
小时候拥有一台任天堂是多少熊孩子的梦想,每个夜晚被窝里透出的微弱光线,把小小的童年带入另一个世界,家门口的鸟和狗,森林里的虫和瀑布,山洞里的超音蝠,带着小小的梦,走过一个个城市,一路冒险,飞天潜水,攀瀑碎岩,所向披靡。
每个醒来的清晨,都恍如出门冒险的那天~
要做什么
基于开放地图二次开发,完成简易像素版PokemonGo
准备工作 一、确定功能需求 第一阶段1、用户体系
2、背包
3、图鉴
4、人物定位
5、精灵分布
6、精灵捕捉
7、排行榜
8、移动随机事件
9、新手引导
1、地图增加道馆挑战
2、日常任务系统
1、精灵交易
2、玩家对战
3、AR捕捉场景
*目前只完成第一阶段的功能
二、开放地图选择
功能 / 厂商 | 百度地图 | 腾讯地图 | 高德地图 |
---|---|---|---|
自定义皮肤 | 支持 | 不支持 | 支持 |
实时定位 | 不支持 | 支持 | 支持 |
开发文档 | 差 | 一般 | 友好 |
对比三个地图厂商,我们选择高德地图进行二次开发
三、申请高德地图SDK登录http://lbs.amap.com/
控制台-应用管理-创建新应用-添加新KEY
具体参考微信公众平台开发者文档
https://mp.weixin.qq.com/wiki...
我们需要一些接口来保存用户数据,所以需要找一个服务端的同学配合完成几个简单的接口
1、api/login 判断登录状态,获取用户基本信息
2、api/getGlassPokemon 获取草地精灵
3、api/getMyPokemon 获取背包精灵
4、api/catchPokemon 捕捉精灵
5、api/getRank 获取排行榜信息
1、简单设计主界面UI,确定功能布局、地图的配色方案:
2、准备150只精灵的素材图片(大小各一套)
现在开始 一、接入高德地图在中引入高德地图js-sdk
二、地图美化
默认的地图样式不能满足我们的需求,高德地图提供了地图皮肤编辑器:高德地图皮肤编辑器
在编辑器中修改道路,陆地,建筑,水域,绿地等颜色,同时在配置中隐藏了一些道路、建筑与标记,简化地图。
编辑完成后点击发布,获得地图样式ID:e6fa21422698f8a28585158d9d075f1d
在地图初始化中引入地图样式即可
var gomap;
gomap = new AMap.Map("gomap", {
zoomEnable : false,
zoom:18,
center: [118.18088, 24.4896],
mapStyle : "amap://styles/e6fa21422698f8a28585158d9d075f1d"
});
查看DEMO
这样看起来就有点游戏的样子了
三、地图定位
我们需要把地图和主角定位在当前位置,并且在移动时实时更新定位,这就需要借助AMap的geolocation插件
gomap.plugin("AMap.Geolocation",function(){
var geo = new AMap.Geolocation({
showButton: false,
showCircle: false,
showMarker : true, //显示定位图标
markerOptions : {
content : "", //设置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}
}
查看DEMO
四、罗盘
有了定位,我们还需要知道自己移动的方向,方便接近目标,所以我们在界面右上角放置了一个虚拟罗盘
通过监听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",
}),
});
},
查看DEMO
现在我们就能在地图上看到精灵了~
七、获取背包精灵
这一步我们先把已捕捉到的精灵列表保存起来,以便后续使用:
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);
},
初始化卡牌弹窗:
TIPS 捕捉半径500米 当前距离0米
捕捉
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 += ""+ data.properties[i] +"";
}
$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();
}
}
查看DEMO
九、精灵捕捉场景
首先为卡片下方的捕捉按钮绑定事件,将点击的精灵数据传递到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;
},
查看DEMO
十、去吧精灵球!
现在点击丢出精灵球开始捕捉精灵,首先在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();
});
},
捕捉失败过程:
捕捉成功过程:
查看DEMO
十一、大木博士
还原经典,我们加入一个简单的新手引导,通过与大木博士的对话,确定玩家性别,第一个伙伴,和简单的游戏玩法介绍。
首先实现一个简单的打字效果:
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("
"); //判断换行位置
}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
十二、完善细节
页面加载
精灵刷新倒计时
系统公告
Toast提示
训练师信息
精灵背包
精灵图鉴
精灵大师榜
总结
这个项目是去年6月份开始的,从零开始策划、设计、到程序实现,断断续续写了2个月,期间深度分析对比了几个地图厂商的开发文档,推翻了数次方案,不断对游戏细节,性能,交互,功能取舍做优化,总结了LBS游戏开发的一些经验与建议:
1、文档与社区活跃度很重要(这点高德地图做的更好)
2、作为练手,不依赖框架库开发游戏,更能锻炼逻辑能力,了解底层实现
3、作为落地项目,轻量休闲游戏开发仍然需要借助成熟的游戏开发框架(Phaser、Pixel),或Vue、React(大量状态管理)
4、交互动画部分尽可能交给css3,js负责数据与状态控制
5、用户体验、性能、游戏性同等重要
最后再附上完整版:(完整版仍为百度地图开发,体验相差不大)
have a fun!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/51141.html
摘要:优化后的方案降低了捕捉门槛,也鼓励用户走动去发现和捕捉更多精灵。 写在前面 去吧!皮卡丘!小时候拥有一台任天堂是多少熊孩子的梦想,每个夜晚被窝里透出的微弱光线,把小小的童年带入另一个世界,家门口的鸟和狗,森林里的虫和瀑布,山洞里的超音蝠,带着小小的梦,走过一个个城市,一路冒险,飞天潜水,攀瀑碎岩,所向披靡。每个醒来的清晨,都恍如出门冒险的那天~ 要做什么 基于开放地图二次开发,完成简易...
摘要:优化后的方案降低了捕捉门槛,也鼓励用户走动去发现和捕捉更多精灵。 写在前面 去吧!皮卡丘!小时候拥有一台任天堂是多少熊孩子的梦想,每个夜晚被窝里透出的微弱光线,把小小的童年带入另一个世界,家门口的鸟和狗,森林里的虫和瀑布,山洞里的超音蝠,带着小小的梦,走过一个个城市,一路冒险,飞天潜水,攀瀑碎岩,所向披靡。每个醒来的清晨,都恍如出门冒险的那天~ 要做什么 基于开放地图二次开发,完成简易...
摘要:前端日报精选理解的专题之偏函数译理解事件驱动机制游戏开发前端面试中的常见的算法问题发布中文前端页面传参尚妆产品技术刊读基础系列二之实现大转盘抽奖掘金指南众成翻译编程插入排序众成翻译源码讲解函数技术风暴初体验个人文 2017-08-16 前端日报 精选 理解 JavaScript 的 async/awaitJavaScript专题之偏函数[译]理解 Node.js 事件驱动机制Pokem...
摘要:几个月之内就开发了几十款小程序的开发者陈林,有着自己的解答。资深小程序开发者陈林,是小程序风口下的探索者之一,借助小程序无需安装,易于传播的特点,陈林以小游戏类目为核心,配合小程序间可相互跳转的特性。 showImg(https://segmentfault.com/img/remote/1460000020165110);你是一条产品经理,现在要出一个大型需求的方案,你会怎么做? ...
阅读 1294·2021-11-24 11:16
阅读 3509·2021-11-15 11:38
阅读 2045·2021-10-20 13:47
阅读 682·2021-09-29 09:35
阅读 2279·2021-09-22 15:17
阅读 1104·2021-09-07 09:59
阅读 3460·2019-08-30 13:21
阅读 2974·2019-08-30 12:47