- 进入关卡成功后显示 toast 提示消耗体力及剩余体力 - 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题 - WxSDK.getWx() 改为 static 公开方法,便于外部调用
26 KiB
26 KiB
Architecture & Flow Diagrams
System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Cocos Creator Runtime │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ UI Layer (This Analysis) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌────────────────┐ ┌──────────────┐ │ │
│ │ │ main.ts │────────▶│ ViewManager │ │ │
│ │ │ (Bootstrap) │ │ (Singleton) │ │ │
│ │ └────────────────┘ └──────────────┘ │ │
│ │ │ │ │
│ │ Pages (All extend BaseView): │ │ │
│ │ ┌─────────────────────────────────┼──────────────┐ │ │
│ │ │ • PageLoading (init data) │ │ │ │
│ │ │ • PageHome (hub) │ │ │ │
│ │ │ • PageLevel (game - complex) ◀┘ │ │ │
│ │ │ • PagePreviewLevels (list) │ │ │
│ │ │ • PassModal (modal) │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────┐ ┌──────────────┐ │ │
│ │ │ ToastManager │────────▶│ Toast │ │ │
│ │ │ (Singleton) │ │ (Component) │ │ │
│ │ └────────────────┘ └──────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Data Layer (External - Not Analyzed) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ • AuthManager • LevelDataManager │ │
│ │ • StorageManager • UserAssetsManager │ │
│ │ • ShareManager • WxSDK │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Page State Machine
┌─────────────────────────────┐
│ Application Starts │
└──────────────┬──────────────┘
│
▼
┌──────────────────────┐
│ main.onLoad() │
│ Registers all pages │
└──────────┬───────────┘
│
▼
┌──────────────────────────────┐
│ PageLoading displayed │
│ - Load auth + levels │
│ - Sync progress │
│ - Check share code │
└──────────────┬───────────────┘
│
┌──────────────┴──────────────┐
│ Normal Mode │ Share Mode │
▼ ▼ │
┌──────────────────┐ ┌───────────────┐ │
│ PageHome │ │ PageLevel │ │
│ [Start] [PK] │ │ (in share) │ │
└────┬─────┬───────┘ └───────┬───────┘ │
│ │ │ │
┌────────▼─┐ └────────────┬──────┴────────┘
│ │ │
▼ ▼ ▼
PageLevel PageWrite (Repeat levels)
(game) Levels │
│ (create) ▼
│ │ PassModal
│ ▼ [Share]
│ PagePreview │
│ Levels ▼
│ (verify) Back to Home
│ │
│ ▼
│ PassModal
│ [Next/Share]
│ │
└──────────┼─────────┐
│ │
┌────▼──┐ ┌───▼────┐
│ Next │ │ Share │
│ Loop │ │ to WX │
└───────┘ └────────┘
Page Stack Visualization
Session Trace
INITIALIZATION:
┌────────────────────────────────────────────┐
│ Stack: [] │
│ (Nothing loaded yet) │
└────────────────────────────────────────────┘
│
▼ main.onLoad()
┌────────────────────────────────────────────┐
│ Stack: [] │
│ ViewManager + pages registered in registry │
└────────────────────────────────────────────┘
│
▼ PageLoading.start() → ViewManager.open('PageHome')
┌────────────────────────────────────────────┐
│ Stack: [PageHome] │
│ PageHome._doShow() called │
└────────────────────────────────────────────┘
USER CLICKS "START GAME":
▼ PageHome._onStartGameClick() → ViewManager.open('PageLevel')
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel] │
│ PageHome._doHide() called │
│ PageLevel._doShow() called │
└────────────────────────────────────────────┘
COMPLETES LEVEL:
▼ PageLevel shows PassModal (not in stack, drawn on top)
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel] │
│ UI Layer: [PageLevel, PassModal@z999] │
│ PassModal shown with instantiate() │
└────────────────────────────────────────────┘
CLICKS "NEXT LEVEL":
▼ PassModal closed, PageLevel.nextLevel()
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel] │
│ PageLevel loads next level config │
│ PassModal destroyed │
└────────────────────────────────────────────┘
CLICKS BACK FROM PageLevel:
▼ PageLevel.onIconSettingClick() → ViewManager.back()
┌────────────────────────────────────────────┐
│ Stack: [PageHome] │
│ PageLevel._doHide() called │
│ PageLevel destroyed (cache=true, not kept) │
│ PageHome._doShow() called │
└────────────────────────────────────────────┘
Component Communication Diagram
┌─────────────────────┐
│ ViewManager │
│ (Singleton) │
└──────────┬──────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
open() back() replace()
│ │ │
│ │ │
┌───────────┴────┐ ┌───┴────┐ ┌──┴───────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
PageHome PageLevel PassModal (back) (new page)
│ │ │
│ ├─ Timer ────┐
│ │ │
│ ├─ Points │
│ │ ↑ │
│ │ │ │
▼ ▼ │ ▼
ToastMgr Hints │ PassModal
│ │ │
├──────┤ └──▶ Share to WX
│ │
Unlock Earn Points
Consume (UserAssets)
(check hasPoints)
Data Flow: Level Completion
USER ENTERS ANSWER:
┌─────────────────────────────────────────────────────────┐
│ PageLevel.onSubmitAnswer() │
└─────────────────────────────────────────────────────────┘
│
├─ getAnswer() ──▶ [Retrieve from EditBox]
│
├─ Compare with _currentConfig.answer
│
▼
┌────────────────────────────────────────┐
│ CORRECT ANSWER │
└────────────────────────────────────────┘
│
├─ _isTransitioning = true [Prevent double-submit]
│
├─ stopCountdown()
│
├─ playSuccessSound()
│
├─ Calculate: timeSpent = 60 - _countdown
│
├─┐ Normal Mode:
│ │
│ └─▶ UserAssetsManager.earnPoint(levelId, timeSpent)
│ │
│ ▼
│ └─ UPDATE SERVER + LOCAL
│ └─ Get points amount
│ └─ PageLevel.updatePointsLabel()
│
├─┐ Share Mode:
│ │
│ └─▶ ShareManager.reportLevelProgress(...)
│ │
│ ▼
│ └─ SEND TO SERVER (fire-and-forget)
│
├─ _showPassModal()
│ │
│ ├─ instantiate(passModalPrefab)
│ │
│ ├─ Set z-index = 999 (topmost)
│ │
│ ├─ Add to Canvas
│ │
│ └─ Call passModal.onViewLoad() + onViewShow()
│
└─ WAIT FOR USER:
│
├─ [Next Level] ──▶ PassModal closed
│ │
│ ▼
│ nextLevel()
│ │
│ ┌────────┴────────┐
│ │ No More Levels │ Next Level
│ ▼ ▼
│ BACK HOME Load & Display
│ Restart Timer
│
└─ [Share] ──▶ WxSDK.shareAppMessage()
│
└─ Modal stays open
Hint Unlocking Flow
USER CLICKS UNLOCK BUTTON:
┌──────────────────────────────────────────────┐
│ PageLevel.onUnlockClue(index: 2 or 3) │
└──────────────────────────────────────────────┘
│
├─ Check: _isUnlocking == true?
│ └─ YES: Return immediately (prevent double-click)
│ └─ NO: Continue
│
├─ Check: StorageManager.hasPoints()?
│ └─ NO: Show toast "积分不足!"
│ └─ YES: Continue
│
├─ Set: _isUnlocking = true
│
├─ await UserAssetsManager.consumePoint(levelId, index)
│ │
│ ├─ ASYNC: Contact server/storage
│ │
│ └─ Returns: success boolean
│
├─ If success:
│ │
│ ├─ updatePointsLabel()
│ │
│ ├─ playClickSound()
│ │
│ ├─ hideUnlockButton(index)
│ │
│ ├─ showClue(index)
│ │
│ └─ setClue(index, config.clueN)
│
├─ If failed:
│ │
│ └─ Show toast "积分不足!"
│
└─ Finally: _isUnlocking = false
Timer Sequence Diagram
Timeline: 1s 2s 3s ... 59s 60s 61s
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
countdown: 60 59 58 1 0 (ended)
│ │ │ │ │
display: "60s" "59s" "58s" ......... "1s" "0s"
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
LEVEL LOAD:
│
└─ startCountdown()
│
├─ _countdown = 60
├─ _isTimeUp = false
└─ schedule(onCountdownTick, 1.0) [Repeat every 1 sec]
│
▼ (Every 1 second)
onCountdownTick()
│
├─ if (_isTimeUp) return
│
├─ _countdown-- (60→59→58...→0)
│
├─ updateClockLabel() [Display: "59s"]
│
└─ if (_countdown <= 0):
│
├─ _isTimeUp = true
│
├─ stopCountdown()
│ └─ unschedule(onCountdownTick)
│
└─ onTimeUp()
│
└─ playFailSound()
ANSWER SUBMITTED (Correct):
│
└─ showSuccess()
│
├─ stopCountdown()
│ └─ Timer stops, no more ticks
│
└─ _showPassModal()
OR:
ANSWER SUBMITTED (Wrong):
│
└─ Timer continues running...
│
└─ User keeps playing
Input Box Creation & Management
LEVEL LOADS:
│
└─ _applyLevelConfig(config)
│
└─ createSingleInput(config.answer.length)
│
├─ clearInputNodes()
│ │
│ └─ Destroy old input nodes
│ (If level changed)
│
├─ Hide inputTemplate
│
├─ instantiate(inputTemplate)
│
├─ Set properties:
│ │
│ ├─ active = true
│ ├─ name = 'singleInput'
│ ├─ position = (0, 0, 0)
│ │
│ └─ Get EditBox component
│ │
│ ├─ placeholder = "(Xlength个字)"
│ ├─ maxLength = answerLength
│ ├─ string = ""
│ │
│ └─ Listen events:
│ ├─ TEXT_CHANGED → onInputTextChanged()
│ └─ EDITING_DID_ENDED → onInputEditingEnded()
│
├─ Calculate width:
│ └─ Math.min(600, Math.max(200, length*60+40))
│
├─ Set UITransform contentSize to width x 100
│
├─ Adjust underline width to match
│
├─ Add to inputLayout
│
└─ Store in _inputNodes[0]
USER ENTERS TEXT:
│
├─ EditBox updates string property
└─ TEXT_CHANGED event fires
USER SUBMITS:
│
├─ getAnswer()
│ │
│ └─ _inputNodes[0].getComponent(EditBox).string.trim()
│
└─ Compare with config.answer
USER NAVIGATES AWAY:
│
└─ onViewDestroy() → clearInputNodes()
│
└─ Destroy all input nodes
Share Challenge Mode Flow
LAUNCH FROM WeChat LINK:
│
└─ PageLoading.start()
│
└─ _startPreload()
│
├─ Initialize auth + levels (parallel)
│
├─ WxSDK.getShareCodeFromLaunch()
│ │
│ └─ Extract from wx.getLaunchOptionsSync().query
│
├─ if shareCode exists AND loginSuccess:
│ │
│ └─ ShareManager.instance.joinShare(shareCode)
│ │
│ ├─ ASYNC: Fetch share session from server
│ │
│ ├─ Get shared level set
│ │
│ └─ Return: success boolean
│
├─ if (joinSuccess):
│ │
│ ├─ Set progress = 1
│ │
│ ├─ ViewManager.open('PageLevel', {
│ │ params: { shareMode: true }
│ │ })
│ │
│ └─ Destroy self (PageLoading)
│
└─ else (normal flow)
│
└─ Preload PageHome → Open PageHome
IN SHARE MODE (PageLevel):
│
├─ onViewLoad():
│ │
│ ├─ params = getParams()
│ ├─ _isShareMode = params?.shareMode === true
│ │
│ ├─ if shareMode:
│ │ └─ currentLevelIndex = 0 (start at first)
│ │
│ └─ initLevel() loads from ShareManager, not LevelDataManager
│
├─ showSuccess():
│ │
│ ├─ if normal mode:
│ │ └─ earn points
│ │
│ └─ if share mode:
│ │
│ └─ ShareManager.reportLevelProgress(levelId, true, timeSpent)
│ │
│ └─ FIRE-AND-FORGET (no await)
│
├─ nextLevel():
│ │
│ ├─ if NOT shareMode:
│ │ └─ StorageManager.onLevelCompleted() [Save progress]
│ │
│ ├─ Check level count:
│ │ │
│ │ ├─ Share: ShareManager.getShareLevelCount()
│ │ └─ Normal: LevelDataManager.getLevelCount()
│ │
│ ├─ if (allLevelsComplete):
│ │ │
│ │ ├─ if shareMode:
│ │ │ │
│ │ │ ├─ ShareManager.clearShareMode()
│ │ │ │
│ │ │ └─ ViewManager.replace('PageHome')
│ │ │
│ │ └─ if normal:
│ │ └─ ViewManager.back()
│ │
│ └─ else (more levels):
│ └─ Load next level
│
└─ Back to PageHome (replaces, clears share state)
ViewManager Caching Mechanism
OPEN 'PageHome' (first time):
│
└─ ViewManager.open('PageHome')
│
├─ Check cache: _viewCache.get('PageHome')
│ └─ Cache miss (undefined)
│
├─ _instantiateView('PageHome', prefab)
│ │
│ ├─ instantiate(prefab)
│ │
│ ├─ Get BaseView component
│ │
│ ├─ Set viewId, config, params
│ │
│ ├─ Add to container
│ │
│ ├─ Call view.onViewLoad() ◄─── Called once
│ │
│ ├─ if (config.cache === true): ◄─── PageHome has cache:true
│ │ └─ _viewCache.set('PageHome', view)
│ │
│ └─ _showView(view)
│ │
│ └─ view._doShow()
│ └─ view.onViewShow() ◄─── Called now
│
└─ Stack: [PageHome]
OPEN 'PageLevel':
│
└─ ViewManager.open('PageLevel')
│
├─ PageHome._doHide()
│ └─ view.onViewHide() ◄─── First hide
│
└─ (Same as above for PageLevel)
│
└─ Stack: [PageHome, PageLevel]
BACK FROM 'PageLevel':
│
└─ ViewManager.back()
│
├─ Pop stack: PageLevel
│
├─ PageLevel._doHide()
│ └─ view.onViewHide()
│
├─ Decide: shouldDestroy?
│ │
│ └─ PageLevel has cache:true
│ └─ shouldDestroy = false (keep cached)
│
├─ Get current: PageHome
│
├─ PageHome._doShow()
│ └─ view.onViewShow() ◄─── Called again
│
└─ Stack: [PageHome]
Caches: {PageHome, PageLevel}
OPEN 'PageLevel' AGAIN (from cache):
│
└─ ViewManager.open('PageLevel')
│
├─ Check cache: _viewCache.get('PageLevel')
│ └─ Cache hit! (PageLevel instance)
│
├─ _showView(cachedView)
│ │
│ ├─ PageHome._doHide()
│ │
│ └─ cachedView._doShow()
│ └─ view.onViewShow() ◄─── Called again (2nd time)
│ onViewLoad() NOT called again ◄─── Important!
│
└─ Stack: [PageHome, PageLevel]
Same instance reused
Error Handling Paths
LEVEL LOADING ERROR:
│
├─ initLevel()
│ │
│ └─ await LevelDataManager.ensureLevelReady(index)
│ │
│ └─ Returns null (error)
│
└─ Log warning, return early
│
└─ UI stays as-is (might be blank)
INSUFFICIENT POINTS:
│
└─ onUnlockClue()
│
├─ StorageManager.hasPoints() → false
│
└─ ToastManager.show("积分不足!")
│
└─ User sees notification, unlock blocked
DOUBLE-SUBMIT PREVENTION:
│
├─ onSubmitAnswer()
│ │
│ └─ if (_isTransitioning) return ◄─── Blocks multiple calls
│
└─ showSuccess()
│
└─ _isTransitioning = true ◄─── Set to block
│
└─ After modal shown/closed
└─ (Would be reset on nextLevel)
DOUBLE-CLICK UNLOCK:
│
└─ onUnlockClue()
│
└─ if (_isUnlocking) return ◄─── Blocks rapid clicks
│
└─ _isUnlocking = true at start
└─ Finally: _isUnlocking = false ◄─── Reset after async