械拆裝系統(tǒng)總體設(shè)計(jì):從分層架構(gòu)到數(shù)據(jù)驅(qū)動(dòng)實(shí)現(xiàn))
機(jī)械拆裝類(lèi)項(xiàng)目但凡你碰過(guò)教學(xué)仿真或者工業(yè)培訓(xùn)估計(jì)都有印象一堆人圍著三維模型干瞪眼不知道先拆哪個(gè)螺絲、再用哪個(gè)工具。用Unity來(lái)做機(jī)械拆裝系統(tǒng)本質(zhì)上不是“給模型加個(gè)鼠標(biāo)拖拽”那么簡(jiǎn)單而是一次從模型數(shù)據(jù)、交互邏輯到流程控制的整體設(shè)計(jì)。我早期踩過(guò)不少坑比如模型導(dǎo)入后坐標(biāo)全亂、拆裝順序?qū)懰涝诖a里、一換設(shè)備就各種卡。后來(lái)把“總體設(shè)計(jì)”四個(gè)字認(rèn)真對(duì)待才慢慢摸清楚里面該分幾層、哪些東西必須先想明白。這篇文章就圍繞“Unity機(jī)械拆裝系統(tǒng)的總體設(shè)計(jì)”來(lái)聊。不整虛的直接拆解系統(tǒng)該怎么分層、拆裝序列怎么設(shè)計(jì)、零件交互怎么做、數(shù)據(jù)怎么配最后給一套可以照著搭的簡(jiǎn)易實(shí)現(xiàn)。適合要做機(jī)械拆裝、虛擬仿真實(shí)驗(yàn)、維修培訓(xùn)這類(lèi)項(xiàng)目的開(kāi)發(fā)者尤其是剛從“搭個(gè)場(chǎng)景隨便轉(zhuǎn)轉(zhuǎn)”進(jìn)入到“要做成正規(guī)系統(tǒng)”階段的朋友。1. 總體設(shè)計(jì)之前想清楚機(jī)械拆裝系統(tǒng)的邊界很多項(xiàng)目死在第一步需求沒(méi)拆干凈就上手拖場(chǎng)景。機(jī)械拆裝聽(tīng)起來(lái)很具體但不同場(chǎng)景下的目標(biāo)完全不一樣總體設(shè)計(jì)必須先回答“這個(gè)系統(tǒng)究竟要干什么”。1.1 機(jī)械拆裝類(lèi)項(xiàng)目通常要解決哪些問(wèn)題機(jī)械拆裝系統(tǒng)在行業(yè)里大多跑在這幾類(lèi)場(chǎng)景中教學(xué)培訓(xùn)職校、高校的機(jī)電專(zhuān)業(yè)用虛擬仿真替代真實(shí)拆裝實(shí)訓(xùn)。這種場(chǎng)景下系統(tǒng)要強(qiáng)調(diào)步驟規(guī)范性和操作引導(dǎo)甚至要做考核評(píng)分。維修輔助給售后、運(yùn)維人員做拆裝指導(dǎo)。重點(diǎn)是把復(fù)雜的維修步驟可視化能查某個(gè)零件怎么拆、扭矩多大、有沒(méi)有專(zhuān)用工具。產(chǎn)品展示展會(huì)、客戶(hù)演示。這種偏演示向視覺(jué)效果優(yōu)先交互可以簡(jiǎn)化但不能卡頓。我做過(guò)一個(gè)偏培訓(xùn)類(lèi)的項(xiàng)目當(dāng)時(shí)以為只要“能點(diǎn)、能拖、能裝回去”就行結(jié)果驗(yàn)收時(shí)對(duì)方拿出一份拆裝工藝卡上面每一個(gè)零件都有拆裝順序、方向、工具、力矩要求甚至還有安全注意事項(xiàng)。那一刻才明白機(jī)械拆裝系統(tǒng)的核心不是三維交互而是對(duì)拆裝流程的結(jié)構(gòu)化管理。所以做總體設(shè)計(jì)之前第一件事是把業(yè)務(wù)規(guī)則摸清楚。要拆的部件有多少個(gè)零件哪些必須按順序拆哪些可以并行拆每個(gè)零件拆下來(lái)之后要不要消失、要不要高亮、要不要有特寫(xiě)鏡頭這些規(guī)則不先理清楚Unity里做到一半就得推倒重來(lái)。1.2 Unity做拆裝系統(tǒng)的天然優(yōu)勢(shì)與短板Unity在這個(gè)領(lǐng)域里屬于“上手快、上限看能力”的引擎。優(yōu)勢(shì)很明顯實(shí)時(shí)渲染表現(xiàn)力強(qiáng)PBR材質(zhì)、后處理、光照方案都比較成熟模型展示效果不錯(cuò)。跨平臺(tái)方便Windows、Android、WebGL都能出實(shí)訓(xùn)室里的PC、平板、VR設(shè)備都能覆蓋。生態(tài)里現(xiàn)成插件多比如Highlighting、DOTween、VR交互框架等能省不少底層時(shí)間。短板同樣要認(rèn)清楚。Unity不是CAD軟件導(dǎo)入工業(yè)模型后裝配約束關(guān)系、爆炸圖邏輯、拆裝序列這些統(tǒng)統(tǒng)不會(huì)自動(dòng)生成。物理引擎在拆裝這種場(chǎng)景里往往還幫倒忙剛體加多了零件亂飛不加又缺少真實(shí)感。所以機(jī)械拆裝系統(tǒng)里不要指望物理引擎替你解決裝配判定合理做法是自己寫(xiě)基于空間位置和角度的約束判斷。1.3 設(shè)計(jì)目標(biāo)拆解從教學(xué)演示到交互實(shí)訓(xùn)做總體設(shè)計(jì)時(shí)我習(xí)慣把目標(biāo)拆成三個(gè)層級(jí)再定開(kāi)發(fā)優(yōu)先級(jí)第一層能看。模型可以自由旋轉(zhuǎn)、縮放拉近拉遠(yuǎn)能看清整體結(jié)構(gòu)和零件關(guān)系。 第二層能拆??梢赃x中零件、拆下零件并且拆裝順序有邏輯約束。 第三層能練。系統(tǒng)加入步驟引導(dǎo)、錯(cuò)誤提示、考核評(píng)分不只是演示還能訓(xùn)練。第一次做項(xiàng)目別一上來(lái)就奔第三層。先把“能拆”這一層做扎實(shí)把拆裝序列的數(shù)據(jù)結(jié)構(gòu)定義好、交互手感調(diào)好再順著數(shù)據(jù)層往上去做引導(dǎo)和評(píng)分會(huì)順很多。如果一開(kāi)始就堆特效和UI結(jié)果往往是交互邏輯一改UI全崩。2. 總體架構(gòu)設(shè)計(jì)拆裝項(xiàng)目也得分層拆裝系統(tǒng)雖說(shuō)不算大項(xiàng)目但如果不分層代碼很快就會(huì)變成“一堆腳本互相調(diào)用、誰(shuí)也不知道誰(shuí)負(fù)責(zé)什么”的狀態(tài)。我建議按數(shù)據(jù)層、邏輯層、表現(xiàn)層三層來(lái)切。2.1 數(shù)據(jù)層零件信息與拆裝序列怎么存機(jī)械拆裝里最怕硬編碼。比如把每個(gè)零件該往哪移、按什么順序拆全寫(xiě)死在腳本里。換個(gè)模型、改個(gè)順序就得動(dòng)代碼這不是總體設(shè)計(jì)該有的樣子。數(shù)據(jù)層的任務(wù)就是把這些業(yè)務(wù)規(guī)則和數(shù)據(jù)從代碼里抽出來(lái)。我常用的做法是先用ScriptableObject定義零件數(shù)據(jù)和拆裝步驟數(shù)據(jù)。零件數(shù)據(jù)記錄ID、顯示名稱(chēng)、所屬部件、拆裝時(shí)的目標(biāo)位置和旋轉(zhuǎn)步驟數(shù)據(jù)記錄當(dāng)前步驟要操作的零件ID、操作類(lèi)型拆/裝、提示文案、是否允許并行。[CreateAssetMenu(fileName PartData, menuName MechanicalDemo/PartData)] public class PartData : ScriptableObject { public string partId; public string displayName; public GameObject prefab; public Vector3 installPosition; public Vector3 installRotation; public float installTolerance 0.05f; }這里有個(gè)經(jīng)驗(yàn)如果項(xiàng)目后期可能換模型最好別直接把prefab引用寫(xiě)在ScriptableObject里而是用partId在啟動(dòng)時(shí)去資源池里匹配實(shí)例。這樣模型更新、材質(zhì)替換不用改動(dòng)業(yè)務(wù)邏輯。2.2 邏輯層拆裝規(guī)則、狀態(tài)機(jī)與流程控制邏輯層是拆裝系統(tǒng)的中樞管三件事當(dāng)前處于什么狀態(tài)、當(dāng)前步驟允許對(duì)哪些零件操作、操作結(jié)果是否合法。流程控制我用狀態(tài)機(jī)來(lái)管理。不需要引入復(fù)雜的狀態(tài)機(jī)插件自己寫(xiě)一個(gè)輕量的就夠用?;緺顟B(tài)可以分成準(zhǔn)備中、待拆卸、待裝配、完成。每個(gè)步驟對(duì)應(yīng)一個(gè)或一組零件只有當(dāng)當(dāng)前步驟的零件滿(mǎn)足條件流程才能推進(jìn)到下一步。public enum DisassembleState { Pending, WaitingDisassemble, WaitingAssemble, Completed }邏輯層要把“能拆嗎”“裝對(duì)了嗎”這類(lèi)判斷獨(dú)立出來(lái)不直接操作UI也不直接控制模型動(dòng)畫(huà)。UI層只負(fù)責(zé)監(jiān)聽(tīng)狀態(tài)事件模型層只負(fù)責(zé)播放對(duì)應(yīng)的位置/旋轉(zhuǎn)變化邏輯層不關(guān)心畫(huà)面怎么表現(xiàn)只管規(guī)則。這個(gè)分層做對(duì)了后面加考核、加語(yǔ)音提示、加VR支持都方便。2.3 表現(xiàn)層模型、動(dòng)畫(huà)、特效與UI如何協(xié)同表現(xiàn)層是和用戶(hù)直接打交道的地方但也是容易失控的地方。最常見(jiàn)的問(wèn)題是UI腳本里混了業(yè)務(wù)判斷比如按鈕點(diǎn)擊后直接判斷“當(dāng)前能不能拆”結(jié)果換了一個(gè)流程規(guī)則UI里改半天。我的習(xí)慣是表現(xiàn)層里只做三類(lèi)事接受輸入、播放反饋、顯示狀態(tài)。接受輸入鼠標(biāo)點(diǎn)擊、觸摸、VR手柄的事件捕獲后交給邏輯層判斷。播放反饋零件高亮、移動(dòng)動(dòng)畫(huà)、拆下時(shí)的飛入特效、安裝時(shí)的卡扣音效。顯示狀態(tài)根據(jù)邏輯層發(fā)來(lái)的狀態(tài)事件更新UI面板、步驟列表、提示文字。舉個(gè)實(shí)際例子當(dāng)用戶(hù)點(diǎn)擊一個(gè)零件Input模塊先拾取到目標(biāo)然后問(wèn)邏輯層“當(dāng)前狀態(tài)下這個(gè)零件能不能被操作”邏輯層返回可拆、可裝或者禁止。Input模塊再根據(jù)返回結(jié)果去決定是否播放拾取動(dòng)畫(huà)、是否彈出提示。這樣流程規(guī)則始終在邏輯層表現(xiàn)層再花哨也不影響核心邏輯。3. 核心模塊設(shè)計(jì)與關(guān)鍵技術(shù)點(diǎn)機(jī)械拆裝系統(tǒng)的核心模塊并不復(fù)雜但每個(gè)模塊都有幾個(gè)繞不開(kāi)的細(xì)節(jié)。我挑幾個(gè)重點(diǎn)展開(kāi)講。3.1 零件的高亮與拾取交互拆裝系統(tǒng)里用戶(hù)必須知道當(dāng)前能操作哪些零件、鼠標(biāo)指向的是哪個(gè)零件。沒(méi)有高亮反饋的拆裝系統(tǒng)體驗(yàn)基本等于閉著眼睛拆盲盒。高亮方案我通常用兩種。簡(jiǎn)單項(xiàng)目直接用兩層材質(zhì)切換原來(lái)的材質(zhì)存下來(lái)替換成高亮材質(zhì)復(fù)雜一點(diǎn)用URP的Render Objects或者第三方描邊插件。需要注意高亮不只是懸停時(shí)做步驟引導(dǎo)時(shí)也要做——當(dāng)前步驟允許拆的零件常亮提示不允許的零件鼠標(biāo)懸停也不變亮這能傳達(dá)很多信息。拾取交互上核心是射線(xiàn)檢測(cè)。用Camera主攝像頭發(fā)射線(xiàn)檢測(cè)到帶Part標(biāo)簽的物體后觸發(fā)事件。有一個(gè)細(xì)節(jié)很容易忽略當(dāng)鼠標(biāo)穿過(guò)多個(gè)零件時(shí)要取射線(xiàn)碰撞點(diǎn)最近的零件而不是第一個(gè)碰撞的。尤其機(jī)械零件經(jīng)常有嵌套關(guān)系靠得近射線(xiàn)命中的往往是后面的擋板。Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit[] hits Physics.RaycastAll(ray, 100f); Array.Sort(hits, (a, b) a.distance.CompareTo(b.distance)); foreach (RaycastHit hit in hits) { if (hit.collider.CompareTag(Part)) { // 只處理最近的有效零件 break; } }3.2 基于約束的裝配/拆卸判定裝一個(gè)零件什么才算裝到位了這是機(jī)械拆裝系統(tǒng)里最核心的問(wèn)題。很多人第一反應(yīng)是“算距離”但只算距離不夠圓柱銷(xiāo)插進(jìn)去但旋轉(zhuǎn)角度錯(cuò)了一樣不行。我常用的做法是位置約束加角度約束兩項(xiàng)都滿(mǎn)足才算安裝成功。位置約束看零件當(dāng)前位置和目標(biāo)安裝位置的距離角度約束看當(dāng)前旋轉(zhuǎn)和目標(biāo)旋轉(zhuǎn)的角度差每個(gè)零件都配容差范圍。這個(gè)方案被我們內(nèi)部叫“傻瓜約束”代碼簡(jiǎn)單但足以覆蓋絕大多數(shù)機(jī)械拆裝的判定場(chǎng)景。public bool IsInstallCorrect(Transform part, PartData data) { float distanceError Vector3.Distance(part.position, data.installPosition); float angleError Quaternion.Angle(part.rotation, Quaternion.Euler(data.installRotation)); return distanceError data.installTolerance angleError data.angleTolerance; }更精細(xì)的項(xiàng)目可以引入軸向約束、工具約束等。比如某個(gè)螺栓必須先用扳手?jǐn)Q松才能拆下這時(shí)候單純坐標(biāo)判斷就不好使了需要在PartData里增加一個(gè)requiredTool字段。但記住約束規(guī)則越復(fù)雜數(shù)據(jù)配置越重總體設(shè)計(jì)上一定要權(quán)衡別為了“顯得專(zhuān)業(yè)”把所有約束都堆上去。3.3 拆裝序列管理與步驟引導(dǎo)拆裝序列是機(jī)械拆裝系統(tǒng)的靈魂。設(shè)計(jì)的時(shí)候要區(qū)分兩種流程嚴(yán)格順序和任務(wù)組。嚴(yán)格順序模式下步驟是線(xiàn)性的只有完成當(dāng)前步驟下一步才解鎖。這種模式適合教學(xué)考核邏輯最簡(jiǎn)單用數(shù)組加索引就能實(shí)現(xiàn)。任務(wù)組模式下一組零件內(nèi)可以任意順序拆完組全部完成后再進(jìn)入下一組。比如拆一個(gè)減速器端蓋上的6顆螺栓可以隨便先拆哪顆但6顆都拆完才能打開(kāi)端蓋。這種模式更貼近真實(shí)操作但流程控制要額外維護(hù)一個(gè)組內(nèi)完成計(jì)數(shù)。在我們項(xiàng)目里我直接用List 定義整個(gè)拆裝過(guò)程DisassembleStep里放一個(gè)List 的partIds。如果列表只有一個(gè)零件就是嚴(yán)格順序如果有多個(gè)就是并行任務(wù)組。這樣一套數(shù)據(jù)結(jié)構(gòu)覆蓋兩種流程而且配置數(shù)據(jù)時(shí)非常直觀。[Serializable] public class DisassembleStep { public string stepName; public Liststring partIds new Liststring(); public string guideText; public bool isAssemblyStep; }步驟引導(dǎo)除了UI文字還應(yīng)該有鏡頭引導(dǎo)。步驟觸發(fā)時(shí)相機(jī)平滑移動(dòng)到目標(biāo)零件附近并給出一個(gè)特寫(xiě)角度。這個(gè)效果對(duì)體驗(yàn)提升非常顯著很多用戶(hù)反饋說(shuō)“相機(jī)跟著走不用自己找零件在哪”。實(shí)現(xiàn)上可以用DOTween替身控制相機(jī)位置和旋轉(zhuǎn)監(jiān)聽(tīng)步驟切換事件后播放一個(gè)2秒左右的移動(dòng)動(dòng)畫(huà)。3.4 數(shù)據(jù)驅(qū)動(dòng)設(shè)計(jì)用配置表驅(qū)動(dòng)流程這里再展開(kāi)講講數(shù)據(jù)驅(qū)動(dòng)的整體思路因?yàn)樗苯記Q定項(xiàng)目后期維護(hù)成本。我給學(xué)員做項(xiàng)目評(píng)審時(shí)最常問(wèn)的一句話(huà)是“換一個(gè)型號(hào)的機(jī)器你的系統(tǒng)要改多少代碼”理想答案是“一個(gè)代碼都不改”只改配置數(shù)據(jù)。模型換掉、零件列表?yè)Q掉、拆裝序列換掉邏輯代碼原地不動(dòng)。要做到這點(diǎn)所有零件實(shí)例在場(chǎng)景啟動(dòng)時(shí)都要根據(jù)配置去實(shí)例化或加載所有步驟流程都從配置里讀取而不是對(duì)著預(yù)設(shè)寫(xiě)引用。數(shù)據(jù)源根據(jù)項(xiàng)目規(guī)模選擇。小項(xiàng)目用ScriptableObject最方便編輯器里就能配置中大型項(xiàng)目用JSON或Excel導(dǎo)出再轉(zhuǎn)ScriptableObject更友好工藝人員也能參與配置。我之前一個(gè)項(xiàng)目還接過(guò)后端動(dòng)態(tài)下發(fā)放置數(shù)據(jù)客戶(hù)端啟動(dòng)拉取JSON實(shí)現(xiàn)同一個(gè)App適配多個(gè)設(shè)備型號(hào)思路都是一樣的就是數(shù)據(jù)驅(qū)動(dòng)。4. 實(shí)操示例從零搭建一個(gè)簡(jiǎn)易拆裝流程理論講一堆不動(dòng)手等于零。下面我用一個(gè)最簡(jiǎn)單的例子跑通“拾取零件-拆下-安裝-進(jìn)入下一步”的完整流程。場(chǎng)景物體就用三個(gè)Cube加一個(gè)Cube模擬一個(gè)簡(jiǎn)化的機(jī)械部件組合重點(diǎn)在交互和流程結(jié)構(gòu)。4.1 場(chǎng)景準(zhǔn)備與模型處理先在場(chǎng)景里建四個(gè)Cube一個(gè)當(dāng)?shù)鬃潭ú粍?dòng)三個(gè)當(dāng)零件零件A、零件B、零件C。給底座和零件分別設(shè)置Tag底座設(shè)為Untagged零件設(shè)為Part方便射線(xiàn)檢測(cè)時(shí)過(guò)濾。給每個(gè)零件添加Box Collider底座的Collider可以保留但拾取檢測(cè)時(shí)會(huì)忽略掉。再建一個(gè)空物體PartsRoot把三個(gè)零件設(shè)為它的子物體。后續(xù)用代碼實(shí)例化零件時(shí)統(tǒng)一掛在這個(gè)節(jié)點(diǎn)下場(chǎng)景層級(jí)會(huì)干凈很多。固定底座的原始位置記錄下來(lái)作為零件安裝時(shí)的參考基準(zhǔn)。我給每個(gè)零件建一個(gè)PartData配置指定安裝位置和角度。這里的位置值不用精算先大致把三個(gè)零件擺在底座上用腳本把當(dāng)前Transform數(shù)值保存進(jìn)配置。[ContextMenu(Capture Transform As InstallData)] public void CaptureTransforms() { installPosition transform.position; installRotation transform.eulerAngles; }這個(gè)編輯器腳本特別實(shí)用。在場(chǎng)景里手動(dòng)把零件擺到正確裝配位置右鍵執(zhí)行這個(gè)方法安裝數(shù)據(jù)就自動(dòng)寫(xiě)進(jìn)配置不用手動(dòng)抄坐標(biāo)。4.2 用腳本實(shí)現(xiàn)零件拾取和移動(dòng)拆裝系統(tǒng)的交互流程一般是點(diǎn)擊零件選中可以用鼠標(biāo)拖動(dòng)。我這里用簡(jiǎn)化方案點(diǎn)擊后讓零件進(jìn)入“跟隨鼠標(biāo)”狀態(tài)在鼠標(biāo)射線(xiàn)與一個(gè)水平面或固定深度平面的交點(diǎn)處移動(dòng)松開(kāi)鼠標(biāo)則放置。核心邏輯寫(xiě)在PartInteraction.cs里public class PartInteraction : MonoBehaviour { private Camera mainCamera; private PartData currentPartData; private Transform currentPart; private bool isDragging; void Start() { mainCamera Camera.main; } void Update() { if (Input.GetMouseButtonDown(0)) { TryPickPart(); } else if (Input.GetMouseButtonUp(0) isDragging) { DropPart(); } if (isDragging currentPart ! null) { MovePartToMouse(); } } private void TryPickPart() { Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { if (hit.collider.CompareTag(Part)) { // 這里需要向邏輯層查詢(xún)當(dāng)前步驟是否允許操作這個(gè)零件 currentPart hit.collider.transform; isDragging true; } } } }我這里沒(méi)寫(xiě)具體向邏輯層查詢(xún)的代碼但實(shí)際項(xiàng)目中一定要加。否則就會(huì)出現(xiàn)“序列第一步還沒(méi)拆螺栓呢用戶(hù)先把第10個(gè)零件拽出來(lái)了”的尷尬情況。4.3 判斷裝配位置并鎖定零件拆下來(lái)容易裝回去才是真考驗(yàn)。用戶(hù)拖動(dòng)零件到目標(biāo)位置附近時(shí)系統(tǒng)要實(shí)時(shí)判斷是否“到位”。我的做法是拖拽過(guò)程中實(shí)時(shí)計(jì)算零件當(dāng)前位置和目標(biāo)安裝位置的誤差誤差進(jìn)入容差范圍就給一個(gè)“吸附”效果再播放卡扣音效并鎖定位置。吸附效果用DOTween實(shí)現(xiàn)非常簡(jiǎn)單只需要判斷通過(guò)后把零件DO Move到目標(biāo)位置再關(guān)閉拖拽即可。要注意這里的吸附判定不能每幀都只判定一次就立刻吸附而是要先連續(xù)多幀滿(mǎn)足條件再觸發(fā)否則用戶(hù)只是快速劃過(guò)目標(biāo)位置也會(huì)被“吸住”體驗(yàn)很糟。private int fitFrameCount; private const int fitFrameNeed 5; private void FitCheck() { bool isFit distanceError currentPartData.installTolerance angleError currentPartData.angleTolerance; if (isFit) { fitFrameCount; if (fitFrameCount fitFrameNeed) { SnapAndLock(); } } else { fitFrameCount 0; } }這段代碼不復(fù)雜但它是整個(gè)手感的核心。連續(xù)幀判斷很關(guān)鍵我見(jiàn)過(guò)不少項(xiàng)目省略這一步結(jié)果零件在目標(biāo)位置附近稍微晃一下就被吸過(guò)去了用戶(hù)感覺(jué)“這軟件太敏感了”。4.4 加入步驟管理與UI提示最后把流程控制串起來(lái)。定義一個(gè)DisassembleManager來(lái)控制當(dāng)前步驟索引監(jiān)聽(tīng)零件拆下和安裝成功的事件然后推動(dòng)步驟前進(jìn)。public class DisassembleManager : MonoBehaviour { public ListDisassembleStep steps; public int currentStepIndex; public Text stepText; private DisassembleStep CurrentStep steps[currentStepIndex]; void Start() { UpdateUI(); } public void OnPartRemoved(string partId) { if (CurrentStep.partIds.Contains(partId) !CurrentStep.isAssemblyStep) { // 標(biāo)記為已拆 NextStepIfComplete(); } } public void OnPartInstalled(string partId) { if (CurrentStep.partIds.Contains(partId) CurrentStep.isAssemblyStep) { NextStepIfComplete(); } } private void NextStepIfComplete() { // 檢查當(dāng)前步驟組內(nèi)是否全部完成完成則索引1并更新UI if (CheckCurrentStepComplete()) { currentStepIndex; UpdateUI(); } } }UI方面用Text顯示當(dāng)前步驟“第3步拆除端蓋螺栓2/6”。建議把步驟ICON和文字一起顯示效果更直觀。實(shí)際項(xiàng)目里還可以加進(jìn)度條、步驟列表、錯(cuò)誤操作記錄但底層的Manager結(jié)構(gòu)都差不多核心就是監(jiān)聽(tīng)零件操作事件控制步驟索引更新。5. 常見(jiàn)問(wèn)題與避坑指南這部分內(nèi)容不是網(wǎng)上扒來(lái)的是實(shí)操中一個(gè)個(gè)踩出來(lái)的。整理成表格方便查閱。常見(jiàn)問(wèn)題現(xiàn)象解決思路射線(xiàn)檢測(cè)點(diǎn)穿透點(diǎn)擊前面零件選中的卻是后面零件用RaycastAll取最近的有效碰撞體或者給零件分層Raycast時(shí)用LayerMask過(guò)濾裝配位置吸附過(guò)敏感零件快速劃過(guò)目標(biāo)位置就被吸住連續(xù)5幀以上滿(mǎn)足容差條件再吸附避免單幀誤判拆裝序列狀態(tài)丟失已經(jīng)拆完的零件重啟場(chǎng)景后又要重新拆用PlayerPrefs或存檔保存步驟索引退回主界面時(shí)保存進(jìn)度模型導(dǎo)入后坐標(biāo)錯(cuò)亂零件位置不對(duì)安裝判定永遠(yuǎn)失敗建模時(shí)統(tǒng)一單位、統(tǒng)一軸方向?qū)朐O(shè)置里把Scale Factor設(shè)為1用空物體做基準(zhǔn)對(duì)齊拆裝時(shí)零件穿模零件移動(dòng)過(guò)程中和其他物體交錯(cuò)穿插簡(jiǎn)單項(xiàng)目關(guān)閉零件自身碰撞體開(kāi)啟自動(dòng)遮擋半透明復(fù)雜項(xiàng)目用插值移動(dòng)讓位移更平滑多零件并行步驟判定異常一組里拆了2個(gè)就跳下一步檢查CurrentStep.partIds的完成記錄需要按partId標(biāo)記狀態(tài)而不是用“當(dāng)前步驟只處理一個(gè)零件”的邏輯5.1 交互穿透與遮擋問(wèn)題交互穿透是拆裝項(xiàng)目最容易遇到的問(wèn)題尤其是零件密集的地方。射線(xiàn)穿過(guò)去打在后面的零件上是常事更麻煩的是你想點(diǎn)A零件但B零件的Collider把它擋住了。我的經(jīng)驗(yàn)是給零件分兩個(gè)層級(jí)主要零件用精確Collider小零件如螺栓、墊片用近似Collider用盒體或球體替代射線(xiàn)檢測(cè)時(shí)優(yōu)先命中大件小件通過(guò)名字匹配。還有一個(gè)辦法是按住Alt鍵切換高亮候選每次檢測(cè)到多個(gè)零件時(shí)按Alt可以在命中列表里循環(huán)切換適合小零件操作。5.2 坐標(biāo)精度與裝配容差容差設(shè)置直接決定手感。容差太大隨便放哪都算裝好沒(méi)有裝配感容差太小位置偏一點(diǎn)就判失敗用戶(hù)會(huì)暴躁。我們項(xiàng)目里位置容差一般設(shè)在零件尺寸的2%~5%角度容差設(shè)在3度到5度。更穩(wěn)妥的做法是給用戶(hù)一個(gè)“輔助吸附”的設(shè)計(jì)當(dāng)誤差接近容差范圍時(shí)零件半透明顯示目標(biāo)位置的輪廓引導(dǎo)用戶(hù)對(duì)齊。這個(gè)效果直接用Unity的Bounds畫(huà)線(xiàn)或者用單獨(dú)的Wireframe材質(zhì)做能顯著降低操作難度尤其對(duì)于新手用戶(hù)。5.3 大規(guī)模模型卡頓優(yōu)化機(jī)械模型經(jīng)常動(dòng)輒幾十萬(wàn)面一個(gè)減速器就有幾十上百個(gè)零件。如果不做優(yōu)化Unity場(chǎng)景能卡成PPT。優(yōu)先做幾下幾件事一是模型導(dǎo)入時(shí)開(kāi)Enable GPU Instancing合并相同材質(zhì)的網(wǎng)格二是遠(yuǎn)處零件用LOD組三是零件拆下后把它身上的Collider、動(dòng)畫(huà)組件等非必要組件臨時(shí)禁用四是高亮材質(zhì)不要一換就重新生成材質(zhì)實(shí)例盡量用共享材質(zhì)參數(shù)控制避免Draw Call飆升。5.4 序列狀態(tài)丟失與重入問(wèn)題“拆了一半不小心點(diǎn)了退出再進(jìn)來(lái)又從頭開(kāi)始”是用戶(hù)最抓狂的情況。拆裝系統(tǒng)如果需要復(fù)用一定要記錄當(dāng)前拆裝狀態(tài)。我的簡(jiǎn)單做法是步驟索引和每個(gè)零件的當(dāng)前狀態(tài)未拆、已拆、已裝用PlayerPrefs存JSON。每次步驟切換時(shí)保存一次場(chǎng)景加載時(shí)讀取并恢復(fù)零件位置和步驟UI。要注意保存時(shí)機(jī)的選擇頻繁保存會(huì)產(chǎn)生IO開(kāi)銷(xiāo)每步保存一次即可。另外一個(gè)容易忽略的點(diǎn)是重入問(wèn)題。步驟完成回調(diào)有時(shí)會(huì)重復(fù)觸發(fā)比如安裝判定連續(xù)兩幀都滿(mǎn)足條件OnPartInstalled被調(diào)了兩次。用“零件狀態(tài) 步驟狀態(tài)”雙重校驗(yàn)可以避免只有零件狀態(tài)從“未安裝”變?yōu)椤按惭b”時(shí)才觸發(fā)回調(diào)后續(xù)重復(fù)滿(mǎn)足條件不再觸發(fā)。寫(xiě)在后面幾個(gè)實(shí)踐體會(huì)這套總體設(shè)計(jì)的思路在我做過(guò)的幾個(gè)拆裝項(xiàng)目里反復(fù)使用從教學(xué)演示到考核系統(tǒng)都靠它撐住了。幾點(diǎn)體會(huì)供參考。數(shù)據(jù)驅(qū)動(dòng)的架構(gòu)一定要盡早做哪怕最開(kāi)始只需要拆三個(gè)零件。我見(jiàn)過(guò)太多項(xiàng)目一開(kāi)始圖省事零件名、步驟順序全寫(xiě)在代碼里結(jié)果后期加模型、調(diào)流程一個(gè)Bug改完另一個(gè)Bug又出來(lái)。配置表化雖然前期多寫(xiě)一點(diǎn)代碼但后面維護(hù)、擴(kuò)展、換設(shè)備型號(hào)時(shí)收益是成倍的。容差和手感的調(diào)校比功能實(shí)現(xiàn)更花時(shí)間。不要指望一套參數(shù)走天下。不同大小的零件、不同操作習(xí)慣的用戶(hù)手感差別很大最好把容差、吸附幀數(shù)、相機(jī)跟隨速度都做成可配置項(xiàng)讓現(xiàn)場(chǎng)調(diào)試時(shí)能直接調(diào)不用改代碼重新打包。拆裝系統(tǒng)在Unity里實(shí)現(xiàn)的難度不在功能而在流程控制和數(shù)據(jù)組織的取舍。想清楚要解決什么問(wèn)題、數(shù)據(jù)怎么配置、邏輯和表現(xiàn)怎么分離再用文章里的框架去搭基本能少走一半彎路。如果后續(xù)要做考核評(píng)分、多人協(xié)同、VR頭顯適配只要分層合理都是在這套骨架上加肉的事。