Tuesday, October 7, 2025

Level Map với các “Level Nodes” trong Unity (mobile portrait)

Đây là quy trình để bạn có thể tạo ra được một Level Map với cái Node trong Unity từ những hình ảnh cần chuấn bị cho tới code để làm việc trong Unity :)

1) Checklist hình ảnh cần chuẩn bị

Tỷ lệ & xuất file

  • Kích thước gốc: 2× mật độ hiển thị (ví dụ 2160×3840 cho app 1080×1920) để sắc nét.

  • PNG 32-bit, nền trong suốt (trừ background).

  • Giữ đường viền dàymảng màu phẳng + gradient nhẹ; tránh noise để nén tốt.

Bộ art cơ bản

  1. Background Map (to bản): rừng/sa mạc/băng tuyết… có đường mòn gợi hướng đi. Có thể tách tầng parallax:

    • Sky / Distant Mountains

    • Mid-ground (rừng, đá, sông)

    • Foreground (bụi cỏ, bảng gỗ)

  2. Paths: dải đường uốn lượn (1–2 biến thể) hoặc để vẽ spline trong Unity.

  3. Level Node Base (khung nút): hình tròn/biểu tượng đá/gỗ với outline dày.

  4. State Overlays cho node:

    • Locked (ổ khóa, xích)

    • Available (sáng nhẹ)

    • Completed (viền vàng, vương miện nhỏ)

    • Stars (0–3 ngôi sao tách riêng)

    • Number Plate (biển ghi số level)

  5. POI/Decor (trang trí): bảng gỗ “World 1”, cọc tiêu, nấm, cờ, lồng đèn…

  6. UI nhẹ: nút Back, Header, Currency, nút World Switch (mũi tên), panel Info.

  7. Hiệu ứng (nếu có): sprite glow quanh node, sparkle (hạt sáng), tuyến đường sáng khi mở khóa.

Gợi ý style Mystic Realms: màu bão hòa – outline dày – đổ bóng mềm; ưu tiên solid fills + gradient mượt, tránh texture nhiễu.


2) Dữ liệu & Prefab (Unity)

2.1 ScriptableObject cho Level

using UnityEngine; [CreateAssetMenu(menuName="Game/Level Data")] public class LevelData : ScriptableObject { public int levelIndex; // 1,2,3... public bool unlockedByDefault; public Vector2 mapPosition; // toạ độ trong không gian map (Canvas/World) public Sprite iconOverride; // nếu muốn node có icon riêng public int starsRequiredToUnlock; // mở nhánh }

2.2 Trạng thái người chơi

[System.Serializable] public class LevelProgress { public int bestStars; // 0-3 public bool unlocked; } public static class SaveSystem { public static void SaveLevel(int idx, int stars) { int best = PlayerPrefs.GetInt($"lvl_{idx}_stars", 0); if (stars > best) PlayerPrefs.SetInt($"lvl_{idx}_stars", stars); PlayerPrefs.SetInt($"lvl_{idx}_unlocked", 1); } public static LevelProgress LoadLevel(int idx) { return new LevelProgress { bestStars = PlayerPrefs.GetInt($"lvl_{idx}_stars", 0), unlocked = PlayerPrefs.GetInt($"lvl_{idx}_unlocked", 0) == 1 }; } }

2.3 Prefab LevelNode

  • RootButton (UI) + Image nền node.

  • Con: Image LockedImage CompletedStarsGroup (3 icon), TextMeshProUGUI số level, Glow (tắt/bật).

  • Animator (tuỳ chọn): state “Idle”, “Pulse”, “Unlock”.

  • Script:

using UnityEngine; using UnityEngine.UI; using TMPro; using UnityEngine.Events; public class LevelNode : MonoBehaviour { public LevelData data; public Image baseImage, lockImage, completeImage; public Transform starsGroup; public TextMeshProUGUI numberText; public UnityEvent<LevelData> onClick; LevelProgress progress; void Start() { ApplyData(); GetComponent<Button>().onClick.AddListener(()=> onClick?.Invoke(data)); } public void ApplyData() { numberText.text = data.levelIndex.ToString(); progress = SaveSystem.LoadLevel(data.levelIndex); bool unlocked = progress.unlocked || data.unlockedByDefault; lockImage.gameObject.SetActive(!unlocked); completeImage.gameObject.SetActive(progress.bestStars > 0); for (int i=0;i<starsGroup.childCount;i++) starsGroup.GetChild(i).gameObject.SetActive(i < progress.bestStars); } }

2.4 Prefab WorldMap

  • Canvas (Screen Space – Camera) → Panel MapViewport (Mask/Safe Area) → MapContent (nhóm tất cả layer nền + nodes).

  • Gắn script kéo/thả/zoom vào MapContent.

  • Nodes được Instantiate từ list LevelData:

using UnityEngine; public class WorldMap : MonoBehaviour { public RectTransform mapContent; public LevelNode nodePrefab; public LevelData[] levels; void Start() { foreach (var lvl in levels) { var node = Instantiate(nodePrefab, mapContent); node.data = lvl; node.GetComponent<RectTransform>().anchoredPosition = lvl.mapPosition; node.onClick.AddListener(OpenLevelPopup); } } void OpenLevelPopup(LevelData data) { // mở panel thông tin, nút Play, hiển thị điều kiện mở khoá, reward, v.v. } }

3) Thiết lập Scene & Tương tác

3.1 Kéo/Phóng to (Pan/Zoom) cho Map (mobile-friendly)

  • Dùng RectTransform (UI) để dễ mask & safe area.

  • Xử lý drag/two-finger pinch:

using UnityEngine; using UnityEngine.EventSystems; public class MapPanZoom : MonoBehaviour, IDragHandler, IScrollHandler { public RectTransform content; public RectTransform viewport; public float dragFactor = 1f; public Vector2 zoomRange = new Vector2(0.8f, 1.8f); public float zoomSpeed = 0.1f; float scale = 1f; public void OnDrag(PointerEventData e) { content.anchoredPosition += e.delta * dragFactor; ClampToViewport(); } public void OnScroll(PointerEventData e) { // mouse wheel (Editor); trên mobile dùng Input.touches scale = Mathf.Clamp(scale + e.scrollDelta.y * zoomSpeed * 0.1f, zoomRange.x, zoomRange.y); content.localScale = Vector3.one * scale; ClampToViewport(); } void ClampToViewport() { // Giữ content không trôi khỏi viền (tính theo kích thước đã scale) Vector2 size = content.rect.size * scale; Vector2 view = viewport.rect.size; Vector2 min = view - size; Vector2 pos = content.anchoredPosition; pos.x = Mathf.Clamp(pos.x, min.x * 0.5f, -min.x * 0.5f); pos.y = Mathf.Clamp(pos.y, min.y * 0.5f, -min.y * 0.5f); content.anchoredPosition = pos; } }

Muốn tween mượt đến node (khi chọn), dùng DOTween: tween anchoredPosition & localScale của MapContent.

3.2 Spline Path (tùy chọn)

  • Dùng LineRenderer + một spline (plugin miễn phí hoặc Unity Spline) để vẽ đường nối các node đã mở khóa.

  • Màu đường sáng – outline dày, có hiệu ứng dot chạy (shader/texture UV scroll) để gợi mở.

3.3 Parallax 2D (nhẹ)

  • Đặt các layer BG/FG là con của MapContent, thêm script dịch chuyển tỷ lệ thấp hơn khi kéo để tạo chiều sâu.

3.4 Raycast & UI

  • Map là UI → bảo đảm có EventSystem + GraphicRaycaster.

  • Node dùng Button để nhận click. Ưu tiên hit area lớn (Padding 24–32px).

3.5 Popup Level Info

  • Panel bán trong suốt, hiển thị: tên/ảnh level, StarsRewardsĐiều kiện yêu cầu, nút Play.

  • Dùng Animator hoặc DOTween cho mở/đóng (scale + fade).


4) Lưu ý quan trọng (đừng bỏ qua)

Hiển thị & tỉ lệ

  • Thiết kế cho 1080×1920 (portrait). Bật Canvas Scaler = Scale With Screen Size, Reference 1080×1920, Match 0.5.

  • Chừa safe area (tai thỏ, thanh điều hướng). Có thể dùng SafeArea script đặt MapViewport theo insets.

Import Settings (Sprite)

  • Texture Type: Sprite (2D and UI); Mesh Type: Tight.

  • Pixels Per Unit: giữ thống nhất (100–200).

  • Filter Mode: Bilinear (giữ gradient mượt); Compression: High Quality hoặc None cho icon nhỏ có viền sắc.

  • Gom vào Sprite Atlas để giảm draw calls.

Sorting/Layers

  • Nếu dùng Canvas: sắp xếp bằng Hierarchy. Nếu dùng 2D Renderer: Sorting Layers = BG / Path / Nodes / UI FX.

  • FX (sparkle) đặt lên layer trên Node để không bị che.

Trạng thái node rõ ràng

  • Locked: xám + ổ khóa + tắt sparkle.

  • Available (next): sáng nhẹ + pulse chậm (1.1x scale, 0.8–1s).

  • Completed: viền vàng + 1–3 sao.

  • Current: highlight mạnh nhất + chỉ báo (mũi tên, vòng sáng xoay).

Dòng chảy mở khóa (progression)

  • Đừng mở quá nhiều nhánh sớm. Luôn hiển thị điều kiện (ví dụ: “Cần 10 sao để mở khu Rừng 2”).

  • Gợi ý đường đi bằng đường mòn sáng dần đến node tiếp theo.

Hiệu năng

  • Dùng pool cho sparkle/hạt.

  • Parallax đơn giản bằng script, hạn chế nhiều Animator cùng lúc.

  • Cắt map thành vài chunk nếu quá nặng; chỉ bật chunk nằm trong viewport.

Âm thanh & haptics

  • Tap node: “pop” ngắn.

  • Unlock: “shing” + rung nhẹ (Haptic Medium).

  • Mở World mới: nhạc fanfare ngắn.

Quản lý dữ liệu

  • Tách LevelData theo “World” (ScriptableObject list), dễ chuyển giữa các bản đồ.

  • Lưu bestStars & unlocked bằng PlayerPrefs hoặc một SaveManager (JSON).

QA checklist nhanh

  • Node tap area đủ lớn? (≥ 96×96 dp)

  • Kéo map có chặn biên? Zoom có giới hạn?

  • Popup đóng/mở mượt, không che safe area?

  • Text/outline sắc nét trên mọi DPI?

  • Đường dẫn/nhánh mở khoá hiển thị đúng logic?


Gợi ý quy trình dựng nhanh (30–60 phút)

  1. Tạo Canvas (Screen Space – Camera), thêm MapViewport (Mask + SafeArea).

  2. Đặt MapContent, import BG + layers + path sprite, scale map khoảng 1.2×.

  3. Tạo prefab LevelNode chuẩn (Button + Images + TMP text).

  4. Tạo 10–20 LevelData (điền mapPosition theo layout).

  5. Viết WorldMap sinh node, gắn MapPanZoom.

  6. Làm LevelInfoPopup + liên kết từ onClick.

  7. Thêm hiệu ứng nhỏ: pulse cho node current, sparkle nhẹ cho available.

  8. Test trên 3 tỷ lệ: 1080×1920, 720×1600, 1440×3040.

No comments: