微信小游戏从立项到上线!谈谈《猎头专家》的开发历程

三个月了,终于有时间坐下来写一点东西了。

在这个系列里,我将制作《猎头专家》的过程进行梳理,作为射手座团队开发第一款小游戏的小结。希望对小游戏行业的朋友们有用。

我会详细阐述《猎头专家》这个产品从立项到技术选型的过程,在开发过程中遇到的有代表性的问题以及我们的解决方案,欢迎大家批评指正。

关于猎头专家

直接看游戏视频最能说明问题了:

这是一款极具趣味性的射击游戏!凭借精湛的技能即可获得源源不断的奖赏!

加入战斗, 扮演弓箭手,用你的弓箭征服你的好友,征服世界各地的玩家,成为最强的弓箭手!!

深吸一口气,瞄准目标,发出你的箭,争取一击爆头!!!

你会是最好的弓箭手吗?还在等什么,快来玩玩看吧~~~

提纲

蓝色的部分代表已经完成,点击下面的链接可以直接进入对应部分阅读。

猎头专家文章目录

因为《猎头专家》的开发还在进行当中,团队各位成员也在长期加班加点,你们也知道写文档是技术人员最不愿意做的事情啦…… 所以,这个系列可能会更新比较慢。但我保证,慢是慢点,坚决不太监!

关于射手座团队

射手座团队

射手座团队坐标武汉,由一群游戏爱好者组成的。团队中有游戏行业的老司机,持续游戏创业者,中老年程序员,当然也少不了朝气蓬勃的小姐姐。我们希望做出让自己开心,让朋友和家人都爱玩的精品游戏。

我的博客可能会更新快一些,所以呢……

关于猎头专家

这部分我就偷懒直接上宣传语好了:

这是一款极具趣味性的射击游戏!凭借精湛的技能即可获得源源不断的奖赏!

加入战斗, 扮演弓箭手,用你的弓箭征服你的好友,征服世界各地的玩家,成为最强的弓箭手!!

深吸一口气,瞄准目标,发出你的箭,争取一击爆头!!!

你会是最好的弓箭手吗?还在等什么,快来玩玩看吧~~~

下面当然就是厚颜无耻的广告了

大家可以来体验下哈,现在进游戏还有红包可以送呢。

猎头专家二维码

22赞

为什么要做猎头专家

《猎头专家》对标的游戏是 Bowmasters ,e这款游戏的打击感和操作都很不错,评分相当高。但它是一款单机功能为主的休闲游戏,联网对战功能很弱,画面太血腥,不适合年龄较小的玩家和女性玩家。

射手座团队认为,血腥的画面的确给人更强的力量感,但这是一个比较小众的需求,也不符合国内用户的习惯。我们可以把游戏做得足够有趣——在战斗终结技中加入大家耳熟能详的梗,让人物的动作更丰富,让武器的技能更出人意料……这是一个更大的发展方向,能够吸引到更加广泛的玩家。

然而,微信玩家们对终结技似乎并不买账。这点以后再说。

与现在充斥微信小游戏平台上的大部分小游戏不同,在《猎头专家》中,我们瞄准的主要玩法是在线对战。这是猎头专家第一个版本就包含的功能,也是游戏最核心,最主要的功能。这让《猎头专家》的开发过程变得不那么简单。玩家习惯和在线人数都会影响到对战游戏的表现。射手座团队认为,和真实的玩家对战,可以促进玩家真正主动地分享游戏。这是《猎头专家e》用户增长的个重要途径之一。

从5月9日发布 1.0 版本以来,《猎头专家》已经发布了 27 个版本,平均每周 3~4 个版本,新增游戏角色 21 个。射手座团队正在用自己特有的速度,让《猎头专家》这款对战游戏变得更加丰满和完善。

技术选型

《猎头专家》一出现就面向微信小游戏平台,因此,技术上我们无法选择更成熟的 Unity ,而是限制在 HTML5 平台。对于国内 HTML5 的“三大”引擎 Cocos/Egret/Layabox 的“终极选择”,我们并没有纠结过多。虽然深入用过其中两款,但 Cocos 对于团队来说还是更熟悉一些。 我们的不少开发者都经历过 cocos2d-x 2~3 的阵痛,踩过 CocosStudio 这个大坑,因此对 CocosCreator(CCC) 挺有好感,我们也希望这款编辑器能越来越好。想让一款工具变好的最有力的方法就是: 选择它,使用它。

服务器方面,为了方便开发者进行全栈开发,我们选择了网易的 pomelo 这款基于 node.js 的服务器开发框架。在开发《猎头专家》之前,射手座团队已经使用过该框架半年以上。

事实证明,选择团队熟悉的工具,对团队开发速度影响很大。CocosCreator 支持 TypeScript 对多人合作有好处,在 node.js 中大量使用 ES6/7 也能提升开发者的幸福感。射手座的开发者全部以全栈方式工作,这也进一步提升了功能模块的开发质量和进度。

角色方面,我们使用了 Spine 这个成熟的 2D 骨骼动画制作工具。CCC 对 Spine 的封装还不是很完善,我们对其做了一些修改。

对于系统工具,我们大量使用了 FabricJenkins 来实现自动部署,微信小游戏的发布、打包,游戏服务器的部署都是一键自动完成的。

顺便说一句,CocosCreator 支持命令行打包是很好的功能,但如果能支持 Linux 就更好了(最好能把打包工具从 CocosCreator 中剥离出来,作为一个独立的工具发布),这样我们就不需要拿一台 Mac 做打包机了,真浪费……

微信小游戏引擎的性能问题

这点真的要拿出来说一下的,微信小游戏引擎的性能 !真!的!很!差!劲!

《猎头专家》的开发调试均在 Chrome 浏览器中进行,测试阶段也会在手机浏览器和微信浏览器中进行测试。在原生的 HTML5 环境下,游戏运行非常流畅,但在小游戏环境下,游戏非常卡顿。这导致我们不得不减少特效的使用,简化物理碰撞盒,同时对计算进行优化。

例如,我们在 6 月上旬推出的游戏角色 “龙娘” ,她的武器龙蛋可以爆炸造成溅射伤害,地图上会留下火焰粒子效果。这个角色在测试中表现的效果非常惊艳,但上了小游戏引擎后却出现了卡顿噩梦。我们不得不减少火焰效果的表现以及粒子数量才能让帧率勉强回升到 40 FPS以上。

经过测试,我们发现小游戏引擎的渲染性能尚可,但 Javascript 引擎的执行效率有很大的问题。因此,诸如物理碰撞以及粒子之类需要大量计算的场合,就会拖慢帧率。

开发者只能通过降低效果来改变自己的游戏在微信小游戏引擎中的表现,或者等待微信小游戏团队的更新。看着我的骁龙 835 也这么卡,真的让人很沮丧。

8赞

:grinning:不错啊

《猎头专家》动态资源加载与释放技巧

本文作者: 射手座团队 阿森

查看更完整的文章可以去 我的博客

游戏总杜绝不了 BUG,每当出现一个 BUG 时,程序猿们总是需要搅破脑汁去解决,这是一件很不愉快的事情。但如果 BUG 的出现暴露出了更严重的其它问题时,也算是不幸中的万幸了。我们在《猎头专家》的运营过程中就遇到过这么一个事。

《猎头专家》中有一个 无限模式 的玩法,可以用有限的复活次数不停挑战难度越来越高的电脑关卡,技术好的玩家一般能打到十多层。但某次更新版本后,我们发现有个玩家居然挑战到了 23 层,而且复活次数为 0 ,我们整个团队都震惊了…… 为了研究玩家如何做到的,我们也进入无限模式不断尝试,结果发现这种奇异的现象居然是由于一个 BUG 导致的!!!

出现 BUG 的原因是,上次版本更新导致复活次数没有正确累计,玩家可无限复活并不消耗复活次数。但当我们达到 无限模式 30 层希望更进一步的时候,游戏崩溃了!!!鉴于无限模式不停切换地图与角色的特点,我们把问题的矛头指向了内存泄露。原来那个达到 23 层的玩家之所以没有打得更高,是因为手机比我们的差啊……

我们使用的一直是 CCC 推荐的释放资源的方法,在切换地图与人物后,将“所有未用到的已加载资源”通过 cc.loader.release 方法释放掉。但在深入了解、不停地观测 cc.loader._cache 中所有加载的资源条目之后,我们发现手动释放掉的资源只是冰山一角————大量依赖资源并未被同时释放,而这些资源占用了较多的内存。我们先尝试了一个简单的解决方案:使用接口 cc.loader.getDependsRecursively 获取到资源的依赖关系并将其释放,但这个方案并不好用。因为某些资源间的互相引用,极有可能导致资源错误释放,出现更严重的问题。为此,我们设计了一个动态资源管理模块,让单个功能模块或界面或场景,能够更简单的管理自己所使用的资源,如下:

// 加载资源:
this.loader.load(path, sp.SkeletonData, (skeletonDatas) => {
  // xxx
})
// 释放资源
this.loader.release()

Cocos Creator资源加载特性

在介绍动态资源管理模块之前,我们先需要了解一下 Cocos Creator 的资源加载机制与其特性。依据官方提供的相关文档 获取和加载资源 与简单的 DEMO 测试,我们对 Cocos Creator 的资源加载特性做一个简单的总结:

  1. 资源动态加载都是异步的。在处理资源加载与释放时,就需要考虑加载中的资源如何释放的问题。
  2. 资源是互相依赖的,指定加载资源时,也会加载其所有依赖项。在处理资源释放时,需要考虑资源的依赖关系。
  3. 动态加载的资源都是不会自动释放的。就算切换场景,动态加载的资源依然需要手动释放。
  4. cc.loader.getDependsRecursively 接口可以获取到资源的所有依赖项,包括依赖的依赖。这个接口可以让我们方便地获取到资源的依赖关系。
  5. cc.loader.getDependsRecursively 接口获取到的数据,是每个资源对应的唯一的 reference id ,该值可以通过cc.loader的私有方法 _getReferenceKey 获取。释放时使用 cc.loader.release 直接传入资源的 reference id 进行释放。
  6. 按路径加载或释放资源时,需要指定目标资源的类型(简单的配置文件除外)。释放资源时,如果该路径下有多种资源类型(比如 spine 动画相关文件有 json/png/atlas ),你将不知道它会释放什么资源,而且释放也不完全(只会释放其中一种资源)。
  7. 类似于上一条,在使用 cc.loader.getDependsRecursively 接口获取依赖项时,不要使用文件路径作为参数获取。

《猎头专家》资源概况

在《猎头专家》小游戏中,除去开启页资源外,其它所有资源均为动态加载,即资源文件绝大部分都在 resources 文件夹中存放。全局只有一个主场景,游戏过程中没有场景的切换。这种结构既有优势也有劣势:

优势:

  • 全局脚本的挂载更容易
  • 小游戏初包尽量小
  • 不会出现自动加载资源与动态加载资源间的互相依赖问题(这个会加大游戏过程中的资源管理难度)

劣势:

  • 必须有完善的释放机制
  • 在适合的时候需要释放未使用的资源来减轻内存压力

为了满足《猎头专家》资源管理的诉求,我们设计了一套资源管理模块,仅供大家参考。

资源管理模块设计及实现

资源单项加载与释放

资源加载时需要指定资源类型(配置文件不需要),于是在加载资源时,我们选择了资源分类加载。而为了方便资源释放,在资源加载完成后,需要记录所有加载到的资源,包括其依赖项。

因此,我们设计了一个专用于单个资源加载的类 LoaderItem ,有以下几个主要属性:

class LoaderItem {
  isReleased: boolean      = false      // 是否已被释放
  urls: string[]           = null       // 加载项列表
  type: typeof cc.Asset    = null       // 加载资源类型
  resources: Object        = null       // 所有使用资源的reference id
  maxRetryTimes: number    = 0          // 最大重试次数
}

在资源加载完成时,记录 LoaderItem 对象所有使用到的资源(包括自身),具体实现如下:

type SUCCESS_CALL  = (res:any[])=>void
type FAILED_CALL   = (err:Error)=>void
type ERROR_CALL    = (error:string)=>void
type PROGRESS_CALL = (completedCount: number, totalCount: number, item: any) => void

/**
 * 缓存已使用资源
 * @param resource   缓存单个资源的所有使用资源
 */
private _cacheRes (resource: any) {
  let loader: any = cc.loader
  this.resources[loader._getReferenceKey(resource)] = true
  for (let key of loader.getDependsRecursively(resource)) {
    this.resources[key] = true
  }
}

/**
 * 开始加载资源
 * @param successCall    加载成功回调
 * @param failedCall     加载失败回调
 * @param progressCall   加载进度回调
 */
load (successCall: SUCCESS_CALL, failedCall:FAILED_CALL, progressCall:PROGRESS_CALL) {
  let completedCallFunc = (error: Error, resources: any[])=>{
    if (!error) {
      for (let res of resources) {
        this._cacheRes(res, errorCall)
      }
      successCall && successCall(resources)
    } else {
      if (this.maxRetryTimes === this._currentRetryTimes) {
        failedCall && failedCall(error)
      } else {
        this._currentRetryTimes += 1
        return this.load(successCall, failedCall, errorCall, progressCall)
      }
    }
  }
  let callFuncArgs: any[] = [this.urls]
  this.type && callFuncArgs.push(this.type)
  progressCall && callFuncArgs.push(progressCall)
  callFuncArgs.push(completedCallFunc)
  cc.loader.loadResArray.apply(cc.loader, callFuncArgs)
}

由于在加载完成后我们记录了全部资源,释放时的资源处理就会非常简单直接:

/**
 * 释放资源
 */
release () {
  this.isReleased = true
  let resources: string[] = Object.keys(this.resources)
  cc.loader.release(resources)
  this.resources = {}
}

/**
 * 释放资源
 * @param otherDepends  其它依赖项,释放资源会跳过这些资源
 */
releaseWithout (otherDepends: Object) {
  for (let reference in this.resources) {
    if (otherDepends[reference]) {
      delete this.resources[reference]
    }
  }
  this.release()
}

模块的资源管理类

功能模块使用的资源不可能全都是单一类型,而且模块与模块之间,资源加载对象之间也有着可能的重合的使用资源。当某个模块资源需要释放时,其它模块引用的资源需要确保不被释放。因此,需要一个资源加载与释放的管理者,来告知 LoadItem 在释放时需要过滤的资源。

鉴于模块或界面之间的树状关系结构,管理者也需要设计成树状结构,即有一个根管理者及其派生管理者,而每一个节点上的资源在被释放时,都需要考虑其它所有节点所使用到的资源。管理者对象结构如下:

class Loader {
  private _parentLoader: Loader = null
  private _subLoaders: Loader[] = null
  private _loadItems: LoaderItem[] = null
  private _released: boolean  = false
}

我们需要一个根管理器来加载通用资源,这些资源将不会被其它管理器释放。同时,所有其它管理器应当是根管理器的子节点,这样才契合 Cocos 节点的树状关系,在释放时可以方便的获得到其它模块使用到的所有资源。

/**
 * 获取到根管理器
 */
get rootLoader (): Loader {
  let root: Loader = this
  while (root._parentLoader) {
    root = root._parentLoader
  }
  return root
}

/**
 * 创建子管理器
 */
createSubLoader (): Loader {
  let loader = new Loader()
  loader._parentLoader = this
  this._subLoaders.push(loader)
  return loader
}

/**
 * 移除子管理器
 * @param loader  需移除的子管理器
 */
private _removeSubLoader (loader:Loader) {
  let index: number = this._subLoaders.indexOf(loader)
  if (index >= 0) {
    this._subLoaders.splice(index, 1)
  }
}

加载时,管理器需将所有的加载项记录下来;释放时,直接释放这些加载项。需要注意的是:

  1. 由于资源加载是异步的,资源加载完成时可能该加载项已被释放,此时需要单独处理释放逻辑。
  2. 释放需要在下一个 Tick 进行。因为同一时刻,同一个文件可能有多个加载项在加载,LoaderItem 对象中还未对已引用的资源作记录,直接释放可能会错误释放掉其它资源的依赖项。
/**
 * 
 * @param urls            加载资源项
 * @param type            加载资源类型
 * @param succCall        加载成功回调
 * @param failCall        加载失败回调
 * @param retryTimes      重试次数
 * @param progressCall    加载进度回调
 */
load (urls: string[]|string, type:typeof cc.Asset, succCall: SUCCESS_CALL = null, failCall: FAILED_CALL = null, retryTimes:number = 0, progressCall:PROGRESS_CALL = null) {
  let item: LoaderItem = new LoaderItem(urls, type, retryTimes)
  item.load((res:any[])=>{
    if (this._released|| item.isReleased) {
      // 释放刚加载的资源,需在下一Tick释放,保证其它加载成功
      return callInNextTick (()=>{
        item.releaseWithout(this.rootLoader.getAllResources())
      })
    }
    return succCall && succCall(res)
  }, (error:Error)=>{
    if (this._released) return
    failCall && failCall(error)
  }, progressCall)
  this._loadItems.push(item)
}

/**
 * 释放管理器
 */
release () {
  this._released = true
  this._parentLoader._removeSubLoader(this)
  // 释放当前加载的所有资源,需在当前Tick释放,以让后续的加载请求生效
  let allResouces: Object = this.rootLoader.getAllResources()
  this._releaseWithout(allResouces)
}

/**
 * 选择性释放资源
 * @param allResouces   不能被释放的资源
 */
private _releaseWithout (allResouces: Object = null) {
  for (let item of this._loadItems) {
    item.releaseWithout(allResouces)
  }
  this._loadItems.length = 0

  for (let loader of this._subLoaders) {
    loader._releaseWithout(allResouces)
  }
}

如何使用资源管理类

在使用资源管理类 Loader 时,我们需要一个根管理器来加载所有无需动态释放的公共资源。根管理器可以直接使用 new Loader 来创建,全局或场景唯一。其它子模块的资源管理器,需要通过根管理器的 createSubLoader 来创建。这样就建立了一个全局唯一的资源管理树。我们可以很方便的获取到当前正在使用的所有资源,也可以针对某个节点的资源进行定点释放。

例如,我们需要对某个界面中所有加载的 Spine 骨骼动画资源进行管理,在显示动画时加载资源,在界面销毁时移除资源,只需要按如下方式进行加载和释放:

onLoad () {
  // app.mainLoader为根管理器
  this._actorLoader = app.mainLoader.createSubLoader()
}

showSpine (path) {
  this._actorLoader.load(path, sp.SkeletonData, (skeletonDatas) => {
    let skeletonData = skeletonDatas[0]
    if (this && this.node && cc.isValid(this.node) && skeletonData) {
      this.spine.skeletonData = skeletonData
      this.spine.setAnimation(0, 'idle', true)
    }
  }, null, -1)
}

onDestroy () {
  this._actorLoader.release()
}

需要注意的是,资源加载在节点创建之前。在需要对该节点资源(包括节点自身)进行管理时, Loader 需要提前创建并用来加载节点预制件。为了方便节点在销毁时自动释放相关资源,我们增加了 LoaderKeeper 组件,动态加到节点上并将其 Loader 记录下来,在销毁时释放资源:

@ccclass
export default class LoaderKeeper extends cc.Component {
  private _loader: Loader = null

  get loader ():Loader {
    return this._loader
  }

  init (loader: Loader) {
    this._loader = loader
    return this
  }

  onDestroy () {
    if (this._loader) {
      this._loader.release()
      this._loader = null
    }
  }
}

完整的使用方式如下:

let loader: Loader = app.mainLoader.createSubLoader()
loader.load(prefabPath, cc.Prefab, (prefabs) => {
  let prefab: cc.Prefab = prefabs[0]
  let node = cc.instantiate(prefab)
  app.canvas.addChild(node)
  node.addComponent(LoaderKeeper).init(loader)
}, (err:Error)=>{
  loader.release()
})

这套资源加载机制可以依据项目的需求进行灵活调整,Loader/LoaderKeeper/LoaderItem 联合使用能有效地按需 加载/释放 游戏中所使用到的所有动态资源。

结语

由于《猎头专家》的资源结构特点,资源管理在设计时并未考虑自动加载资源与动态加载资源之间互相依赖时,资源释放的过程与特性。在此特别提醒,如果项目中有这种使用方式时,请自行研究以免出现资源错误释放的问题。此文主要是通过提供一种管理器的设计思路来介绍 Cocos Creator 的资源加载与释放机制,与各位交流学习。

欢迎留言讨论,欢迎吐槽拍砖。

9赞

#《猎头专家》地形和背景实现

本文作者:射手座团队 蟹老板

查看更完整的文章可以去 我的博客

在《猎头专家》里,站得高不一定是好事,掉坑里也不一定是坏事,大家应该都体会到了,地形的重要性不言而喻,今天就来聊聊地形在《猎头专家》里的实现。

设计目标

  • 实现容易(简单)
  • 生成灵活(新奇)
  • 难度可控(又好玩)

设计思路

地形在游戏的战斗场景中长度固定且需要考虑画面外的碰撞,所以我们的地形没有使用渐进式生,而是在战斗场景初始化时就全部生成完成,在线对战时,会预先生成地形数据发送给双方。

地形的组成

对于 size 这个单位,1 代表 128x128 像素的矩形贴图。

  1. **平路:**type=0,size:1x1,插件:RigidBody, PhysicsBoxCollider
  2. **小坡:**type=1,size:3x2,插件:RigidBody, PhysicsPolygonCollider 红色的圆点是顶点,组成多边形碰撞盒,顶点越多越平滑,当然性能也会受影响
  3. **大坡:**type=2,size:4x3,插件:RigidBody, PhysicsPolygonCollider 红色的圆点是顶点,组成多边形碰撞盒,顶点越多越平滑,当然性能也会受影响

我们游戏中的地形仅使用了Box2D自带的插件,没有使用第三方插件,小坡与大坡的原理是一样的,主要是视觉上的差别,如果你愿意可以做出更复杂的地形。

坡路的设计思路

  1. 坡的角度为45度,这样保证了物体滑落时的感觉比较自然。
  2. 坡底和坡顶拐角处都包含了一块平路并且设置了2个顶点,确保物体滑落时的平滑感。
  3. 坡度和坡顶都是平路可以和”平路“地形无缝连接。

地形数据格式

从下面的配置文件片段中可以看出,如果type为负值,则表示翻转,那么 上坡 素材就会变成 下坡 素材。

[
  {"type": 0, "x": 0, "y": 0},
  {"type": 0, "x": 28, "y": 0},
  {"type": 0, "x": 256, "y": 0},
  {"type": 1, "x": 384, "y": 0},
  {"type": 0, "x": 768, "y": 128},
  {"type": -1, "x": 896, "y": 0}
]

地形的控制

地形组件有一下几个参数:

  1. 战场宽度:目前是一个固定宽度8000px
  2. 最大高度:地形顶部可触及高度
  3. 最小高度:地形底部最低高度
  4. 最大连续平路:连续平路的最大数量
  5. 最小连续平路:连续平路的最小数量

参数2、3控制地形的最大落差,落差越大越可能出现大坑。

参数4、5控制平路的最小与最大长度,平路少,则容易掉沟里,有利有弊吧,这两组参数决定了地形的难度,坡路地形会根据高度的变化自动翻转,实现上坡和下坡的无缝连接。

地形的美化

刚才提到的地形生成,仅仅填充了角色脚下的那一排地面。还有很多空白区块需要填充,我们用一个 size 1x1 的地形同色纹理来补充,并随机补上一些装饰用的纹理。

不管是地形组件还是地形纹理,都按颜色风格分组为多个plist,在战场生成时,随机调用。

最后附上战场地形实现示意图

初始地图:没有经过纹理填充时

美化过的地图

为优化填充性能,填充是按自上而下找出整块区域使用平铺的方式填充,以节省Sprite的使用数量。

gound.ts 部分代码实现

@ccclass
export default class Ground extends cc.Component {
  // 纹理的 plist
  atlas: cc.SpriteAtlas = null
  // 纹理组件
  tile: cc.Prefab = null
  // 平路组件
  flat: cc.Prefab = null
  // 小坡组件
  ramp1: cc.Prefab = null
  // 大坡组件
  ramp2: cc.Prefab = null

  @property(cc.Integer)
  sceneWidth: number = 8000
  @property(cc.Integer)
  maxHeight: number = 5
  @property(cc.Integer)
  minHeight: number = -2
  @property(cc.Integer)
  maxStraightParts: number = 4
  @property(cc.Integer)
  minStraightParts: number = 2
  // 起始方向:1为朝右(正),-1为朝左(反)
  @property(cc.Integer)
  direction: number = 1
  // 起始高度(台阶)
  @property(cc.Integer)
  stairs: number = 0

  // 每个体积单位的实际像素
  sizeUnit = 128

  onLoad () {
    // 地形使用了缓存类管理(内部使用的是cc.NodePool)
    this._nodeCacheManager = new NodeCacheManager()
  }

  // 设置纹理主题(配合背景),之后动态更换组件的 spriteFrame
  setAtlas (atlas) {
    this.atlas = atlas
  }

  // 根据server发来的地形数据动态生成
  onSyncGround (groundData: any[]) {
    for (let i = 0; i < groundData.length; i++) {
      // 取出每一块的数据
      let prefabData = groundData[i]
      // 根据地形类型取出组件并实例化到界面上
      let prefab = this._getPrefab(prefabData.type)
      let node = cc.instantiate(prefab).addTo(this.panel, -1)
      node.getComponent(cc.Sprite).spriteFrame = this.atlas.getSpriteFrame(prefabData.type)
      // 坡路可能需要翻转
      if (prefab.size.h > 0) {
        node.scaleX = prefabData.type > 0 ? 1 : -1
      }
      node.x = prefabData.x
      node.y = prefabData.y

      // 每铺一块地形,计算出下块地形坐标
      this.startX += prefab.width * this.sizeUnit
      // 如果是反向,则坡路由于锚点问题会造成偏移,所以此处修正一下
      if (this.direction < 0 && prefab.size.h > 0) {
        node.x = this.startX
      }
      // 根据当前地形坐标及大小创建它下面的装饰纹理
      this.createTiles(prefab.size.w, prefab.size.h, node.x, node.y)
      // 超过屏幕宽度后退出
      if (this.startX > this.sceneWidth) {
        break
      }
    }
    // 地形创建完成后清除缓存
    this._nodeCacheManager.clear()
  }

  // 平铺空白处
  createTiles (w, h, x, y) {
    // 在每块地形底部向下铺一块纹理(不超过minHeight)
    // ...
    // 随机创建其它风格样式砖块
    // ...
  }

  // 以地形最底部的y点(即 minHeight 之下)算出最大平铺纹理区域
  private _createMainTile (y: number = 0) {
    let stails: number = Math.floor(y / 128)
    let mainTile = this._nodeCacheManager.createNode(this.tile)
    let sprite = mainTile.getComponent(cc.Sprite)
    sprite.spriteFrame = this.atlas.getSpriteFrame('tile')
    let height: number = (5 + stails) * this.sizeUnit
    mainTile.setContentSize(cc.size(this.sceneWidth, height))
    mainTile.x = 0
    mainTile.y = y + this.minHeight * this.sizeUnit - height
    this.panel.addChild(mainTile, -2)
  }
}
4赞

楼上的不好意思,还是我在占楼 :innocent:

这真的是最后一个楼了呢!

好吧,先占这么多吧

做的不错,和之前有款单机游戏很像

1赞

玩过类似的游戏:grin:

1赞

这个是小游玩家上面推荐的吗?不错不错

1赞

手感不错啊,赞一个!

Facebook上玩过同类型的,总感觉楼主这个镜头移动的时候不够平滑,有些卡卡的感觉,还有对战全部是真人吗?为什么我这边有辅助线?新玩家前面几次有?

我们马上要上FB了

镜头卡的问题是微信的锅啊。微信引擎的性能低于标准的H5非常多。我们不得不减掉许多特效才能保证在微信上的相对流畅。

在Facebook和H5上,你能感到丝般柔顺……

希望能分享点开发经验。

基本上分享得都是开发经验。正在努力写。

你的设备和微信版本能告知一下么?

新玩家前三场战斗会给辅助线,四至六场战斗会给上次力度和角度提示。

更新产品立项内容(为什么做猎头专家,技术选型)

http://forum.cocos.com/t/topic/62521/2

感同身受

不暴力不血腥啊,Bowmasters 里面最精华的部分不见了~~