Files
mp-xieyingeng/ARCHITECTURE.md
2026-04-05 13:37:58 +08:00

497 lines
26 KiB
Markdown

# Architecture Overview
## System Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ COCOS CREATOR GAME │
│ WeChat Mini-Game Version 3.8.8 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (UI) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PageLoading.ts ──────┐ │
│ (Loading Screen) │ │
│ │ │
│ ┌──────────┐ │ ┌──────────┐ ┌──────────┐ │
│ │PageHome │─────────┴──────│PageLevel │──────│PassModal │ │
│ │(Menu) │ │ (Play) │ │(Victory) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ │ │
│ │ │ │ │
│ └────────────────┬───────────┘ │ │
│ │ (Back button) │ │
│ └─────────────────────────────┘ │
│ │
│ Toast.ts ────────────────────────────────────────────────────────────│
│ (Notifications) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ CORE LOGIC LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ PageLevel.ts │ │ PassModal.ts │ │
│ ├──────────────────────┤ ├──────────────────────┤ │
│ │ • Answer validation │ │ • Next level │ │
│ │ • Hint unlock (-life)│ │ • Share button │ │
│ │ • Countdown (60s) │ │ • Callbacks │ │
│ │ • Sound/Vibration │ │ │ │
│ │ • Life display │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ └────────────────┬───────────────┘ │
│ │ │
│ ┌──────────────────────────┴──────────────────────────────────┐ │
│ │ ViewManager.ts (View Navigation) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ • Page registration & caching │ │
│ │ • Page stack management (push/pop/replace) │ │
│ │ • Lifecycle: onViewLoad → onViewShow → onViewHide → destroy │ │
│ │ • Parameter passing between pages │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ BaseView.ts (Abstract base class for all pages) │
│ • Lifecycle hooks │
│ • Page state management │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ DATA & STATE MANAGEMENT LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ StorageManager.ts (Single Source of Truth) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ Lives Management: │ │
│ │ • getLives() / setLives() / consumeLife() / addLife() │ │
│ │ │ │
│ │ Progress Management: │ │
│ │ • getCurrentLevelIndex() / getMaxUnlockedLevelIndex() │ │
│ │ • onLevelCompleted(levelIndex) │ │
│ │ • isLevelUnlocked(levelIndex) │ │
│ │ │ │
│ │ Storage Backend: sys.localStorage │ │
│ │ └─ game_lives: string number (default: "10") │ │
│ │ └─ game_progress: JSON with currentIndex & maxUnlockedIdx │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ LevelDataManager.ts (Level Data & Assets) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ • initialize(onProgress) - Fetch API & load first level │ │
│ │ • getLevelConfig(index) - Get level data + image │ │
│ │ • ensureLevelReady(index) - On-demand level loading │ │
│ │ • preloadNextLevel(currentIndex) - Async preload │ │
│ │ │ │
│ │ Memory Caches: │ │
│ │ • _apiData: All levels from server │ │
│ │ • _levelConfigs: Map of loaded level configs │ │
│ │ • _imageCache: Map of loaded images (SpriteFrames) │ │
│ │ • _loadingLevels: Set of levels being loaded │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ UTILITY & SDK LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────┐ ┌────────────────────┐ ┌──────────────────┐ │
│ │ HttpUtil.ts │ │ WxSDK.ts │ │ToastManager.ts │ │
│ ├────────────────────┤ ├────────────────────┤ ├──────────────────┤ │
│ │ • get<T>() │ │ • isWechat() │ │ • init() │ │
│ │ • post<T>() │ │ • initShare() │ │ • show() │ │
│ │ │ │ • shareAppMessage()│ │ │ │
│ │ Timeout: 10s │ │ • onShareAppMsg() │ │ Display duration │ │
│ │ Error handling │ │ • vibrateShort() │ │ Fade out anim │ │
│ │ │ │ • vibrateLong() │ │ │ │
│ └────────────────────┘ └────────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL SYSTEMS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────┐ │
│ │ WeChat Mini-Game SDK (wx global) │ │
│ ├──────────────────────────────────────┤ │
│ │ • wx.shareAppMessage() │ │
│ │ • wx.onShareAppMessage() │ │
│ │ • wx.showShareMenu() │ │
│ │ • wx.vibrateShort() │ │
│ │ • wx.vibrateLong() │ │
│ └──────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ │ │
│ ┌──────────────────────────────────────┐ │
│ │ Backend API Server │ │
│ │ https://ilookai.cn │ │
│ ├──────────────────────────────────────┤ │
│ │ GET /api/v1/wechat-game/levels │ │
│ │ • Returns: {success, data, message} │ │
│ │ • Retry: 2x with 1s delay │ │
│ │ • Timeout: 8s │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Data Flow Diagram: Complete User Session
```
START
├─ [main.ts] onLoad()
│ └─ ViewManager.init(canvas)
│ └─ ToastManager.init(toastPrefab)
│ └─ Register PageHome & PageLevel
├─ [PageLoading.ts] start()
│ ├─ LevelDataManager.initialize()
│ │ ├─ HttpUtil.get() → API call
│ │ │ └─ https://ilookai.cn/api/v1/wechat-game/levels
│ │ ├─ Cache response → _apiData
│ │ ├─ Load first level image
│ │ │ └─ assetManager.loadRemote() → SpriteFrame
│ │ │ └─ Cache in _imageCache
│ │ └─ Return success
│ │
│ └─ ViewManager.preload('PageHome')
│ └─ ViewManager.open('PageHome')
│ └─ PageHome.onViewLoad()
│ └─ WxSDK.initShare()
├─ [USER CLICKS "START GAME"]
├─ ViewManager.open('PageLevel', {params: {levelIndex: 0}})
│ └─ PageLevel.onViewLoad()
│ ├─ StorageManager.getCurrentLevelIndex()
│ ├─ PageLevel.initLevel()
│ │ ├─ LevelDataManager.ensureLevelReady(levelIndex)
│ │ │ ├─ Check cache (_levelConfigs)
│ │ │ ├─ If not cached: load image + create config
│ │ │ └─ Store in cache
│ │ │
│ │ └─ PageLevel._applyLevelConfig()
│ │ ├─ Display main image (sprite)
│ │ ├─ Show Hint 1 (free)
│ │ ├─ Hide Hints 2, 3 (show unlock buttons)
│ │ ├─ Create input field based on answer.length
│ │ ├─ Display lives: StorageManager.getLives()
│ │ ├─ Start countdown (60s)
│ │ └─ LevelDataManager.preloadNextLevel() [async]
├─ [USER INTERACTS]
├─ Option A: UNLOCK HINT
│ ├─ PageLevel.onUnlockClue(2 or 3)
│ ├─ Check: StorageManager.hasLives()?
│ ├─ Yes → StorageManager.consumeLife() → display hint
│ └─ No → Show "生命值不足" message
├─ Option B: SUBMIT ANSWER
│ ├─ PageLevel.onSubmitAnswer()
│ ├─ Validate: userAnswer === correctAnswer?
│ │
│ ├─ YES (Correct)
│ │ ├─ PageLevel.showSuccess()
│ │ ├─ Stop countdown
│ │ ├─ Play successAudio
│ │ ├─ StorageManager.addLife() [+1]
│ │ ├─ PageLevel._showPassModal()
│ │ │ ├─ Instantiate PassModal prefab
│ │ │ ├─ PassModal.onViewLoad()
│ │ │ ├─ PassModal.onViewShow()
│ │ │ │ └─ Play success sound
│ │ │ │ └─ Adjust widget to full screen
│ │ │ │
│ │ │ └─ User clicks button:
│ │ │ ├─ "Next Level" → PageLevel.nextLevel()
│ │ │ │ ├─ StorageManager.onLevelCompleted(currentIndex)
│ │ │ │ │ ├─ currentLevelIndex++
│ │ │ │ │ ├─ maxUnlockedLevelIndex = max(prev, current)
│ │ │ │ │ └─ Save to localStorage
│ │ │ │ ├─ Reload PageLevel with new index
│ │ │ │ │ └─ Repeat from Level Setup
│ │ │ │ │
│ │ │ │ └─ If last level:
│ │ │ │ └─ ViewManager.back() → return to PageHome
│ │ │ │
│ │ │ └─ "Share" → WxSDK.shareAppMessage()
│ │ │ └─ query: `level=${currentIndex+1}`
│ │
│ ├─ NO (Wrong)
│ │ ├─ PageLevel.showError()
│ │ ├─ Play failAudio
│ │ ├─ WxSDK.vibrateLong() [device vibration]
│ │ ├─ ToastManager.show("答案错误,再试试吧!")
│ │ │ ├─ Create Toast node
│ │ │ ├─ Show for 2000ms
│ │ │ ├─ Fade out animation (300ms)
│ │ │ └─ Destroy node
│ │ │
│ │ └─ Continue playing (allow retry)
├─ Option C: TIMEOUT
│ └─ Countdown reaches 0
│ ├─ PageLevel.onTimeUp()
│ ├─ Play failAudio
│ └─ [Incomplete: can still submit after timeout]
├─ [USER CLICKS BACK BUTTON]
│ ├─ ViewManager.back()
│ │ ├─ PageLevel._doHide()
│ │ └─ PageHome._doShow()
│ │
│ └─ Return to home (progress saved in localStorage)
└─ END
```
---
## State Management Flow
```
USER PROGRESS STATE
├─ localStorage: game_progress
│ └─ {
│ "currentLevelIndex": 5,
│ "maxUnlockedLevelIndex": 5
│ }
├─ Memory Cache (LevelDataManager)
│ ├─ _apiData: ApiLevelData[] (all levels)
│ ├─ _levelConfigs: Map<levelIndex, RuntimeLevelConfig>
│ ├─ _imageCache: Map<url, SpriteFrame>
│ └─ _loadingLevels: Set<levelIndex> (currently loading)
└─ Session State (PageLevel component)
├─ currentLevelIndex: number
├─ _currentConfig: RuntimeLevelConfig
├─ _countdown: number (60 → 0)
├─ _isTimeUp: boolean
├─ _isTransitioning: boolean
└─ _passModalNode: Node | null
LIVES STATE
├─ localStorage: game_lives
│ └─ "10" (string representation of number)
├─ Cached value (StorageManager._progressCache)
│ └─ Read on first access, cached for performance
└─ Operations (immediate storage update)
├─ getLives() → read from storage
├─ setLives(n) → validate & write to storage
├─ consumeLife() → getLives() - 1, setLives()
├─ addLife() → getLives() + 1, setLives()
└─ hasLives() → getLives() > 0
API CACHE
├─ First Load (Startup)
│ ├─ HttpUtil.get(apiUrl, 8000ms timeout)
│ ├─ Retry: 2 times (1s delay between)
│ └─ Cache in LevelDataManager._apiData
└─ Per-Level Resources (On-Demand)
├─ Check _levelConfigs cache
├─ If missing: assetManager.loadRemote(imageUrl)
├─ Create SpriteFrame
└─ Cache in _imageCache
```
---
## View Stack & Navigation
```
VIEW STACK (LIFO - Last In First Out)
├─ Level 0 (Bottom)
│ └─ PageHome
├─ Level 1 (Middle)
│ └─ PageLevel
├─ Level 2 (Top - Current)
│ └─ PassModal (temporary, on PageLevel)
└─ Operations
├─ open(viewId) → push to stack + show
├─ back() → pop from stack + hide + show prev
├─ replace(viewId) → pop + push new view
└─ close() → pop + show previous
CACHING BEHAVIOR
├─ PageHome
│ ├─ cache: true
│ └─ Cached after first open, reused on back
├─ PageLevel
│ ├─ cache: true
│ └─ Cached per instance (but reinitializes on each open)
└─ PassModal
├─ Dynamically instantiated
├─ Not cached
└─ Destroyed after close
```
---
## Class Hierarchy
```
Component (Cocos)
├─ BaseView (Abstract base)
│ ├─ PageHome
│ ├─ PageLevel
│ └─ PassModal
└─ Toast
Managers (Singleton)
├─ ViewManager
│ └─ Manages page lifecycle & navigation
├─ LevelDataManager
│ └─ Manages API data & asset loading
├─ StorageManager
│ └─ Manages user data persistence
├─ ToastManager
│ └─ Manages toast notifications
└─ WxSDK
└─ Manages WeChat integration
Utilities
├─ HttpUtil
│ └─ Static HTTP methods (GET/POST)
└─ LevelTypes
└─ TypeScript interfaces for API data
```
---
## Dependency Graph
```
main.ts
├─ ViewManager
├─ ToastManager
└─ [Page Prefabs]
├─ PageHome
│ └─ WxSDK
├─ PageLevel
│ ├─ StorageManager
│ ├─ WxSDK
│ ├─ LevelDataManager
│ ├─ ToastManager
│ └─ PassModal
│ ├─ WxSDK
│ └─ BaseView
└─ PageLoading
├─ ViewManager
├─ LevelDataManager
│ ├─ HttpUtil
│ │ └─ XMLHttpRequest
│ ├─ LevelTypes
│ └─ Cocos assetManager
└─ ToastManager
EXTERNAL APIS
├─ https://ilookai.cn/api/v1/wechat-game/levels
│ └─ Called by LevelDataManager._fetchApiData()
└─ WeChat SDK (wx global)
├─ wx.shareAppMessage()
├─ wx.onShareAppMessage()
├─ wx.showShareMenu()
├─ wx.vibrateShort()
└─ wx.vibrateLong()
```
---
## Performance Considerations
### Memory Management
- **Image Loading**: Remote images loaded on-demand with local caching
- **Level Configs**: Loaded incrementally, current + next level only
- **View Caching**: Pages cached after first load, reused on navigation back
- **Message Queuing**: No event queue; direct method calls
### Network
- **Single API Call**: Startup only (all levels fetched at once)
- **Timeout**: 8 seconds for API requests
- **Retry Logic**: 2 attempts with 1-second delay
- **Cache Strategy**: In-memory cache, no expiration
### Storage
- **localStorage**: 2 keys (lives + progress)
- **No Batching**: Each update writes immediately
- **Synchronous**: No async operations needed
### UI/Graphics
- **Single Image**: Main puzzle image per level
- **Dynamic Input**: Input field size adjusts based on answer length
- **Audio**: One-shot playback, no loops
- **Animations**: Toast fade-out only
---
## Security Considerations
### Current Implementation
- ⚠️ All data stored locally (no encryption)
- ⚠️ No user authentication
- ⚠️ No server-side validation
- ⚠️ Progress can be manually edited via localStorage
### Vulnerabilities
1. localStorage can be inspected/modified via browser console
2. No server-side checks on progress/lives
3. Can modify localStorage to skip levels
4. Can modify lives directly
### Improvements Needed
- Server-side progress validation
- User authentication
- Encrypted storage
- Server-side truth for lives/progress