Android?上實現(xiàn)DragonBones換裝功能
前言
最近在預研一款換裝的小游戲,通過在積分樂園中兌換服裝,就可以在不同場景中展示穿上新服裝的角色。對于這類有主題形象的動畫,自然就想到了骨骼動畫,通過網(wǎng)格自由變形和蒙皮技術就能在視覺上呈現(xiàn)所需要的動畫效果,并且骨骼動畫也支持皮膚替換,或者插槽的圖片替換,對于換裝的需求比較友好。因此決定使用骨骼動畫來實現(xiàn)換裝小游戲的Demo,以下就是在Android平臺上實現(xiàn)DragonBones換裝的過程。
技術選型
對于DragonBones在Android端的渲染顯示,有多個方案可以選擇,例如:白鷺引擎或者Cocos2d游戲引擎。最終選擇使用korge來進行渲染,為什么拋棄Cocos2d這個廣泛使用的游戲引擎來渲染呢?主要理由是:
- Cocos2d 游戲引擎加載比較耗時,其首次加載時間無法接受;
- Cocos2d 編譯出來的底層依賴需要單獨裁剪,裁剪后的libcocos.so依然較大;
- Cocos2d 對于游戲動畫的渲染,其渲染的載體是Activity,也就是編譯出來的CocosActivity,這個是無法滿足業(yè)務需要的。因此需要自定義游戲容器,并且需要改動畫加載的容器載體和加載路徑。簡單點來說,可以從任意路徑來加載游戲資源(例如網(wǎng)絡或者本地,不僅僅是assets目錄),并且可以在自定義View中進行渲染。解決思路可以參考:Android實戰(zhàn)之Cocos游戲容器搭建
最終,還是在官方的Github上發(fā)現(xiàn)這條Issue,從而找到了Android上渲染DragonBones的方式。Korge的介紹是這樣的
Modern Multiplatform Game Engine for Kotlin.
Korge的基本用法
1)創(chuàng)建 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 函數(shù)
@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的配置數(shù)據(jù)即可顯示骨骼動畫。
實現(xiàn)換裝的多種實現(xiàn)
靜態(tài)換裝 vs 動態(tài)換裝
靜態(tài)換裝
如果換裝的素材是固定的,可以預先放置在插槽里,通過切換插槽的displayIndex實現(xiàn)換裝。
在骨骼動畫設計時,每個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
? }動態(tài)換裝
如果換裝的素材是不固定的,需要動態(tài)獲取資源,或者通過一張外部圖片來實現(xiàn)換裝效果,可以通過修改slot的顯示紋理即可實現(xiàn)。
```
// 換裝原理是:通過factory.parseTextureAtlasData來解析紋理數(shù)據(jù),紋理為外部圖片,紋理配置為Mock數(shù)據(jù)
private fun changeSlotDisplay(slot: Slot, replaceBitmap: Bitmap) {
? ?// 使用 HashCode 來作為 骨架名稱 和 骨骼名稱
? ?val replaceArmatureName = replaceBitmap.hashCode().toString()
? ?// 需要替換的插槽所包含的顯示對象
? ?val replaceDisplayName = slot._displayFrames.first { it.rawDisplayData != null }.rawDisplayData!!.name
? ?// 通過factory解析紋理數(shù)據(jù)
? ?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 不包含動畫
如果換裝的部位不包含動畫,則可以使用圖片做為換裝素材,具體實現(xiàn)方法如上。 如果換裝的部位包含動畫,則可以使用子骨架做為換裝的素材,API調用方法和換圖片是一樣的,只不過換進去的是子骨架的顯示對象,在引擎層面,圖片和子骨架的顯示對象都是顯示對象,所以處理起來是一樣的,唯一不同的是子骨架不需要考慮軸點,也不能重新設置軸點,因為他自身有動畫數(shù)據(jù)相當于已經(jīng)包含軸點信息。
先將原始骨骼動畫文件中,該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"
? ?// 加載子骨架數(shù)據(jù)
? ?factory.parseDragonBonesData(Json.parse(res[dragonBonesJSONPath].readString())!!)
? ?factory.parseTextureAtlasData(
? ? ? ?Json.parse(res[textureAtlasJSONPath].readString())!!,
? ? ? ?res[textureAtlasPath].readBitmap().mipmaps()
? )
? ?// 獲取解析后的骨骼數(shù)據(jù)
? ?val replaceArmatureData = factory.getArmatureData("xx")
? ?// 通過 replaceSkin 的方式修改 slot display
? ?factory.replaceSkin(armatureDisplay.armature, replaceArmatureData!!.defaultSkin!!)
}
局部換裝 vs 全局換裝
之前說的都是局部換裝,替換的是紋理集中的一塊子紋理,如果希望一次性替換整個紋理集也是支持的。但是紋理集的配置文件不能換(如果配置文件也要換的話,就直接重新構建骨架就好) 也就是說游戲中可以有一套紋理集配置文件對應多個紋理集圖片,實現(xiàn)配置文件不變的情況下?lián)Q整個紋理集。利用這個技術可以實現(xiàn)例如格斗游戲中同樣的角色穿不同顏色的衣服的效果。
全局換裝之Skin修改
DragonBones支持多套皮膚的切換,如果皮膚時固定的,可預先配置在骨骼動畫文件中,需要時直接切換即可。
private fun changeDragonBonesSkin(armatureDisplay: KorgeDbArmatureDisplay) {
? ?val replaceSkin = factory.getArmatureData("xxx")?.getSkin("xxx") ?: return
? ?factory.replaceSkin(armatureDisplay.armature, replaceSkin)
}
全局換裝之紋理修改
如果皮膚并未固定的,需要動態(tài)配置或者網(wǎng)絡下發(fā),那么可以使用紋理替換的方式。
private suspend fun changeDragonBonesSkin() {
? ?val texDeferred = asyncImmediately { res["body/texture_01.png"].readBitmap().mipmaps() }
? ?factory.updateTextureAtlases(texDeferred.await(), "body")
}
總結
對于一款換裝小游戲來講,使用Spine或者是DragonBones的差異不大,其設計思路基本相同,而且Korge同樣也是支持Spine的渲染。從技術實現(xiàn)上,換裝的功能并不難實現(xiàn),只是需要考慮的細節(jié)方面還有很多,例如:
- 服裝商城的在線配置和管理,并且有些服裝還可能自帶動畫
- 某些服裝可能涉及多個插槽,例如:一套裙子,有一部分的層級在身體前面,另一部分的層級在身體后面,那就意味需要兩個插槽才能實現(xiàn)
- 如果該人物形象在多個界面或者應用中出現(xiàn),動畫效果不同,但是身上的服裝相同,需要考慮處理換裝后服裝同步的問題
到此這篇關于Android 上實現(xiàn)DragonBones換裝功能的文章就介紹到這了,更多相關Android DragonBones換裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android采取ContentObserver方式自動獲取驗證碼
這篇文章主要為大家詳細介紹了Android采取ContentObserver方式自動獲取驗證碼,感興趣的小伙伴們可以參考一下2016-08-08
超詳細的Android開發(fā)調試工具ADB命令及安裝介紹
這篇文章主要介紹了Android調試工具ADB安裝使用技巧,非常詳細,有需要的朋友可以借鑒參考下,希望可以有所幫助,祝大家早日升值加薪2021-09-09
Android ViewDragHelper完全解析 自定義ViewGroup神器
這篇文章主要為大家詳細介紹了Android ViewDragHelper完全解析,自定義ViewGroup神器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03

