Android 上实现DragonBones换装功能

目录
  • 前言
  • 技术选型
  • Korge的基本用法
  • 实现换装的多种实现
    • 静态换装 vs 动态换装
      • 静态换装
      • 动态换装
    • 包含动画 vs 不包含动画
      • 局部换装 vs 全局换装
      • 全局换装之Skin修改
      • 全局换装之纹理修改
  • 总结

前言

最近在预研一款换装的小游戏,通过在积分乐园中兑换服装,就可以在不同场景中展示穿上新服装的角色。对于这类有主题形象的动画,自然就想到了骨骼动画,通过网格自由变形和蒙皮技术就能在视觉上呈现所需要的动画效果,并且骨骼动画也支持皮肤替换,或者插槽的图片替换,对于换装的需求比较友好。因此决定使用骨骼动画来实现换装小游戏的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换装内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android新建水平节点进度条示例

    目录 前言 效果图 圆圈和文字状态 文字居中 代码 声明下style 接着创建布局文件 再Activity中使用它 mTextList数据集合 前言 效果图 前几天在网上没有找到合适的横向节点进度条,自己动手写了一个,先来看看效果图 圆圈和文字状态 我们看到小圆圈和文字有几种状态呢? 第一个空心的小圆圈是处理完成的状态 第二个实心的小圆圈是处理中的状态 第三个实心的小圆圈是待处理的状态没错,我们看到了小圆圈和文字有三种处理状态 文字居中 我们写一个类继承自AppCompatTextView,通过

  • Android颜色处理SweepGradient扫描及梯度渲染示例

    目录 扫描渲染 效果图: 代码: 扫描渲染 为什么什么叫扫描渲染呢?  相信大家都看过雷达扫描的效果,尤其是在安全软件中. public SweepGradient(float cx, float cy, int[] colors, float[] positions) Parameters: cx 渲染中心点x 坐标 cy 渲染中心y 点坐标 colors 围绕中心渲染的颜色数组,至少要有两种颜色值 positions 相对位置的颜色数组,可为null,  若为null,可为null,颜色沿渐

  • Android Canva实现渐变进度条

    目录 前言 前言 标题说渐变进度条是为了方便理解,这里本身的项目背景是一款表盘的分针.先上图: 表盘 周圈蓝色的渐变条(分针)就是本次要实现的东西. 1.拆分 首先,熟悉Canvas的朋友应该知道它可以画出各种形状,但偏偏没有一头是圆的环形(这里不考虑使用path绘制).所以我们不得不把它拆分为2个形状:圆环与圆. 2.绘制圆环 绘制圆环有很多种方法,比如画2个圆取补集之类的.这里直接使用canvas.drawArc()函数来画.先看看函数原型: void drawArc (RectF oval

  • Android 上实现DragonBones换装功能

    目录 前言 技术选型 Korge的基本用法 实现换装的多种实现 静态换装 vs 动态换装 静态换装 动态换装 包含动画 vs 不包含动画 局部换装 vs 全局换装 全局换装之Skin修改 全局换装之纹理修改 总结 前言 最近在预研一款换装的小游戏,通过在积分乐园中兑换服装,就可以在不同场景中展示穿上新服装的角色.对于这类有主题形象的动画,自然就想到了骨骼动画,通过网格自由变形和蒙皮技术就能在视觉上呈现所需要的动画效果,并且骨骼动画也支持皮肤替换,或者插槽的图片替换,对于换装的需求比较友好.因此决

  • android使用SkinManager实现换肤功能的示例

    试着用鸿洋大神写的SkinManager实现了换肤功能. 一.配置 在app下build.gradle中添加依赖: //换肤功能 compile 'com.zhy:changeskin:4.0.2' 这样就配置好了,然后在程序入口进行初始化. 二.全局初始化 在自己创建的继承application的类中添加: //换肤sdk初始化 SkinManager.getInstance().init(this); 这个类肯定要在清单文件<application/>节点配置的. 接下来还需要注册. 三.

  • Android编程实现换肤功能实例

    本文实例讲述了Android编程实现换肤功能的方法.分享给大家供大家参考,具体如下: 本系列专题培训适用范围:初级Android程序员,即有J2SE基础和Android初级水平.J2SE基础是指掌握JAVA语法,1.5.1.6新增的语法不完全掌握也没关系.了解基本的面向对象思想.能编写简单的J2SE程序,掌握基本的调试方法,熟悉Swing更好.Android初级是指掌握Activity.Service.BroadcastReceiver.Intent.SQLite.UI组件的使用,能参照例子编写

  • Android实现文件上传和下载倒计时功能的圆形进度条

    screenshot 截图展示 import step1. Add it in your root build.gradle at the end of repositories: allprojects { repositories { ... maven { url 'https://jitpack.io' } } } step2. Add the dependency dependencies { compile 'com.github.yanjiabin:ExtendsRingPrigr

  • Android RecyclerView上拉加载更多功能回弹实现代码

    实现原理是使用RecyclerView的OnTouchListener方法监听滑动 在adapter里面增加两项footview 其中date.size为显示的加载条,可以自定义,date.size+1为空白的View,我们设置其高度为0 我们通过LinearLayoutManager的 findLastVisibleItemPosition判断显示的最后一条数据,如果是空白view,表示加载条已经完全展示,松开即可刷新. 回弹效果是通过在滑动时动态改变空白view的高度,达到阻尼效果 ,回弹时

  • Android Recyclerview实现上拉加载更多功能

    在项目中使用列表的下拉刷新和上拉加载更多是很常见的功能,下拉刷新我们可以用Android自带的SwipeRefreshLayout这个很好解决.但是上拉加载更多就要去找一些框架了,刚开始的时候我找到一个Mugen的github开源框架,但是有个问题,当页面能够一次加载全部item的时候,上拉加载的功能就失效了. 这是因为当界面一次能够加载完全部item的时候,继续往上拉,Recyclerview的滑动监听,中的onScrolled方法只会在页面加载的时候调用一次,只后就不会被调用了,并且dy=0

  • android换肤功能 如何动态获取控件中背景图片的资源id?

    这个是在在做一个换肤功能时遇到的问题. 对于换肤,网上都有示例,可以从别的皮肤安装包中读取所要的资源,前提是你必须先持有这个资源的引用名称,像R.drawable.background(喂,这不是废话嘛).这个换肤的方案原理就是,自身应用的资源名称是R.drawable.background,那皮肤包中应该也是这个名称,然后通过这个名称获取该资源在皮肤包中的具体id,代码: //先获取本地资源引用名称,type name是R.drawable.background中的"drawable"

  • Android开发实现切换主题及换肤功能示例

    本文实例讲述了Android开发实现切换主题及换肤功能.分享给大家供大家参考,具体如下: 废话不说先看效果: 创建ColorTheme类用于主题更换: public class ColorTheme { AppCompatActivity ap; public ColorTheme(AppCompatActivity _ap){ap=_ap;} public void updateTheme(int _data){ String data=Integer.toString(_data); Fil

  • Android使用WebView实现离线阅读功能

    1.先看效果图,加载动画: 加载完成,注意当前为飞行模式! 2.使用 1).让你的javabean实现OffLineLevelItem接口,因为我的这个离线阅读支持多级下载,比如Demo中的每个频道下面的第一页item都可以缓存. package com.zgh.offlinereader; import java.util.List; public interface OffLineLevelItem { //是否有下一级 boolean haveNextLevel(); //内容url St

  • 分析Android App中内置换肤功能的实现方式

    Android平台api没有特意为换肤提供一套简便的机制,这可能是外国的软件更注重功能和易用,不流行换肤.系统不提供直接支持,只能自行研究. 换肤,可以认为是动态替换资源(文字.颜色.字体大小.图片.布局文件--).这个使用编程语言来动态设置是可以做到的,例如使用View的setBackgroundResource.setTextSize.setTextColor等函数.但我们不可能在每个activity里对页面里的所有控件都通过调用这些函数来换肤,这样的程序代码难以维护.扩展,也违背了UI和代

随机推荐