前言

最近在预研一款换装的小游戏,通过在积分乐园中兑换服装,就可以在不同场景中展示穿上新服装的角色。对于这类有主题形象的动画,自然就想到了骨骼动画,通过网格自由变形和蒙皮技术就能在视觉上呈现所需要的动画效果,并且骨骼动画也支持皮肤替换,或者插槽的图片替换,对于换装的需求比较友好。因此决定使用骨骼动画来实现换装小游戏的Demo,以下就是在Android平台上实现DragonBones换装的过程。

技术选型

对于DragonBones在Android端的渲染显示,有多个方案可以选择,例如:白鹭引擎或者Cocos2d游戏引擎。最终选择使用korge来进行渲染,为什么抛弃Cocos2d这个广泛使用的游戏引擎来渲染呢?主要理由是:

  • Cocos2d 游戏引擎加载比较耗时,其首次加载时间无法接受;
  • Cocos2d 编译出来的底层依赖需要单独裁剪,裁剪后的libcocos.so依然较大;
  • Cocos2d 对于游戏动画的渲染,其渲染的载体是Activity,也就是编译出来的CocosActivity,这个是无法满足业务需要的。因此需要自定义游戏容器,并且需要改动画加载的容器载体和加载路径。简单点来说,可以从任意路径来加载游戏资源(例如网络或者本地,不仅仅是assets目录),并且可以在自定义View中进行渲染。解决思路可以参考:Android实战之Cocos游戏容器搭建

最终,还是在官方的Github上发现这条Issue,从而找到了Android上渲染DragonBones的方式。Korge的介绍是这样的

Modern Multiplatform Game Engine for Kotlin.

Korge的基本用法

1)创建 DragonBones Scene

class DisplayChangeImgScene : BaseDbScene() {
    companion object {
        private const val SKE_JSON = "mecha_1004d_show/mecha_1004d_show_ske.json"
        private const val TEX_JSON = "mecha_1004d_show/mecha_1004d_show_tex.json"
        private const val TEX_PNG = "mecha_1004d_show/mecha_1004d_show_tex.png"
    }
    private val factory = KorgeDbFactory()
    override suspend fun Container.createSceneArmatureDisplay(): KorgeDbArmatureDisplay {
        val skeDeferred = asyncImmediately { res[SKE_JSON].readString() }
        val texDeferred = asyncImmediately { res[TEX_JSON].readString() }
        val imgDeferred = asyncImmediately { res[TEX_PNG].readBitmap().mipmaps() }
​
        val skeJsonData = skeDeferred.await()
        val texJsonData = texDeferred.await()
        factory.parseDragonBonesData(Json.parse(skeJsonData)!!)
        factory.parseTextureAtlasData(Json.parse(texJsonData)!!, imgDeferred.await())
​
        val armatureDisplay = factory.buildArmatureDisplay("mecha_1004d")!!.position(500, 700)
        armatureDisplay.animation.play("idle")
​
        return armatureDisplay
    }
}

2)使用KorgeAndroidView加载 Scene Module

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }​
    private val slotDisplayModule by sceneModule<DisplayChangeImgScene>()​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        binding.root.addView(KorgeAndroidView(this).apply { 
            loadModule(slotDisplayModule)
        })
    }
}

3)sceneModule 函数

@MainThread
inline fun <reified DS : BaseDbScene> Activity.sceneModule(
    windowWidth: Int = resources.displayMetrics.widthPixels,
    windowHeight: Int = resources.displayMetrics.heightPixels
): Lazy<Module> {
    return SceneModuleLazy(DS::class, windowWidth, windowHeight)
}
class SceneModuleLazy<DS : BaseDbScene>(
    private val dbSceneClass: KClass<DS>,
    private val width: Int,
    private val height: Int
) : Lazy<Module> {
    private var cached: Module? = null​
    override val value: Module
        get() {
            return cached ?: object : Module() {
                override val mainScene = dbSceneClass
                override suspend fun AsyncInjector.configure() {
                    mapPrototype(dbSceneClass) {
                        val sceneInstance = Class.forName(dbSceneClass.qualifiedName!!).newInstance()
                        sceneInstance as DS
                    }
                }
                override val fullscreen = true​
                override val size: SizeInt
                    get() = SizeInt(width, height)
                override val windowSize: SizeInt
                    get() = SizeInt(width, height)
            }
        }​
    override fun isInitialized(): Boolean = cached != null
}

上面就是最简单的Demo,通过加载DragonBones的配置数据即可显示骨骼动画。

实现换装的多种实现

静态换装 vs 动态换装

静态换装

如果换装的素材是固定的,可以预先放置在插槽里,通过切换插槽的displayIndex实现换装。

在骨骼动画设计时,每个slot可对应多个display,例如:

{
  "name": "weapon_hand_l",
  "display": [
    {
      "name": "weapon_1004_l",
      "transform": {
        "x": 91.22,
        "y": -30.21
      }
    },
    {
      "name": "weapon_1004b_l",
      "transform": {
        "x": 122.94,
        "y": -44.14
      }
    },
    {
      "name": "weapon_1004c_l",
      "transform": {
        "x": 130.95,
        "y": -56.95
      }
    },
    {
      "name": "weapon_1004d_l",
      "transform": {
        "x": 134.67,
        "y": -55.25
      }
    },
    {
      "name": "weapon_1004e_l",
      "transform": {
        "x": 155.62,
        "y": -59.2
      }
    }
  ]
}

在代码中,可直接切换display进行换装,即:

    private var leftWeaponIndex = 0
    private val leftDisplayList = listOf(
        "weapon_1004_l", "weapon_1004b_l", "weapon_1004c_l", "weapon_1004d_l", "weapon_1004e_l"
    )
    override suspend fun Container.createSceneArmatureDisplay(): KorgeDbArmatureDisplay {
        val skeDeferred = asyncImmediately { Json.parse(res["mecha_1004d_show/mecha_1004d_show_ske.json"].readString())!! }
        val texDeferred = asyncImmediately { res["mecha_1004d_show/mecha_1004d_show_tex.json"].readString() }
        val imgDeferred = asyncImmediately { res["mecha_1004d_show/mecha_1004d_show_tex.png"].readBitmap().mipmaps() }
        factory.parseDragonBonesData(skeDeferred.await())
        factory.parseTextureAtlasData(Json.parse(texDeferred.await())!!, imgDeferred.await())​
        val armatureDisplay = factory.buildArmatureDisplay("mecha_1004d")!!.position(500, 700)
        armatureDisplay.animation.play("idle")
​
        val slot = armatureDisplay.armature.getSlot("weapon_hand_l")!!
        mouse {
            upAnywhere {
                leftWeaponIndex  ;
                leftWeaponIndex %= leftDisplayList.size
​
                factory.replaceSlotDisplay(
                    dragonBonesName = "mecha_1004d_show",
                    armatureName = "mecha_1004d",
                    slotName = "weapon_hand_l",
                    displayName = leftDisplayList[leftWeaponIndex],
                    slot = slot
                )
            }
        }​
        return armatureDisplay
    }

动态换装

如果换装的素材是不固定的,需要动态获取资源,或者通过一张外部图片来实现换装效果,可以通过修改slot的显示纹理即可实现。

```
// 换装原理是:通过factory.parseTextureAtlasData来解析纹理数据,纹理为外部图片,纹理配置为Mock数据
private fun changeSlotDisplay(slot: Slot, replaceBitmap: Bitmap) {
    // 使用 HashCode 来作为 骨架名称 和 骨骼名称
    val replaceArmatureName = replaceBitmap.hashCode().toString()
    // 需要替换的插槽所包含的显示对象
    val replaceDisplayName = slot._displayFrames.first { it.rawDisplayData != null }.rawDisplayData!!.name
    // 通过factory解析纹理数据
    val mockTexModel = mockTexModel(replaceArmatureName, replaceDisplayName, replaceBitmap.width, replaceBitmap.height)
    val textureAtlasData = Json.parse(gson.toJson(mockTexModel))!!
    factory.parseTextureAtlasData(textureAtlasData, replaceBitmap.mipmaps())
​
    // 替换 Display 的纹理,替换的图片和原图大小、位置一致
    val replaceTextureData = getReplaceDisplayTextureData(replaceArmatureName, replaceDisplayName)
    slot.replaceTextureData(replaceTextureData)
​
    slot._displayFrame?.displayData?.transform?.let {
        // 修改 display 相对于 slot 的位置、初始缩放等配置
    }
}
private fun getReplaceDisplayTextureData(replaceArmatureName: String, replaceDisplayName: String): TextureData {
    val data = factory.getTextureAtlasData(replaceArmatureName)
    data!!.fastForEach { textureAtlasData ->
        val textureData = textureAtlasData.getTexture(replaceDisplayName)
        if (textureData != null) {
            return textureData
        }
    }
    throw Exception("getNewDisplayTextureData null")
}
private fun mockTexModel(armatureName: String, displayName: String, imgW: Int, imgH: Int): DragonBonesTexModel {
    val originTexModel = gson.fromJson(texJsonData, DragonBonesTexModel::class.java)
​
    val subTexture: DragonBonesTexModel.SubTexture = run loop@{
        originTexModel.subTexture.forEach { subTexture ->
            if (subTexture.name == displayName) {
                return@loop subTexture.apply {
                    this.x = 0
                    this.y = 0
                }
            }
        }
        throw Exception("Can not find replace display!")
    }
    return DragonBonesTexModel(
        name = armatureName,
        width = imgW,
        height = imgH,
        subTexture = listOf(subTexture)
    )
}
```

包含动画 vs 不包含动画

如果换装的部位不包含动画,则可以使用图片做为换装素材,具体实现方法如上。 如果换装的部位包含动画,则可以使用子骨架做为换装的素材,API调用方法和换图片是一样的,只不过换进去的是子骨架的显示对象,在引擎层面,图片和子骨架的显示对象都是显示对象,所以处理起来是一样的,唯一不同的是子骨架不需要考虑轴点,也不能重新设置轴点,因为他自身有动画数据相当于已经包含轴点信息。

先将原始骨骼动画文件中,该slot的display信息定义为空。例如:

{
  "name": "1036",
  "display": [
    {
      "name": "blank"
    }
  ]
},
{
  "name": "1082",
  "display": [
    {
      "name": "blank"
    }
  ]
},

在子骨架中定义 slot 的 display 信息。例如:

           "slot": [
                {
                    "name": "1019",
                    "parent": "root"
                }
            ],
            "skin": [
                {
                    "name": "",
                    "slot": [
                        {
                            "name": "1019",
                            "display": [
                                {
                                    "type": "mesh",
                                    "name": "glove/2080500b",
                                    "width": 159,
                                    "height": 323,
                                    "vertices": [
                                        104.98,
                                        -1078.6,
                                        108.08,
                                        -1094.03
                                    ],
                                    "uvs": [
                                        0.45257,
                                        0.1035,
                                        0.4721,
                                        0.15156,
                                        0.4234,
                                        0.05575
                                    ],
                                    "triangles": [
                                        7,
                                        11,
                                        18,
                                        20
                                    ],
                                    "weights": [
                                        2,
                                        3,
                                        0.92
                                    ],
                                    "slotPose": [
                                        1,
​
                                        0,
                                        0
                                    ],
                                    "bonePose": [
                                        6,
                                        0.193207,
​
                                        139.903737,
                                        -897.076346
                                    ],
                                    "edges": [
                                        19,
                                        18,
                                        18,
                                        20,
                                        19
                                    ],
                                    "userEdges": [
                                        16,
                                        11,
                                        7
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ],

使用子骨架的显示对象进行替换,以下是使用直接替换 skin 的方式,和替换 display 的原理相同。

private suspend fun replaceDragonBonesDisplay(armatureDisplay: KorgeDbArmatureDisplay) {
    val path = "you_xin/suit1/replace/"
    val dragonBonesJSONPath = path   "xx_ske.json"
    val textureAtlasJSONPath = path   "xx_tex.json"
    val textureAtlasPath = path   "xx_tex.png"
    // 加载子骨架数据
    factory.parseDragonBonesData(Json.parse(res[dragonBonesJSONPath].readString())!!)
    factory.parseTextureAtlasData(
        Json.parse(res[textureAtlasJSONPath].readString())!!,
        res[textureAtlasPath].readBitmap().mipmaps()
    )
    // 获取解析后的骨骼数据
    val replaceArmatureData = factory.getArmatureData("xx")
    // 通过 replaceSkin 的方式修改 slot display
    factory.replaceSkin(armatureDisplay.armature, replaceArmatureData!!.defaultSkin!!)
}

局部换装 vs 全局换装

之前说的都是局部换装,替换的是纹理集中的一块子纹理,如果希望一次性替换整个纹理集也是支持的。但是纹理集的配置文件不能换(如果配置文件也要换的话,就直接重新构建骨架就好) 也就是说游戏中可以有一套纹理集配置文件对应多个纹理集图片,实现配置文件不变的情况下换整个纹理集。利用这个技术可以实现例如格斗游戏中同样的角色穿不同颜色的衣服的效果。

全局换装之Skin修改

DragonBones支持多套皮肤的切换,如果皮肤时固定的,可预先配置在骨骼动画文件中,需要时直接切换即可。

private fun changeDragonBonesSkin(armatureDisplay: KorgeDbArmatureDisplay) {
    val replaceSkin = factory.getArmatureData("xxx")?.getSkin("xxx") ?: return
    factory.replaceSkin(armatureDisplay.armature, replaceSkin)
}

全局换装之纹理修改

如果皮肤并未固定的,需要动态配置或者网络下发,那么可以使用纹理替换的方式。

private suspend fun changeDragonBonesSkin() {
    val texDeferred = asyncImmediately { res["body/texture_01.png"].readBitmap().mipmaps() }
    factory.updateTextureAtlases(texDeferred.await(), "body")
}

总结

对于一款换装小游戏来讲,使用Spine或者是DragonBones的差异不大,其设计思路基本相同,而且Korge同样也是支持Spine的渲染。从技术实现上,换装的功能并不难实现,只是需要考虑的细节方面还有很多,例如:

  • 服装商城的在线配置和管理,并且有些服装还可能自带动画
  • 某些服装可能涉及多个插槽,例如:一套裙子,有一部分的层级在身体前面,另一部分的层级在身体后面,那就意味需要两个插槽才能实现
  • 如果该人物形象在多个界面或者应用中出现,动画效果不同,但是身上的服装相同,需要考虑处理换装后服装同步的问题

到此这篇关于Android 上实现DragonBones换装功能的文章就介绍到这了,更多相关Android  DragonBones换装内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Android 上实现DragonBones换装功能的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  6. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

  7. Xamarin iOS图像在Grid内部重叠

    heyo,所以在Xamarin我有一个使用并在其中包含一对,所有这些都包含在内.这在Xamarin.Android中看起来完全没问题,但是在Xamarin.iOS中,图像与标签重叠.我不确定它的区别是什么–为什么它在Xamarin.Android中看起来不错但在iOS中它的全部都不稳定?

  8. 在iOS上向后播放HTML5视频

    我试图在iPad上反向播放HTML5视频.HTML5元素包括一个名为playbackRate的属性,它允许以更快或更慢的速率或相反的方式播放视频.根据Apple’sdocumentation,iOS不支持此属性.通过每秒多次设置currentTime属性,可以反复播放,而无需使用playbackRate.这种方法适用于桌面Safari,但似乎在iOS设备上的搜索限制为每秒1次更新–在我的情况下太慢了.有没有办法在iOS设备上向后播放HTML5视频?解决方法iOS6Safari现在支持playbackRat

  9. 使用 Swift 语言编写 Android 应用入门

    Swift标准库可以编译安卓armv7的内核,这使得可以在安卓移动设备上执行Swift语句代码。做梦,虽然Swift编译器可以胜任在安卓设备上编译Swift代码并运行。这需要的不仅仅是用Swift标准库编写一个APP,更多的是你需要一些框架来搭建你的应用用户界面,以上这些Swift标准库不能提供。简单来说,构建在安卓设备上使用的Swiftstdlib需要libiconv和libicu。通过命令行执行以下命令:gitclonegit@github.com:SwiftAndroid/libiconv-libi

  10. Android – 调用GONE然后VISIBLE使视图显示在错误的位置

    我有两个视图,A和B,视图A在视图B上方.当我以编程方式将视图A设置为GONE时,它将消失,并且它正下方的视图将转到视图A的位置.但是,当我再次将相同的视图设置为VISIBLE时,它会在视图B上显示.我不希望这样.我希望视图B回到原来的位置,这是我认为会发生的事情.我怎样才能做到这一点?编辑–代码}这里是XML:解决方法您可以尝试将两个视图放在RelativeLayout中并相对于彼此设置它们的位置.

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部