Android?上實現(xiàn)DragonBones換裝功能
前言
最近在預(yù)研一款換裝的小游戲,通過在積分樂園中兌換服裝,就可以在不同場景中展示穿上新服裝的角色。對于這類有主題形象的動畫,自然就想到了骨骼動畫,通過網(wǎng)格自由變形和蒙皮技術(shù)就能在視覺上呈現(xiàn)所需要的動畫效果,并且骨骼動畫也支持皮膚替換,或者插槽的圖片替換,對于換裝的需求比較友好。因此決定使用骨骼動畫來實現(xiàn)換裝小游戲的Demo,以下就是在Android平臺上實現(xiàn)DragonBones換裝的過程。
技術(shù)選型
對于DragonBones在Android端的渲染顯示,有多個方案可以選擇,例如:白鷺引擎或者Cocos2d游戲引擎。最終選擇使用korge來進(jìn)行渲染,為什么拋棄Cocos2d這個廣泛使用的游戲引擎來渲染呢?主要理由是:
- Cocos2d 游戲引擎加載比較耗時,其首次加載時間無法接受;
- Cocos2d 編譯出來的底層依賴需要單獨裁剪,裁剪后的libcocos.so依然較大;
- Cocos2d 對于游戲動畫的渲染,其渲染的載體是Activity,也就是編譯出來的CocosActivity,這個是無法滿足業(yè)務(wù)需要的。因此需要自定義游戲容器,并且需要改動畫加載的容器載體和加載路徑。簡單點來說,可以從任意路徑來加載游戲資源(例如網(wǎng)絡(luò)或者本地,不僅僅是assets目錄),并且可以在自定義View中進(jìn)行渲染。解決思路可以參考: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)換裝
如果換裝的素材是固定的,可以預(yù)先放置在插槽里,通過切換插槽的displayIndex實現(xiàn)換裝。
在骨骼動畫設(shè)計時,每個slot可對應(yīng)多個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進(jìn)行換裝,即:
? ?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調(diào)用方法和換圖片是一樣的,只不過換進(jìn)去的是子骨架的顯示對象,在引擎層面,圖片和子骨架的顯示對象都是顯示對象,所以處理起來是一樣的,唯一不同的是子骨架不需要考慮軸點,也不能重新設(shè)置軸點,因為他自身有動畫數(shù)據(jù)相當(dāng)于已經(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 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ] ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? ? ? ? ] ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ] ? ? ? ? ? ? ? } ? ? ? ? ? ],
使用子骨架的顯示對象進(jìn)行替換,以下是使用直接替換 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 全局換裝
之前說的都是局部換裝,替換的是紋理集中的一塊子紋理,如果希望一次性替換整個紋理集也是支持的。但是紋理集的配置文件不能換(如果配置文件也要換的話,就直接重新構(gòu)建骨架就好) 也就是說游戲中可以有一套紋理集配置文件對應(yīng)多個紋理集圖片,實現(xiàn)配置文件不變的情況下?lián)Q整個紋理集。利用這個技術(shù)可以實現(xiàn)例如格斗游戲中同樣的角色穿不同顏色的衣服的效果。
全局換裝之Skin修改
DragonBones支持多套皮膚的切換,如果皮膚時固定的,可預(yù)先配置在骨骼動畫文件中,需要時直接切換即可。
private fun changeDragonBonesSkin(armatureDisplay: KorgeDbArmatureDisplay) { ? ?val replaceSkin = factory.getArmatureData("xxx")?.getSkin("xxx") ?: return ? ?factory.replaceSkin(armatureDisplay.armature, replaceSkin) }
全局換裝之紋理修改
如果皮膚并未固定的,需要動態(tài)配置或者網(wǎng)絡(luò)下發(fā),那么可以使用紋理替換的方式。
private suspend fun changeDragonBonesSkin() { ? ?val texDeferred = asyncImmediately { res["body/texture_01.png"].readBitmap().mipmaps() } ? ?factory.updateTextureAtlases(texDeferred.await(), "body") }
總結(jié)
對于一款換裝小游戲來講,使用Spine或者是DragonBones的差異不大,其設(shè)計思路基本相同,而且Korge同樣也是支持Spine的渲染。從技術(shù)實現(xiàn)上,換裝的功能并不難實現(xiàn),只是需要考慮的細(xì)節(jié)方面還有很多,例如:
- 服裝商城的在線配置和管理,并且有些服裝還可能自帶動畫
- 某些服裝可能涉及多個插槽,例如:一套裙子,有一部分的層級在身體前面,另一部分的層級在身體后面,那就意味需要兩個插槽才能實現(xiàn)
- 如果該人物形象在多個界面或者應(yīng)用中出現(xiàn),動畫效果不同,但是身上的服裝相同,需要考慮處理換裝后服裝同步的問題
到此這篇關(guān)于Android 上實現(xiàn)DragonBones換裝功能的文章就介紹到這了,更多相關(guān)Android DragonBones換裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
android 加載本地聯(lián)系人實現(xiàn)方法
在android開發(fā)過程中,有些功能需要訪問本地聯(lián)系人列表,本人搜集整理了一番,拿出來和大家分享一下,希望可以幫助你們2012-12-12Android activity動畫不生效原因及解決方案總結(jié)
android activity動畫是一個比較簡單的功能。但是使用時總會由于各種小問題導(dǎo)致動畫失效,筆者根據(jù)自己經(jīng)驗,整理了各種可能導(dǎo)致的原因,期望能對你有所幫助2021-11-11Android如何實現(xiàn)設(shè)備的異顯功能詳解
這篇文章主要給大家介紹了關(guān)于Android如何實現(xiàn)設(shè)備的異顯功能的相關(guān)資料,這篇文章通過示例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-02-02android實現(xiàn)簡單進(jìn)度條ProgressBar效果
這篇文章主要為大家詳細(xì)介紹了android實現(xiàn)簡單進(jìn)度條ProgressBar效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07