# 如何建立一個 CustomData ###### tags: `reallusion` `venus` [toc] ## 前言 ## 類別宣告 定義的類別需要繼承 `CCustomData`,最少要加入 `RLDeclareRtti`、`RLDeclareStream`。 * `RLDeclareRtti`: 專門給 `RL::DynamicCast` 用,生成 `Rtti`。 * `RLDeclareStream`: 宣告版號 如果這個類別可能會被用於 `CPointer` 的話要加入 `DeclareSmartPointer`。 如果這個類別有包含其他 `CCustomData` 的話那需要加入 `RLDeclareHoldObject` 如果這個類別有記錄到 Pointer 的話那需要加入 `RLDeclareReferenceObject` > 這邊的 Pointer 係指原生指標例如 `CBase*` 或是 `CNode*`,寫入使用 `StreamWriteReference()`;如果是 `CPointer< T >` 則使用 `StreamWriteLink`;但兩種讀檔都是要使用 `StreamReadLink()` ![](https://hackmd.io/_uploads/r1PszwKe6.png) 以下為範例: ```cpp= #include "RLZeus/RLCustomData.h" #include "RLPangu/RLPointer.h" namespace RL { class CYourData : public CCustomData { DeclareSmartPointer; RLDeclareRtti; RLDeclareStream( 1, 0 ); public: CYourData() = default; virtual ~CYourData() = default; CYourData( const CYourData& kData ); }; typedef CPointer< CYourData > CYourDataPtr; } ``` ## 類別實作 ### 基本款 首先最基礎是一定要實作 Rtti 跟 Stream: ```cpp= RLImplementRtti( CYourData, CCustomData ); RLImplementStream( CYourData ); ``` 和實作以下函數: ```cpp= // override CBase size_t CYourData::GetMemoryUsed() const { size_t uMemused = 0; size_t uParentdUsed = 0; // 注意:公式一定是 當前類別大小 減去 父類別大小 size_t uClassSize = sizeof( CYourData ) - sizeof( CCustomData ); // 計算記憶體大小 uParentdUsed += CCustomData::GetMemoryUsed(); return uMemused + uParentdUsed + uClassSize; } // override CBase size_t CYourData::GetDiskUsed() const { size_t uDynamicSize = 0; size_t uParentSize = CCustomData::GetDiskUsed(); // 計算硬碟大小 return uDynamicSize + uParentSize; } // override CBase Result CYourData::AssignTo( RL::CBase* pObject, ObjectContainer& rkObjects, CProgress* pProgress ) { RLFRA( CCustomData::AssignTo( pObject, rkObjects, pProgress ) ); // Undo / Redo 會呼叫到 CYourData* pkYourData = StaticCast< CYourData >( pObject ); RLPtrInvalidCall( pkYourData ); // 只需要在乎你在 .h 定義的資料就好,直接指派即可 pkYourData->m_kSomeVector = m_kSomeVector; return RL_OK; } // override CBase Result CYourData::Save( CIoStream& rkStream, CProgress* pProgress ) const { RLFRA( CCustomData::Save( rkStream, pProgress ) ); // 存檔案 return RL_OK; } // override CBase Result CYourData::Load( CIoStream& rkStream, CMemberLink* pkLink, CProgress* pProgress ) { RLFRA( CCustomData::Load( rkStream, pkLink, pProgress ) ); const auto& kStreamVersion = Version( rkStream ); if ( kStreamVersion >= CVersion( 1, 0 ) ) { // 讀檔案 } return RL_OK; } // override CBase bool CYourData::IsLinkObjectRequired() const { return CCustomData::IsLinkObjectRequired(); } // override CBase void CYourData::DumpDetail( CDumpDetailInterface* pInterface ) { // 給 Zeus Viwer 用的,看要怎麼呈現資料 // ... CCustomData::DumpDetail( pInterface ); } // override CBase Result CYourData::Link( CIoStream& rkStream, CMemberLink* pkMemberLink, CProgress* pProgress ) { RLFRA( CCustomData::Link( rkStream, pkMemberLink, pProgress ) ); return RL_OK; } // override CBase Result CYourData::RegisterUniqueObject( CIoStream& rkStream, CProgress* pProgress ) const { RLSafeReturn( CCustomData::RegisterUniqueObject( rkStream, pProgress ) ); return RL_OK; } ``` > 注意如果有修改到儲存的資料,要記得 `SetModified()` 千萬別忘記註冊你的類別,可參考 `ICDataFactory.cpp` 或 `RLMotionMatchingFactory.cpp`。 ```cpp= RLRegisterStreamFactory( CYourData ); ``` :::spoiler 實際在 LoadObject 的時候,會用 class 的名稱去建立 object,如果沒註冊則無法 load ![](https://hackmd.io/_uploads/BJSgngLTh.png) ::: ### 如果內含 CustomData 如果該類別有宣告 `RLDeclareHoldObject`,則實作: ```cpp= bool CMDInteractiveData::IsLinkObjectRequired() const { if ( m_spMatchingData ) { return true; } return CCustomData::IsLinkObjectRequired(); } // 如果你的 CustomData 持有 SmartPointer 的話才需要 RegisterUniqueObject Result CMDInteractiveData::RegisterUniqueObject( CIoStream& rkStream, CProgress* pProgress ) const { RLSafeReturn( CCustomData::RegisterUniqueObject( rkStream, pProgress ) ); if ( m_spMatchingData ) { RLFailReturnAssert( m_spMatchingData->RegisterUniqueObject( rkStream, pProgress ) ); } return RL_OK; } CBase* CUnusedBoneData::GetObjectByNameRtti( const wchar_t* acName, NameCmpFunction pNameCmpFunc, const CRtti* pkRtti, bool bDerivedFromClass, bool bMarkOnly ) const { return CCustomData::GetObjectByNameRtti( acName, pNameCmpFunc, pkRtti, bDerivedFromClass, bMarkOnly );; } void CUnusedBoneData::GetAllObjectsByNameRtti( std::vector< CBase* >& rkObjects, const wchar_t* acName, NameCmpFunction pNameCmpFunc, const CRtti* pkRtti, bool bDerivedFromClass, bool bMarkOnly ) const { CCustomData::GetAllObjectsByNameRtti( rkObjects, acName, pNameCmpFunc, pkRtti, bDerivedFromClass, bMarkOnly ); } bool CUnusedBoneData::SetObjectTreeMarked( std::set< CZeusID >& kMarkId ) { return CCustomData::SetObjectTreeMarked( kMarkId ); } void CUnusedBoneData::ResetObjectTreeMarked() { CCustomData::ResetObjectTreeMarked(); } void CUnusedBoneData::DumpTree( CDumpTreeInterface* pInterface ) { if ( m_spMatchingData ) { pInterface->DumpObjectTree( m_spMatchingData ); } CCustomData::DumpTree( pInterface ); } // spObject 是 Copy 出來的複製體 // ObjectMap 是過程複製的對應 Map // 該 Data 的 smart pointer 需要寫進來這 Result CUnusedBoneData::DeepCopyTo( CPointer< CBase >& spObject, ObjectMap& rkObjects, CProgress* pProgress ) const { RLSafeReturn( CCustomData::DeepCopyTo( spObject, rkObjects, pProgress ) ); CMDInteractiveData* pInteractiveData = StaticCast< CMDInteractiveData >( spObject ); if ( m_spMatchingData ) { // 這邊基本上就是說,目前的資料 'm_spMatchingData' 複製一份 DeepCopyTo // sp 是複製出來的,然後這邊同時也會設定值給 spObject // 所以跑完這個流程你資料內的 smart pointer 都會是指向複製版本的 data // 不需要 GraftToByMap CMotionMatchingDataPtr sp; RLSubDeepCopyTo( sp, m_spMatchingData, CMotionMatchingData ); RLMemoryCheck( sp ); pInteractiveData->SetMatchingData( sp ); } return RL_OK; } ``` ### 如果儲存到指標 如果該類別有宣告 `RLDeclareReferenceObject`,則實作: ```cpp= RLImplementReferenceObject( CYourData ); ``` ```cpp= Result CYourData::Link( CIoStream& rkStream, CMemberLink* pkMemberLink, CProgress* pProgress ) { RLFRA( CCustomData::Link( rkStream, pkMemberLink, pProgress ) ); // 這裡要寫讀取那些指標 // Link 順序很重要! 跟你的 Load 呼叫的 StreamReadLink() 順序要一致 return RL_OK; } Result CYourData::GraftToByMap( ObjectMap& rkObjects ) { // 因為 RLDeclareReferenceObject 導致必定要實作 return GraftToByMap( rkObjects, ENotFoundAction::KeepOriginal ); } // CBase 有宣告,但不使用巨集會不能繼承 Result CYourData::GraftToByMap( ObjectMap& rkObjects, const ENotFoundAction& eAction ) { // 基本雛形 auto kCopyObjectPair = rkObjects.find( m_pMyPointer ); if ( kCopyObjectPair != rkObjects.end() ) { m_pMyPointer = kCopyObjectPair->second; } else { switch ( eAction ) { case ENotFoundAction::KeepOriginal: break; case ENotFoundAction::SetNull: m_pMyPointer = nullptr break; case ENotFoundAction::ReturnFail: return RL_FAIL; } } return CCustomData::GraftToByMap( rkObjects ); } ``` 要使用上面的 Marco 才可以呼叫到 `StreamWriteReference()`(讀檔時) ### 如果儲存到 smart pointer (持有) 如果有持有指標(smart pointer)的話,要確保這兩個 function 都能 query 到那個成員,否則存出去會沒存到那個物件,導致回來會 link error - `GetAllObjectsByNameRtti()` - `RegisterUniqueObject()` #### GraftToByMap 使用時機: 當 class 內有 pointer 時,`DeepCopy` 時仍然會指向舊的物件,這時候就需要 `GraftToByMap()` 雖然 `RLDeclareReferenceObject` marco 會幫你宣告 `GraftToByMap(ObjectMap& rkObjects)`,導致你一定要覆寫,不然根據下面的 code ```cpp // RLBase.cpp Result CBase::MapGraftTo( ObjectMap& rkObjects, const ENotFoundAction& eAction ) { CSortedSet< CBase* > kAllObject; RLFRA( FindAllObjects< CBase >( kAllObject, this, nullptr, true ) ); for ( const auto pBase : kAllObject ) { if ( pBase->GraftToByMap( rkObjects, eAction ) == RL_NOT_IMPLEMENTED ) { RLFRA( pBase->GraftToByMap( rkObjects ) ); // 應該要慢慢改成用 Enum 的版本 } } return RL_OK; } ``` 理論上會先採用 enum 版的 `GraftToByMap()`,現在都是提倡你**使用 enum 版**的 `GraftToByMap()` ### 讀檔存檔 ``` StreamBytesLink() 和 MemoryBytes() ``` ### Copy Constructor & Assignment Operator 一般會看到把 copy ctor 和 assignment operator 關在 `protected` ![](https://hackmd.io/_uploads/ryNkpBo23.png =x500) - copy ctor - 需要實作 `CopyConstructTo()` - assignment operator 不開放的原因是因為 shallow/deep copy 的問題 - 所以不開放 `operator=` ### 如果要刪除成員,但還要支援載入 當開發過程中,想要刪除資料結構中的成員,但因為 TA or Art 已經存出 content 了,還要支援載入,那麼要做的就是: - 步驟: - 升級版號 - 拿掉該變數除了 `Load()` 以外的 Usage - 將原本 Load 的 `StreamRead` 改成一個 dummy (建立 same type local variable) 並傳入,並且要將整個區塊用 `if ( kVersion <= 新的板號 )` 隔開 - 可以在這邊進行將舊資料升級成新資料 但如果該資料已經在 validate 中有用過,就拿不掉了,能做的就是建立一個 Deprecated 區塊,並將該變數,以及相關的 member function 移進去,並祈禱有一天這個東西能被拿掉 > 至少在撰文的此刻 2025/01/17 仍是如此 ## 如何使用 ### 建立並掛載 CustomData ```cpp= RL::CYourDataPtr spYourData = RLNEW CYourData(); RLMemoryCheck( spYourData ); RLFRA( pNode->AttachCustomData( spYourData ) ); ``` > `AttachCustomData()` 來自於 `CBase*` ### 找尋並使用 ```cpp= RL::CYourDataPtr spYourData = RL::FindObject< RL::CYourData >( pNode->GetCustomData() ); RLPidA( spYourData ); RLFRA( spYourData->DoSomething() ); ``` ### 刪除 ```cpp= RL::CYourDataPtr spYourData = RL::FindObject< RL::CYourData >( pNode->GetCustomData() ); RLPidA( spYourData ); RLFRA( pNode->DetachCustomData( spYourData ) ); ``` ## 轉型 ```cpp= CCustomData* pkCustomData; CCustomDataHP* pDataHP = DynamicCast< CCustomDataHP >( pkCustomData ); ``` > 參考 `CBase::AttachCustomData()` ## 淺談 CustomData 記指標 > ~~CustomData 不太會去記指標,記名字就好~~ [name=Aren, 2023/04/25] > 但其實你如果記 string 的話那它效率就變差了... 因為字串要每個字元比較但指標就比一次就好了 [name=Taco, 2023/04/26] > 記指標的 CustomData 可以參考 `CLinkMapData` [name=Aren] > 記指標的話要特別 > 你如果 Data 存 SmartPointer 就是持有它,反之用 Raw Pointer 就是 Reference [name=Joe, 2024/03/12] 但如果真的記了(例如 `CBase*` ),可以讓 `CYourData` 類別繼承 `CDestroyWatcher`,並且實作 `OnObjectDestroy()`。 然後在 `Link()` 階段讀取資料後記得要使用 `AddDestroyWatcher()`,才有意義。 這個用途就是當該節點被刪除後,你不用特地呼叫刪除,例如你把該 `CBase*` 儲存在一個陣列中,它會去刪掉對應的資料(其實就是呼叫 `OnObjectDestroy()`)。 詳情程式碼可以參考 `CMDInteractiveData` 的 `m_kMDPropNodeDataMap`。 ::: warning DestroyWatcher: 有 Add 必有 Remove ::: ## 疑難排解 ### 讀檔結果跟指標有關的都讀不到(部分讀不到) 我原本在 `CInteractiveDefition` 中新增了 `CArrayList<CLookAtConfig>`,其中 `CLookAtConfig` 的成員 `m_pTarget` 是一個 `RL::CNode*` 原生指標。 我遇到的問題就是在讀檔階段會讀不到,甚至不只 `CArrayList<CLookAtConfig>`,連紀錄在 `CMDInteractiveData` 中的 `m_spMatchingData` 和 `m_kMDPropNodeDataMap` 指標部分全部都讀不到,變成 `nullptr`。 原因很簡單,就是因為我的讀 pointer 的順序,跟你 Link 的順序是有關聯的!讀 pointer 的順序就是看 `StreamReadLink()`。原先因為 `CArrayList<CLookAtConfig>` 是在 `CInteractiveDefition`,而 `CInteractiveDefition` 讀檔的順序比 `m_spMatchingData` 和 `m_kMDPropNodeDataMap` 都來的早。 所以 `Link()` 的**順序**也要對應才正確! ### 儲存 Project 正常但儲存 Prop 就會 Link Error 儲存 Prop 之前會先 Copy 一份,之所以會 link failed 就是因為它指向的指標(`RL::CBase*`)沒有一起被儲存下來,它在 `Link()` 的時候透過 `Load()` 讀取到的 `ZeusID` 是找不到的。 所以要檢查的方向可以從 `GraftToByMap()` 或是 `MapGraftTo()` 開始查起,先把在 `GraftToByMap()` 之前 pointer 指向的位置以及之後的位置記錄起來,然後再看 `Save()` 的時候是不是儲存 graft 之後的位置,基本上會 link failed 就是這個時候它實際 save 到的還是 graft 前的 pointer 位置,所以之後載入 Prop 就會發生 link failed。 如果檢查 `GraftToByMap()` 還是看不出原因,基本上就跟該物件的 copy assginment 或是過程中 function 傳遞是用 copy 而不是 reference 等所導致的。