从小游戏到APP:探索单挑篮球进阶之路的完整历程

2024-12-09 0 823

在游戏开发界,PVP对战和相关系统的构建往往面临诸多挑战,同时也孕育着创新。这其中蕴含着许多值得深入探讨的有趣话题。

帧同步方案的确立

玩传统IO类小游戏的PVP模式会遇到不少难题,比如人数过多导致游戏难以进行,这使得PVP模式不太划算。但如果我们用ECS结构来开发单挑篮球游戏,就能天然实现帧同步,因此我们选择了这个方案。在开发过程中,这算是一个比较明智的决定。某论坛上的一位大佬也专门写文章介绍了这一点。不过,这个方案也有不足之处,通用性强的框架通常需要牺牲性能,比如协议未压缩等问题,导致帧消息体积较大,这对需要高频操作的篮球游戏来说并不友好。

在游戏开发过程中,各种游戏玩法和操作频率对画面同步效果有很大影响。以篮球游戏为例,在频繁操作中,较大的画面信息会导致诸多问题。此外,在应用画面同步技术时,往往难以有效管理服务器的负载。一旦出现问题,仅靠提交工单处理,耗时较长,效率不高。

自建联机对战平台

开发APP时,我采用了Go语言,参照MGOBE的模式,独立开发了一套在线对战平台。该平台的API命名与MGOBE保持一致,因此客户端无需做太大调整即可实现对接。平台具备tcp和udp两种通信协议的支持,这是它的一个显著优势。不过,udp协议存在指令冗余,网络状况不佳时,消息包容易堆积,这对低端手机来说不太友好。因此,为了低端手机的连接体验,我们选择了tcp协议来连接对战服务器。这样的设计可以确保不同设备在游戏中的连接稳定性。

选择网络连接方式时,要全面考虑设备状况。对于配置较低的智能手机,其网络连接的稳定性可能不足。采用TCP协议,可以在与对战服务器连接时,有效减少许多潜在问题。

帧同步问题排查

客户端负责帧同步的计算,这就意味着两个客户端的计算结果必须完全相同,否则画面会出现不同步。在初期上线的单挑篮球版本中,出现了不少画面不同步的问题。因为服务器无法实时判断客户端是否同步,所以问题的排查只能通过分析客户端日志来完成,这无疑加大了排查的难度。

游戏画面不同步会影响玩家的游戏感受。同时,解决问题的方式不多,主要依靠客户端日志来寻找线索。这对开发者而言,是一项不小的挑战,需要投入大量时间去仔细分析日志内容。

游戏重启与追帧

游戏在重启追帧和途中追帧的步骤上大体一致,只是重启追帧需要重新进入战场,并且需要从第一帧开始追踪。对于像单挑篮球这样的游戏,单局只需1到2分钟,从头开始追帧并不会带来太大压力。采取合理的追帧策略,对确保游戏顺畅运行至关重要。

游戏持续时间不同,对追帧技巧的适用性各异。比如,篮球单挑一局时间不长,实时追帧尚可忍受,可若游戏持续时间较长,可能就需要更高效的追帧方法,以减少对玩家体验的负面影响。

AI行为树的设计

以小游戏中11分玩法为例,我针对10个不同难度,设计了10个行为树。这些行为树间的区别细微,主要在于AI对行为的响应速度和触发几率。此外,我还对AI每个行为的具体触发点进行了限制,并通过配置表来调节行为出现的几率和速度。这种精细的安排,有助于增强AI的智能程度。

游戏难度各异,对AI的策略也有所不同。精心构建AI的行为树,可以使游戏中的AI行为更贴合游戏难度,从而提升玩家的游戏感受。

Spine换装系统改进

changeCloth (skinName: string, slotName: string): any {
    let spine: sp.Skeleton = this.node.spine
    let skeletonData = spine.skeletonData.getRuntimeData()
    let skin = skeletonData.findSkin(skinName)
    const slot = spine.findSlot(slotName)
    const slotIndex = skeletonData.findSlotIndex(slotName)
    const attachment = skin.getAttachment(slotIndex, slotName)
    slot.setAttachment(attachment)
  }

将50个角色划分为50个脊骨,虽然这算是个方法,但每个脊骨都包含动画,这样一来就需要大量动画,每增加一个动画,就得在所有脊骨上同步更新,这对美术团队来说是个不小的压力。此外,这里的API与JavaScript不一致,有些方法和属性未导出时还会出现错误,因此我修改了脊骨在C++运行库中的代码。

游戏开发过程中,各系统模块之间相互牵连。Spine换装系统的升级需与动画等内部功能相协调,如何在不多增美术等人员负担的前提下优化系统,实为一大挑战。

在游戏制作过程中,大家是否也遭遇过类似的难题?期待大家能点个赞、转发这篇文章,并踊跃留言交流。

export default class SpineUtil {
  static async setSkin (spine: sp.Skeleton, heroId, skinId, collocation = {}) { // posType 1头饰 2上衣 3裤子 4鞋子 5手部 6腿部
    const skeletonData = spine.skeletonData.getRuntimeData()
    const baseClothesData = await AssetLoader.loadResAsync(`spine/clothes/c_${heroId}/c_${heroId}`, sp.SkeletonData)
    const baseClothesDataRuntimeData = baseClothesData.getRuntimeData()
    const baseClothesSkin = baseClothesDataRuntimeData.findSkin('c_' + skinId)
    if (!baseClothesSkin) return
    let newSkinName = 'newSkin' + heroId + skinId
    for (let pos in collocation) {
      newSkinName += '_' + collocation[pos]
    }
    const newSkin = new sp.spine.Skin(newSkinName)
    const { SkinColor } = app.db.actor.GetActorById(heroId)
    const findSkin = skeletonData.findSkin(['white', 'yellow', 'black'][SkinColor - 1])
    newSkin.copySkin(findSkin)
    // 使用默认外观
    for (const skinEntry of baseClothesSkin.getAttachments()) {
      const slot = !cc.sys.isNative ? skinEntry.slotIndex : baseClothesSkin.getEntrySlot(skinEntry)
      const name = !cc.sys.isNative ? skinEntry.name : baseClothesSkin.getEntryName(skinEntry)
      const attachment = !cc.sys.isNative ? skinEntry.attachment : skinEntry
      this.addAttachment(SKIN_PART, newSkin, slot, name, attachment)
      this.addAttachment(ARM_PART, newSkin, slot, name, attachment)
    }
    
    ... 省略部分代码
    
    if (skeletonData.skins[skeletonData.skins.length - 1].name === newSkin.name) {
      skeletonData.skins[skeletonData.skins.length - 1] = newSkin
    } else {
      !cc.sys.isNative ? skeletonData.skins.push(newSkin) : skeletonData.addSkin(newSkin)
    }
    spine.setSkin(newSkinName)
    }
}

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

七爪网 行业资讯 从小游戏到APP:探索单挑篮球进阶之路的完整历程 https://www.7claw.com/2801166.html

七爪网源码交易平台

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务