Giới thiệu đề tài
Đề tài của chúng em là “Tìm hiểu xây dựng cấu trúc trò chơi trong Unity”Hướng đến cấu trúc tối ưu dễ mở rộng và bảo trì.
Mục tiêu đề tài
Trong lĩnh vực phát triển game Unity, không có một kiến trúc chương trình thống nhất nào, và điều này thường được thảo luận trên các diễn đàn Kiến trúc game chủ yếu phụ thuộc vào kinh nghiệm của từng lập trình viên Vì vậy, chúng tôi hướng đến việc xây dựng một package khuôn mẫu chung, có khả năng áp dụng linh hoạt cho nhiều loại game khác nhau Tuy nhiên, chúng tôi nhận thức rằng không thể có một giải pháp hoàn hảo cho tất cả vấn đề, điều này giải thích tại sao nhiều kiến trúc khác nhau vẫn tồn tại và chưa bị thay thế hoàn toàn.
Các nghiên cứu liên quan
Nghiên cứu kiến trúc chương trình và kiến trúc game có mối liên hệ chặt chẽ Unity, mặc dù có những ưu điểm và nhược điểm riêng, cho phép lập trình viên linh hoạt áp dụng các kiến trúc khác nhau theo nhu cầu, điều này rất quan trọng trong quá trình phát triển.
Ý nghĩa khoa học & thực tiễn của đề tài
Unity là một công cụ lập trình game hiệu quả, cung cấp nhiều chức năng hữu ích cho các vấn đề thường gặp trong quá trình phát triển game Mặc dù tồn tại một số nhược điểm, nhưng các công cụ hỗ trợ của Unity chắc chắn giúp tăng tốc quá trình prototype và phát triển game.
Hiện nay, chưa có kiến trúc Unity thống nhất và quy trình phát triển game với Unity thiếu tiêu chuẩn rõ ràng, gây ra nhiều khó khăn trong việc phát triển, mở rộng và bảo trì game Mỗi dự án thường phải thiết kế hệ thống riêng từ đầu Do đó, việc áp dụng một kiến trúc đa dụng và linh hoạt sẽ giúp việc sử dụng Unity trong xây dựng game trở nên dễ dàng hơn.
Tất nhiên, sẽ không bao giờ có một giải pháp thật sự giải quyết vấn đề
“thiết kế chương trình” Vì vậy văn bản này cũng sẽ cung cấp một số ý tưởng thiết kế để bạn tự quyết định kiến trúc chương trình của mình.
Tổng quan Unity 3
GameEngine là gì?
Các game engine phổ biến hiện nay
Game engine là phần mềm thiết kế và phát triển video game, hoạt động như một cầu nối giữa các ứng dụng trong hệ thống Có nhiều loại game engine phục vụ cho các nền tảng như console và máy tính cá nhân Chức năng chính của game engine bao gồm kết nối với phần cứng, xử lý đồ họa 2D và 3D, tính toán vật lý, quản lý âm thanh, nâng cấp hình ảnh động, và trí tuệ nhân tạo cho nhân vật Việc sử dụng game engine giúp tiết kiệm thời gian trong quá trình phát triển trò chơi.
Môi trường phát triển game
GameEngine là một bộ công cụ hỗ trợ lập trình viên trong việc phát triển game, với giao diện thân thiện, cấu trúc rõ ràng, và tính năng kéo thả Thay vì phải viết lại mã cho các xử lý vật lý mỗi khi tạo game mới, GameEngine như Unity cung cấp các thành phần như “Rigidbody” và “Rigidbody2D” để xử lý các yếu tố vật lý một cách dễ dàng Nếu bạn muốn mở rộng hoặc tự viết thêm chức năng, GameEngine vẫn cho phép bạn thực hiện điều đó một cách linh hoạt.
Không sao cả, GameEngine sẽ cho bạn viết những script xử lý riêng và dùng tronog game của mình nhưng phải theo quy tắc nhất định của
GameEngine mà bạn sử dụng.
Vậy dùng GameEngine có lợi gì và hại gì? Ưu điểm Nhược điểm
Triển khai nhanh chóng và dễ dàng Phải học cách sử dụng GameEngine đó
Nhiều công cụ hỗ trợ Nhiều lập trình viên dùng công cụ nhưng không hiểu công cụ đó bên dưới hoạt động thế nào
Cấu trúc rành mạch, “hình ảnh hóa”
(Visualize) những dòng code khô khan ngày trước, kéo thả dễ dàng
Do cấu trúc nhìn có vẻ đơn giản nên ít ai chú ý và đầu tư vào 1 hệ thống thật sự để tăng hiệu quả cho game
Unity
Unity là phần mềm phát triển game đa nền tảng, hỗ trợ nhiều hệ điều hành như Android, iOS, Linux, macOS, Windows và Windows Phone Unity3D cung cấp hệ thống toàn diện cho lập trình viên, bao gồm soạn thảo mã nguồn, công cụ tự động hóa và trình sửa lỗi, giúp người dùng dễ dàng sử dụng Ngôn ngữ lập trình chính của Unity là C#, trong khi cốt lõi của nó được xây dựng trên C++, và cũng hỗ trợ Javascript.
Unity tận dụng các thư viện phần mềm như PhysX của Nvidia cho mô phỏng vật lý, OpenGL và Direct3D cho kết xuất hình ảnh 3D, cùng OpenAL cho âm thanh, giúp nâng cao khả năng lập trình game Chương trình được viết bằng Mono có thể chạy trên bất kỳ máy ảo Mono nào, mang lại tính linh hoạt cao cho các nhà phát triển.
Giao diện chính của Unity
Tab Scene của Unity (ở chính giữa) Đây là nơi để bạn thao tác và thiết kế các thành phần cho game của mình
The essence of the system is a list that includes all initial game objects along with their respective components There are various methods to set up the scene system, such as loading a splash scene first to retrieve addressables or asset bundles.
Mỗi GameObject trong Unity đều có một component mặc định chứa thông tin về vị trí, góc xoay và độ phóng to, thường là Transform hoặc RectTransform Bạn có thể tùy ý thêm gameObject con và các component khác dựa trên thiết kế hệ thống và mục tiêu của dự án bạn đang thực hiện.
Trong Unity, ngôn ngữ chính được sử dụng là C#, và để tạo thành component, mã cần được thiết kế sao cho có thể kéo vào GameObject và bao gồm các hàm như Start, Awake, v.v Tuy nhiên, một lỗi phổ biến của những người mới làm quen với Unity là biến mọi thứ thành component chỉ vì tính tiện dụng và cơ chế của Unity.
Rigidbody: Mô phỏng vật lý
Camera: Thu nhận hình ảnh scene lên màn hình
Bạn có thể nhập Atlas chứa nhiều tệp hình vào Unity, miễn là kích thước phải là số mũ của 2, như 1024 hoặc 2048 pixel Điều này giúp Unity nén atlas khi xây dựng, giảm đáng kể kích thước ứng dụng.
Các game làm bằng Unity
Unity hiện nay là game engine phổ biến nhất trên thế giới, được ưa chuộng bởi cả người mới và chuyên gia Một số trò chơi nổi bật được phát triển bằng Unity bao gồm Superhot, Outer Wilds và Hollow Knight.
Các vấn đề trong Unity
Cấu trúc chương trình cơ bản của Unity bao gồm các thành phần GameObject độc lập, không có sự biến đổi đặc biệt Theo phân loại cấu trúc chương trình của Shaw & Garlan, Unity được xem như là 'các quá trình giao tiếp song song'.
Cấu trúc chương trình có nhiều ưu điểm và nhược điểm, nhưng một nguyên tắc quan trọng trong thiết kế kiến trúc chương trình là tính linh hoạt để đáp ứng các yêu cầu khác nhau Nhiều trò chơi không thể hoạt động hiệu quả trong khuôn khổ cấu trúc này, dẫn đến một số vấn đề cần được xem xét kỹ lưỡng.
Trong Unity, lập trình viên không thể xác định thứ tự thực thi của các hàm Update và Start trong MonoBehaviour Mặc dù điều này có lợi trong môi trường đa luồng, nhưng đôi khi cần có những đoạn mã nhất định phải được thực thi trước các đoạn khác, và Unity không cung cấp cơ chế để kiểm soát thứ tự này.
Các thành phần trong Unity được thiết kế độc lập và tách rời, giúp tăng cường khả năng tái sử dụng mã và giảm sự gắn kết Tuy nhiên, điều này cũng làm cho việc tích hợp các thành phần trở nên khó khăn hơn.
Các script của GameObject đều là Plug-and-Play, có thể kéo thả vào
GameObject, nhưng các script này hầu như không được kiểm soát Muốn kiểm soát chúng, phải có một cách để reference các script liên quan.
Vấn đề mẫu - Tetris 12 3.1 Cách thực hiện thông thường
Cấu trúc
3.1.1.1 Cell prefab Đây là ô sẽ cấu tạo nên các block của Tetris game Gameobject này chỉ có SpriteRenderer và Transform để lưu vị trí, vị trí của từng cell này sẽ được quy đổi ra số nguyên để chuyển thành vị trí lưu trong grid.
Khi đã có các cell cơ bản rồi ta chỉ cần tạo hình cho chúng đúng với các block cơ bản trong game tetris:
Trong game Tetris, có 7 loại block khác nhau, cho phép lập trình viên dễ dàng sáng tạo hình khối thông qua việc kéo thả và điều chỉnh mà không cần hiểu sâu về cấu trúc của chúng Mỗi block đi kèm với một script BlockComponent để điều khiển hiệu quả.
The adjustable parameters for each block in the prefab include the rotation point and the fall time, defined as public Vector3 rotationPoint and public float fallTime Additionally, the prefab features static properties for height and width, represented as public static int height and public static int width The grid structure is maintained as a private static Transform array.
Sơ đồ luồng xử lý rơi
Khi một block mới được spawn, nó sẽ rơi từ từ theo thời gian đã được thiết lập trong prefab, đồng thời nhận và xử lý input của người chơi theo từng khung hình.
Script này đặc biệt có 3 tham số static đó là width, height, và grid là mảng 2 chiều dùng để chứa các GameObject Cell dùng để kiểm tra và
BlockComponent lưu từng Cell trong mảng 2 chiều Transform[,]
Script điều khiển loại block spawn cho phép spawner hoạt động khi block hiện tại được đặt xuống vị trí cố định, từ đó tạo ra các block mới ngẫu nhiên dựa trên các mẫu prefab.
The NormalTetrisController class manages game flow and data, connecting various UI components, but can become complex and difficult to maintain over time Key attributes include score and line management, game pause functionality, and the current block reference While this approach allows for quick development through simple drag-and-drop integration into the scene, it may lead to increasingly unwieldy code as new features are added.
Sơ đồ luồng Update của GameController
Hoạt động
Luồng hoạt động của GameController
Luồng hoạt động mỗi frame của game
Khi bắt đầu trò chơi, các khối sẽ được tạo ra và kiểm tra xem người chơi có nhập lệnh nào không Bộ đếm thời gian rơi của khối bắt đầu hoạt động và đếm qua từng khung hình Khi thời gian vượt quá giới hạn, khối hiện tại sẽ rơi xuống Tiếp theo, hệ thống kiểm tra khả năng tiếp tục rơi của khối; nếu không còn khả năng, các ô hiện tại sẽ được đặt vào lưới Cuối cùng, điểm số sẽ được tính và một khối mới sẽ được tạo ra.
Hình ảnh game và cấu trúc bên dưới
Việc sử dụng vị trí của các CellGO (Cell gameobject) để chuyển đổi thành số nguyên nhằm xác định tọa độ của các cell trong grid (biến grid kiểu Transform[,]) là một phương pháp không hiệu quả Cách làm này hạn chế khả năng di chuyển camera theo ý thích và kích thước của các cell cũng bị giới hạn theo một ô unit của Unity Điều này có thể dẫn đến việc logic và hình ảnh bị gắn kết chặt chẽ với nhau, gây khó khăn trong việc thiết kế và điều chỉnh.
Kết luận
Triển khai một game có vẻ nhanh chóng và đơn giản, nhưng đi kèm với đó là những thách thức về hiệu năng, mở rộng và sửa chữa Chẳng hạn, trong đoạn code trên, các blockComponent xác định vị trí của chúng trên bảng dựa vào vị trí của sprite, thay vì sử dụng thuật toán trên mảng 2 chiều grid Điều này dẫn đến việc dữ liệu, logic và phần hình ảnh của game bị gắn chặt với nhau, gây khó khăn trong việc tách rời và quản lý.
3.2 Sử dụng kiến trúc MVC
Các lớp kế thừa từ lớp TetrisBehaviour chứa những hằng số và thông tin cơ bản về trò chơi Tetris, bao gồm kích thước bàn chơi Lớp này cũng cung cấp một điểm truy cập đến AppGameplay, là script cấp cao nhất cho phép truy cập tất cả các script khác trong game.
TetrisBehaviour: protected const int maxX Số cột của bàn Tetris protected const int maxY Số hàng của bàn Tetris public static readonly
List ScoreForRow Số điểm khi xoá nhiều hàng cùng lúc. public const int maxLevel Level tối đa
Tetris Tile là mảnh Tetris Nó chứa thông tin vị trí từng ô của mảnh Tetris, và một số logic liên quan.
Tetris Tile có thể xoay trái, xoay phải, và clone.
Lớp cung cấp một mảnh Tetris mới khi cần, sử dụng cơ chế túi đặc biệt Tetris bắt đầu với một túi chứa đủ 7 mảnh khác nhau và chọn ngẫu nhiên từ túi này Khi túi hết, hệ thống sẽ tạo ra 7 mảnh Tetris mới, giúp người chơi không phải chờ đợi quá lâu cho một mảnh cụ thể nào đó.
TetrisTile: public int[4] coordX Toạ độ X so với tâm public int[4] coordY Toạ độ Y so với tâm public static readonly
TetrisType[] bag Túi mẫu public static List< TetrisType> currentBag Túi hiện tại public static Dictionary<
TileType, Color> tileColor Màu của tetris.
GameData chứa thông tin chung về game, bao gồm mảnh Tetris mình đang cầm, bàn tetris, điểm, level, trạng thái thua chưa, và thông tin về cơ chế kick.
Cơ chế kick gồm 2 kiểu là wall kick và floor kick. Wall kick là khi xoay viên Tetris, nếu viên Tetris đụng tường, nó sẽ bị đẩy ra
Floor kick cũng tương tự, nhưng với sàn Khi xoay viên Tetris, nếu viên Tetris đụng sàn, nó sẽ bị đẩy lên.
Cơ chế kick giúp di chuyển khỏi cạnh mượt hơn và tránh việc không xoay được viên Tetris. Ảnh minh hoạ cơ chế kick của Tetris
GameData cung cấp các hàm quản lý hữu ích như xóa một hàng cụ thể, sao chép bàn Tetris để các lớp khác có thể tương tác, lấy viên Tetris mới và thiết lập lại thông tin kick.
The Tetris game data structure includes a grid represented as a list of lists of colors, which visually indicates the game board It tracks the current score, the active Tetris tile, and its position within the grid using tileOffsetX and tileOffsetY coordinates Additionally, the structure holds information for wall and floor kicks through kickX and kickY values The game's difficulty level is represented by an integer, while a boolean flag indicates whether the game is over or still in progress.
7 khối cơ bản của Tetris
Nhưng với kiến trúc này ta không thể dùng vị trí của các khối như data được mà phải dùng mảng 2 chiều:
TilePrefab là một tile mẫu ở vị trí (0,0) trên grid Nó có một hình ảnh có thể tô màu Nó sẽ được dùng để hiển thị trên màn hình.
ViewTile là một lớp đại diện cho ô trên lưới với các thuộc tính quan trọng như tọa độ X và Y của ô, cùng với tọa độ gốc baseX và baseY Nếu baseX hoặc baseY chưa được khởi tạo (bằng 0), chúng sẽ tự động nhận giá trị tọa độ hiện tại Thêm vào đó, thuộc tính size xác định khoảng cách giữa tâm của hai ô; nếu size bằng 0 (chưa khởi tạo), nó cũng sẽ tự khởi tạo với kích thước hiện tại.
Bên ngoài sẽ gán trường X, Y cho ViewTile, và ViewTile sẽ có nhiệm vụ đặt mình vào vị trí chính xác.
Khi cần thay đổi vị trí màn hình, bạn có thể điều chỉnh ViewTile Ngoài ra, việc thay đổi TilePrefab cũng là một lựa chọn, vì ViewTile sẽ lấy dữ liệu từ TilePrefab nếu các giá trị khởi đầu chưa được khởi tạo.
BoardViewer có nhiệm vụ xây bàn Tetris trên màn hình sử dụng TilePrefab và ViewTile.
BoardViewer chứa 2 hàm Hàm BuildBoard xây mới cả bàn Tetris nếu cần, và hàm ChangeTile thay đổi một ô trong bàn Tetris.
BoardViewer áp dụng kỹ thuật Object Pooling, giúp các Block không bị khởi tạo và xoá liên tục mà giữ nguyên trạng thái tĩnh, chỉ thay đổi màu sắc Mặc dù trong game đơn giản như Tetris, điều này không ảnh hưởng nhiều đến hiệu suất, nhưng trong các ứng dụng phức tạp hơn, lợi ích của Object Pooling sẽ trở nên rõ rệt hơn.
ButtonView có nhiệm vụ gán hàm điều khiển button.
5 Cách quản lý chức năng nút bấm
Do giao diện người dùng (UI) của mỗi trò chơi có sự khác biệt, các script quản lý UI thường được tùy chỉnh và phân loại theo mục đích của người phát triển, có thể được tổ chức thành các nhóm như panel hoặc nhóm điều khiển.
Script được chia tách thành nhiều file
Mặc dù có nhiều script khác nhau, chúng thường được gom lại trong một GameObject riêng để đại diện cho view, hoặc một số lập trình viên có thể tích hợp trực tiếp vào canvas.
Trong Unity có 1 component hỗ trợ chức năng nút bấm là “button”
Nhiều lập trình viên, bao gồm cả tôi, thường bắt sự kiện khi người dùng nhấn nút bằng cách kéo script chứa hàm xử lý và gán nó vào nút, sau đó trỏ đến hàm đó.
Cách gắn OnClick cho Button thông dụng
Cách này thì nhanh, gọn và tiện đó, nhưng cũng đi kèm nhiều hạn chế:
● Sau này đổi sang nút mới thì phải kéo và tìm hàm lại
● Khó tìm được button để sửa
● Nếu hàm đó dính tới gameplay, người khác sửa vào thì sẽ làm sai chức năng của nút.
Để giảm thiểu các vấn đề liên quan, chúng ta có thể tập hợp tất cả các nút lại và sử dụng một script riêng để thêm Listener, giúp nhận lệnh khi người dùng nhấn nút.
OnClick được gán trong script, giảm thiểu thời gian tìm kiếm trên UI
Bằng cách này, chúng ta có thể quản lý các hàm được gọi mà không cần thông qua scene Nếu sau này có sự thay đổi về nút, chỉ cần kéo nút lại mà không cần tìm hàm cần gọi, giúp tránh trường hợp gọi nhầm hàm.
Controller kế thừa TetrisController, với một số hàm hữu dụng.
Hàm Move yêu cầu di chuyển viên Tetris cho GameData, trả về liệu việc di chuyển có thành công hay không.
Hàm RotateWithKick yêu cầu xoay viên Tetris cho GameData, trả về liệu việc di chuyển có thành công hay không.
Hàm CheckFit kiểm tra liệu một viên Tetris có nằm trong grid của
BoardController có nhiệm vụ nhận những ô thay đổi lên bàn Tetris, xác định trên bàn Tetris có những ô nào thay đổi, và kêu View, cụ thể là
BoardViewer, thay đổi những ô đó.
DropdownController chịu trách nhiệm di chuyển viên Tetris trong GameData xuống theo thời gian Khi viên Tetris chạm đáy, DropdownController sẽ kiểm tra và xóa dòng, đồng thời tính điểm nếu có Mỗi lần xóa dòng, độ khó của trò chơi sẽ tăng lên và tốc độ cũng nhanh hơn, cho đến khi đạt đến cấp độ tối đa.
Khi di chuyển, DropdownController thông báo cho app là bàn Tetris đã thay đổi.
PlayerController nhận tín hiệu từ ButtonView để di chuyển viên Tetris trong GameData Khi thực hiện di chuyển, nó sẽ thông báo cho ứng dụng rằng bàn Tetris đã có sự thay đổi.
Flow xử lý của DropdownController, vòng lặp chính của game
Flow xử lý khi player bấm vào một nút
Sơ đồ các class của bản MVC
Cấu trúc dữ liệu bên dưới của board
So sánh và đánh giá
- Cả 2 phiên bản đều đáp ứng đủ các yêu cầu và chức năng của 1 game Tetris thông thường, các block cơ bản, cách chơi cơ bản và tính điểm
Phiên bản thường của game có ưu thế về tính dễ hiểu nhờ vào việc lưu trữ ít thông tin, giúp tối giản hóa quá trình xử lý Do đó, mã nguồn của phiên bản này trở nên dễ tiếp cận hơn so với hệ thống MVC, yêu cầu người dùng cần có kiến thức nhất định về kiến trúc đang sử dụng.
Bên trái là view MVC, bên phải là Controller của bản thường
Bảng này thể hiện sự phân chia rõ ràng hơn so với phiên bản thông thường, giúp người dùng dễ dàng tìm kiếm thông tin Mặc dù việc gộp tất cả vào một nơi có thể thuận tiện, nhưng điều này dễ gây cảm giác ngợp và chứa nhiều thông tin không cần thiết.
- Ở bản MVC, đa số các thành phần hoạt động tương đối tách rời nhau
Chúng ta có thể tắt DropdownController mà không làm viên Tetris rơi, và game vẫn hoạt động bình thường Tương tự, khi tắt ButtonView, các nút sẽ không còn điều khiển viên Tetris, nhưng game vẫn không bị crash Điều này cho thấy rằng ít có thành phần nào phụ thuộc quá nhiều vào các thành phần khác, giúp giảm thiểu tác động của những thay đổi trong tương lai cũng như lỗi phát sinh.
Trong phiên bản thường, các thành phần của chương trình kết nối chặt chẽ với nhau, khiến cho hầu hết các chức năng không thể hoạt động độc lập Khi xuất hiện lỗi (bug), ảnh hưởng của nó có thể lan rộng nhanh chóng, làm giảm sự hài lòng của người dùng Đối với game, mức độ nghiêm trọng của một bug không chỉ dựa vào khả năng sửa chữa mà còn bởi tác động của nó đến nhiều yếu tố khác Tương tự, khi có sự thay đổi, toàn bộ hệ thống sẽ được kiểm tra để phát hiện các tác dụng phụ, thay vì chỉ tập trung vào một module nhỏ.
Phiên bản thường của mã code trong game thường không thể tái sử dụng vì nó được viết chỉ để phục vụ nhu cầu cụ thể của trò chơi, không hướng đến việc sử dụng sau này Đây là một sai lầm phổ biến mà nhiều lập trình viên Unity mới thường gặp phải.
Kiến trúc MVC cho phép gói lại và triển khai trong nhiều bối cảnh và dự án khác nhau Các module trong kiến trúc này giúp người viết game kết hợp nhiều thành phần để tạo ra những ý tưởng game mới, độc đáo và dễ hiểu.
- Không một cấu trúc nào hoàn hảo cả, nhưng chúng ta có thể ngày càng tiệm cận nó nhờ những cải tiến phù hợp hằng ngày
- Với cấu trúc MVC đã chọn, cùng xem những vấn đề chúng ta đặt ra hồi đầu có lời đáp chưa.
Chỉ số Bản thường Bản MVC
So sánh bằng công cụ Profiler của Unity
Biểu đồ thông số profile
Bản áp dụng kiến trúc MVC:
Biểu đồ thông số profile
Sự khác biệt về hiệu năng giữa hai phiên bản không lớn, vì hầu hết các trò chơi trên điện thoại đều tối ưu hóa để chạy mượt mà ở mức 60 khung hình trên giây (fps).
So sánh memory sử dụng
Thông số bộ nhớ bản thường
Bản áp dụng kiến trúc MVC
Thông số bộ nhớ bản MVC
Chỉ số Bản thường Bản MVC
Kích thước game không có sự chênh lệch lớn về bộ nhớ, nhưng khi so sánh số lượng đối tượng trong cảnh, phiên bản MVC có ít hơn gấp 3 lần Điều này chứng tỏ rằng trong thời gian runtime, mỗi khung hình chỉ xử lý không quá nhiều đối tượng, giúp giảm thời gian chạy cảnh, từ đó tăng frame rate và giữ cho game luôn mượt mà ổn định.
Quy trình thực hiện
Nghiên cứu nhiều loại kiến trúc khác nhau - Tưởng Thành Long
- Nguyễn Nhật Long Áp dụng và xây dựng thử dựa trên các kiến trúc đã nghiên cứu
Xây dựng nền game - Tưởng Thành Long
- Nguyễn Nhật Long Dựng game bản thường - Nguyễn Nhật Long
Dựng game bản MVC - Tưởng Thành Long
Chỉnh sửa giao diện 2 phiên bản
Thêm âm thanh vào 2 phiên bản - Tưởng Thành Long
Chỉnh chức năng cho bản
Thêm chức năng bản thường - Tưởng Thành Long
Soạn slide, trình bày báo cáo - Nguyễn Nhật Long
Hoàn thiện báo cáo - Tưởng Thành Long
Quản lý source code: Git, lưu trữ: Github
Soạn tài liệu: Google docs
- Cả 2 phiên bản đều vận hành và chạy rất ổn định.
- Thời gian kiểm thử: 30 phút liên tục
- Tình trạng giật khung hình: Không có
- Tiến độ: Trong phạm vi đã lên kế hoạch
- Tự đánh giá mức độ hoàn thành: 90%
Để nâng cao trải nghiệm người chơi, chúng tôi sẽ mở rộng game bằng cách xây dựng thêm các cảnh level mới và tính năng power up Đồng thời, chúng tôi cũng sẽ thêm hiệu ứng và các tính năng mới, vì gameplay cơ bản đã được hoàn thiện.
Kết luận
Trong quá trình làm việc, chúng tôi nhận thấy rằng độ dài mã nguồn không phải là thước đo chính xác cho tính dễ hiểu, khi so sánh giữa bản Tetris thường và bản Tetris MVC Mặc dù bản Tetris MVC dài gấp đôi, nhưng sự mở rộng này lại giúp tăng cường khả năng phát triển và dễ hiểu hơn cho mã nguồn.
Về mặt thông số, Tetris MVC và Tetris cơ bản không có sự khác biệt lớn, điều này phần nào xuất phát từ bản chất đơn giản của trò chơi Tetris, không yêu cầu tính toán phức tạp hay đồ họa nặng.
Một số kiến trúc phần mềm đã nghiên cứu 41 4.1 Entity Component System
Giới thiệu
Entity Component là một kiến trúc được Unity hỗ trợ Một GameObject được coi là một Entity, và chứa nhiều script đóng vai trò Component
Component sẽ định nghĩa một đặc tính của GameObject cũng như các dữ liệu liên quan đến đặc tính này Các Entity có thể dùng hàm Find hoặc
FindWithTag để tìm kiếm, và các Component có thể dùng hàm
GetComponent để tìm kiếm Tuy nhiên, các hàm này đều tốn thời gian và chỉ nên được thực hiện 1 lần rồi cache lại kết quả.
ECS là một phương pháp hiệu quả để tái sử dụng các Script và phân tách các thành phần của lớp, giúp xây dựng các lớp lớn một cách dễ dàng hơn Nó mang lại những đặc tính và ưu điểm của mẫu thiết kế Strategy.
Cơ bản Entity-Component-System là một hệ thống gồm Entity, Component và System
In gaming, every character or object is considered an entity, each possessing a unique identifier (UniqueID) The characteristics, states, and behaviors of these entities are determined by specific attributes and parameters.
Con Enemy là một thực thể được tổng hợp từ nhiều thành phần, bao gồm khả năng hiển thị (Renderable), khả năng di chuyển (Movable), và yếu tố vật lý (Rigidbody) Ngoài ra, nó còn tích hợp AI để tự động tìm kiếm người chơi và tấn công.
Cây bên đường được coi là một entity, bao gồm các thành phần như: Thành phần hiển thị (Renderable), yếu tố vật lý (Rigidbody), và không có yếu tố di chuyển (Movable) hay AI.
Component là đơn vị nhỏ nhất, tạo thuộc tính cho entity, và mọi component đều phải gắn liền với một entity Nó chỉ chứa dữ liệu thô (raw data).
+ Renderable : chứa thông tin về
+ Movable : chứa thông tin tốc độ di chuyển, gia tốc …
+ Collider : hình dạng, kích thước
System hình tham khảo từ trang GameDev về một bản design component
Là những những module con, hoạt động độc lập nhau, xử lý một
“behaviour” nào đó trong game, VD :
+ RenderSystem : nhiệm vụ là “vẽ” tất cả entity có
+ MovingSystem : tính toán thông tin vị trí của các entity có
+ PhysicSystem : kiểm tra va chạm của các entity có ColliderComponent.
ECS, hay Entity-Component-System, là một mô hình thiết kế game không quá phức tạp, nhằm giải quyết các vấn đề trong quá trình phát triển trò chơi Là một giải pháp hiệu quả, ECS mang lại nhiều lợi ích cho các nhà thiết kế, từ việc tối ưu hóa hiệu suất đến tăng cường khả năng mở rộng và linh hoạt trong việc quản lý các đối tượng trong game Chính vì những lợi ích này mà ECS trở nên phổ biến trong cộng đồng phát triển game.
4.1.2, Vấn đề ECS giải quyết:
Entity-Component-System (ECS) là một mẫu thiết kế phân phối và thành phần, giúp tách rời linh hoạt các hành vi theo miền Mô hình này khắc phục nhiều nhược điểm của kế thừa hướng đối tượng truyền thống.
Cách truyền thống để phát triển trò chơi thường yêu cầu một hệ thống phân cấp cho các đối tượng mô hình hóa thế giới Ngay cả những đối tượng đơn giản cũng có thể chứa nhiều phương thức không được sử dụng, điều này cho thấy sự phức tạp trong việc thiết kế và tối ưu hóa các thành phần của trò chơi.
Ví dụ thỏ và cá voi
Trong cấu trúc lớp của Animal, chúng ta có hai lớp con là Bunny và Whale, với Killer Whale là lớp con của Whale Các lớp này có những phương thức riêng biệt như hop(), swim(), và kill() Khi xuất hiện đối tượng mới là BunnyKiller, với hai phương thức hop() và kill(), câu hỏi đặt ra là đối tượng này sẽ kế thừa từ Bunny hay Killer Whale?
Một số ngôn ngữ chỉ cho phép đơn kế thừa, chỉ có thể kế thừa từ 1 class.
Để giải quyết vấn đề này, có thể khởi tạo hai phương thức hop() và kill() trong lớp cha Animal, cho phép BunnyKiller kế thừa từ Animal Tuy nhiên, các lớp con khác như Bunny sẽ không cần đến phương thức kill(), trong khi Whale lại không sử dụng hop() Nếu áp dụng đa thừa kế, BunnyKiller có thể kế thừa từ Bunny và KillerWhale, nhưng sẽ dẫn đến việc dư thừa phương thức swim() và các vấn đề phức tạp khác.
● Mô hình cứng nhắc: Chỉ có KillerWhale hay các lớp con của nó như
KillerBunny chỉ có thể sử dụng phương thức kill(), điều này tạo ra khó khăn trong việc tạo thêm đối tượng mới để thực hiện kill() Phương thức này chỉ khả dụng với các đối tượng không rõ nguồn gốc trong hệ thống gia phả của trò chơi.
Vấn đề kế thừa kim cương là một thách thức phổ biến trong lập trình đa kế thừa, đặc biệt là khi xác định lớp D Khi tiến hành Debug, việc xác định ngã rẽ đúng trở nên khó khăn Do đó, nhiều ngôn ngữ lập trình hiện đại đã ưu tiên sử dụng đơn kế thừa và áp dụng interface để giải quyết vấn đề này hiệu quả hơn.
Cách sử dụng
Ưu điểm / nhược điểm
Trong quá trình phát triển, không có bản thiết kế nào giữ nguyên từ đầu đến cuối; nó luôn cần thay đổi, bổ sung và điều chỉnh Với ECS, việc thay đổi trở nên dễ dàng hơn bao giờ hết.
Nhà thiết kế có khả năng tạo ra các thuộc tính phức tạp bằng cách kết hợp các module thuộc tính nhỏ lẻ Họ cũng có thể cấu hình và thay đổi hành vi của nhân vật bằng cách thêm hoặc bớt các module nhỏ, đồng thời điều chỉnh các thông số cho từng module.
- Không có GameManager quản lý chung
- Unity không hỗ trợ thay đổi System, 1 trong 3 thành phần quan trọng của ECS.
Kết luận
Kiến trúc ECS là một cấu trúc hiệu quả cho thiết kế game, nhưng cũng giống như mẫu thiết kế Strategy, việc lạm dụng ECS có thể dẫn đến nhiều khuyết điểm Mặc dù nhiều trò chơi có thể hưởng lợi từ kiến trúc này, nhưng cũng không ít trò chơi gặp khó khăn khi bị ép buộc phải tuân theo ECS Do đó, cần thiết phải có những kiến trúc khác để bổ sung cho kiến trúc ECS trong Unity.
Việc phát triển game với kiến trúc này là khả thi và dễ dàng cho người mới bắt đầu học Unity Mặc dù lập trình viên có thể tiến xa với kiến trúc này, nhưng nó cũng tồn tại những giới hạn nhất định Hơn nữa, những lập trình viên đến từ các môi trường phát triển game khác có thể gặp khó khăn khi làm quen với kiến trúc và những ràng buộc mà nó mang lại.
Empty Game Object Manager
Một nhược điểm của ECS của Unity là kiến trúc này không có Game
Manager trong game, như Win Manager (xác định người chơi thắng/thua), Turn-based Manager (quản lý lượt chơi) và Enemy Spawner (tạo kẻ thù), là những thành phần quan trọng Các Game Manager này thuộc nhóm System trong Entity Component System, nhưng Unity không cho phép thay đổi System chính Ý tưởng của Empty Game Object Manager là tạo ra một Manager với tham chiếu đến các Game Object liên quan và gom chúng vào một Game Object rỗng gọi là Manager Nhờ vào Empty Game Object Manager, người phát triển có thể xây dựng một System riêng biệt và tập trung quản lý nó ở một nơi.
4.2.2 Vấn đề Empty Game Object Manager giải quyết:
Hệ thống trong ECS của Unity gặp phải vấn đề về tính linh hoạt, khi mà người dùng chỉ có thể thay đổi Entity và Component mà không thể can thiệp vào System Điều này tạo ra một hạn chế lớn trong việc áp dụng ECS, ví dụ như khi cần xóa tất cả Game Object ra khỏi màn hình, người dùng buộc phải kiểm tra từng hàm Update của mọi Component để xác định và xóa các đối tượng ngoài màn hình.
Ví dụ về lặp code khi cài đặt cơ chế chung cho scene bình thường
Việc xóa khi ra khỏi màn hình không chỉ là một đặc tính đơn thuần mà còn vi phạm quy tắc ngữ nghĩa và quy luật Don’t Repeat Yourself.
Trong một thế giới lý tưởng, chúng ta có thể tích hợp System vào kiến trúc ECS, nhưng do hạn chế từ Unity, chúng ta buộc phải sử dụng kiến trúc hiện tại.
Các Manager thường được cài đặt bằng Singleton để đảm bảo có 1 và chỉ
Số lượng Manager trong Scene quá nhiều sẽ gây lãng phí tài nguyên xử lý, vì mỗi Manager thường tiêu tốn nhiều chi phí để quản lý tất cả các MonoBehaviour mà nó quan tâm.
Logic của Manager được đặt ở bên trong hàm Update của nó
Để cài đặt Game Manager hiệu quả, bạn nên gom tất cả các Manager vào một GameObject rỗng, có thể áp dụng mẫu thiết kế Singleton hoặc sử dụng một tag đặc biệt để truy cập nhanh Việc đặt Game Manager trong cùng một GameObject giúp hệ thống code trở nên dễ đọc hơn, với các hệ thống game được tập trung tại một vị trí, thay vì phân tán khắp nơi trong chương trình Khi cần xem lại code, bạn chỉ cần mở GameObject Manager và xem các script liên quan, từ đó dễ dàng hiểu được các hệ thống chính của chương trình.
Các manager trong game có thể được thiết kế để tái sử dụng, giúp tiết kiệm tài nguyên Dù mỗi trò chơi có hệ thống riêng, nhiều hệ thống phổ biến như thư viện âm thanh và hình ảnh thường được áp dụng Ví dụ, mẫu thiết kế Flyweight có thể được sử dụng cho các thư viện này, cùng với hệ thống lưu game, tạo ra sự đồng nhất và hiệu quả trong phát triển game.
… Nếu có thể tái sử dụng Manager, lần tiếp theo làm game sẽ đơn giản hơn rất nhiều.
Ngoài ra, có một cách sử dụng kiến trúc này mạnh mẽ hơn
Sử dụng GameLoopManager để thoát khỏi yêu cầu bắt buộc dùng ECS của Unity
Kiến trúc này cho phép GameManager kiểm soát toàn bộ luồng code của chương trình bằng cách thay thế hàm Update của Unity bằng một hàm Update tự định nghĩa, giúp kiểm soát thứ tự gọi hàm Update Phương pháp này cho phép thực hiện các tác vụ giữa các lần gọi Update, mang lại sự linh hoạt giống như trong một game loop truyền thống, trong khi vẫn tận dụng được các công cụ và tài sản có sẵn của Unity Điều này đặc biệt phù hợp với nhiều lập trình viên game, nên bạn có thể cân nhắc áp dụng cho trò chơi của mình.
Khi phát triển Empty Game Object Manager, cần chú ý đến nguyên tắc Single Responsibility Việc nhiều lập trình viên chỉ tạo ra một Manager sẽ dẫn đến hiệu quả kém trong kiểm thử và bảo trì mã nguồn, đồng thời làm giảm khả năng tái sử dụng của Manager đó.
Empty Game Object Manager là một kiến trúc mạnh mẽ và hiệu quả, thường được các lập trình viên Unity áp dụng trong các dự án của họ Kiến trúc này, mặc dù đơn giản, lại mang lại nhiều lợi ích và là một giải pháp phổ biến mà nhiều người sẽ nghĩ đến hoặc gặp phải trong quá trình phát triển game.
Vì sự đơn giản của kiến trúc này, với độ khó duy trì kiến trúc hầu như khác.
Model View Controller
Mô hình phát triển ứng dụng Model View Controller (MVC) là một phương pháp phổ biến ngoài môi trường phát triển game Mô hình này bao gồm ba thành phần chính: Model, là nơi lưu trữ và cập nhật dữ liệu; View, là các Game Object hiển thị trên màn hình; và Controller, đóng vai trò trung gian kiểm soát luồng thông tin giữa Model và View.
Lợi thế của MVC rất đơn giản nhưng quan trọng Ta có thể thay đổi
Mô hình và View trong các trò chơi như game theo lượt, board game, và puzzle game hoạt động độc lập, tạo ra một kiến trúc đơn giản và linh hoạt Tuy nhiên, trong các trò chơi mà Model ảnh hưởng trực tiếp đến View hoặc ngược lại, chẳng hạn như game có xử lý va chạm và ánh sáng, cần phải xem xét kỹ lưỡng mối quan hệ giữa chúng để đảm bảo hiệu suất và trải nghiệm người chơi tốt nhất.
… ảnh hưởng đến gameplay, thì MVC khó có thể hoạt động tốt.
4.3.2Cách sử dụng: Để cài đặt Model, chỉ cần sử dụng C# như bình thường, không bắt buộc kế thừa MonoBehaviour của Unity Do Model chỉ là lớp dữ liệu, việc cài đặt các lớp này khá đơn giản Ngoài ra, ta có thể theo quy trình phát triển ứng dụng MVC và thiết kế sơ đồ lớp nếu chương trình đủ phức tạp Ở đây cũng cài đặt các Observer hoặc Event để bắt những thay đổi trên Model, báo cho Controller. Để cài đặt View, có nhiều cách Một cách là định nghĩa hàm thay đổi màn hình khi Model thay đổi Để làm điều này, cần liệt kê những thay đổi có thể của màn hình, và cài đặt các hàm thay đổi đó Cách khác là gọi lấy dữ liệu từ Model và xây lại màn hình Cách này thường đơn giản hơn cách thứ nhất, nhưng hiệu năng kém hơn cách trước Tuy nhiên, nhược điểm này có thể không đáng lo cho những game nhẹ về xử lý
Ngoài ra, View cũng có trách nhiệm truyền tín hiệu từ Người dùng đến Controller
The installation of the Controller is considered the most challenging part of the MVC architecture, as it serves as an intermediary between the Model and the View Initially, the Controller must receive signals from the View to modify the Model Subsequently, by utilizing Observer or Event patterns within the Model, the Controller can detect changes in data and instruct the View to update or redraw itself Additionally, as mentioned in the article from Toptal, it is possible to incorporate two additional components into the conventional MVC structure.
Mô hình AMVCC, thêm Application và Component
Lớp Application giữ vai trò quan trọng trong việc tham chiếu đến các thành phần Model, View và Controller Nó cũng có nhiệm vụ điều hướng tín hiệu từ View và Model đến nhiều Controller liên quan, thay vì chỉ từ Model đến Observer.
Lớp Helper, hay còn gọi là Component, là các lớp chứa các hàm xử lý phổ biến như di chuyển và xoay Mặc dù không thuộc về một mô hình cụ thể nào, nhưng chúng có tính tái sử dụng cao và có thể được chuyển giao từ dự án này sang dự án khác.
MVC là một kiến trúc phổ biến trong phát triển ứng dụng, nhưng không phải là lựa chọn tối ưu cho phát triển game Dù vậy, nhiều người vẫn quen thuộc với kiến trúc này hơn là kiến trúc ECS của Unity MVC thường hiệu quả hơn trong các trò chơi có xử lý gameplay ít nhưng yêu cầu xử lý UI phức tạp, chẳng hạn như board game hay turn-based game.
Chương 5: Một số design pattern hữu ích.
Observer pattern
Pattern này cho phép một đối tượng thông báo sự kiện mà không cần phải tiếp cận từng cá nhân Thay vào đó, những người quan tâm sẽ đăng ký theo dõi thông tin trên một bảng tin chung Khi có thông báo mới, những ai đã đăng ký sẽ tự động nhận được thông tin và tìm đến thông báo đó.
Chúng ta sẽ định nghĩa các event trong game trước như
Tạo 1 bảng thông báo chung (Dispatcher) để tiếp nhận và chuyển tiếp các events này đến các đối tượng
Khi bắt đầu game, các đối tượng cần sẽ đăng ký với bảng tin những sự kiện
Unity có hỗ trợ sử dụng Observer pattern với Event System.
Command pattern
Command pattern là một mẫu thiết kế hành vi cho phép đóng gói thông tin cần thiết để thực hiện một hành động hoặc kích hoạt sự kiện sau này Thông tin này bao gồm tên phương thức, đối tượng sở hữu phương thức và giá trị cho các tham số của phương thức.
Giả sử chúng ta đang thiết kế xử lý đầu vào để người chơi điều khiển nhân vật (Input):
Input không nên được gán cứng
Việc thiết kế nút chức năng một cách cứng nhắc sẽ hạn chế khả năng thay đổi sau này, dẫn đến trải nghiệm người dùng kém.
Thay vào đó chúng ta có thể áp dụng Command pattern.
Để thay đổi chức năng của nút X, chúng ta có thể gán các Command khác giúp giảm bớt sự phức tạp của một enum trung gian Ngoài ra, Command pattern còn mang lại nhiều lợi ích khác cho việc quản lý lệnh trong ứng dụng.
Nó cho phép lưu trữ lịch sử các hành động, cài đặt chức năng hoàn tác (Undo) cho những hành động đã thực hiện, và truyền tải các hành động để kích hoạt ở những nơi hoặc thời điểm khác.
Mẫu lệnh (Command pattern) rất linh hoạt và có thể được áp dụng trong nhiều tình huống khác nhau Tuy nhiên, cần lưu ý rằng việc thiết kế quá mức có thể dẫn đến sự phức tạp không cần thiết.
Singleton
Pattern Singleton là một khái niệm quen thuộc trong phát triển game, cho phép tạo ra một đối tượng duy nhất có thể truy cập từ bất kỳ đâu và bất kỳ lúc nào Nó thường được sử dụng cho các manager như GameManager, ScoreManager, hay PlayerManager, mà chỉ cần xuất hiện một lần trong suốt thời gian gameplay Tuy nhiên, việc lạm dụng Singleton có thể dẫn đến khó khăn khi xử lý lỗi, vì bạn sẽ phải kiểm tra từng class Hơn nữa, nếu cần thay đổi một hàm trong singleton, tất cả các nơi gọi đến nó cũng phải được cập nhật, gây tốn công sức đáng kể.
Mọi lớp đều có thể truy cập đến Singleton
Pattern này thường được dùng trong Empty Game Object Manager nói trên Lý do là vì đa số Manager thường chỉ cần và chỉ nên có 1.
Singleton cung cấp quyền truy cập toàn cục cho các Manager, điều này rất quan trọng vì các Manager này ảnh hưởng đến nhiều thành phần khác nhau trong game, chẳng hạn như tính chất của Scene hoặc không gian game.
Service Locator
Mô hình Service Locator giúp giải quyết vấn đề liên quan đến việc sử dụng quá nhiều singleton trong một dự án Mục tiêu chính của Service Locator là tạo ra một điểm truy cập chung, từ đó người dùng có thể dễ dàng truy cập các dịch vụ khi cần thiết thông qua điểm truy cập này.
Vậy dễ mở rộng và sửa chữa hơn singleton ở điểm nào?
Sử dụng 1 class để triển khai interface, ở đây mình dùng class test với mục đích sau này test hệ thống audio có hoạt động ổn không.
Chúng ta đăng ký nó với Locator:
Và khi sử dụng chúng ta chỉ việc
Sau này, khi hệ thống âm thanh có sự thay đổi trong cách hoạt động, bạn chỉ cần đăng ký với một lớp khác triển khai IAudioManager Đây chính là mục đích cốt lõi của service locator.
- Tạo điểm truy cập service chung
- Giấu các lớp xử lý bên dưới, chỉ để lại phần giao diện
- Dễ dàng thay thế và sửa chữa
Object Pooling
Trong Unity, việc Instantiate và Destroy object có thể gây ra vấn đề hiệu năng nghiêm trọng, vì vậy việc tái sử dụng object là một giải pháp hiệu quả để nâng cao trải nghiệm người chơi Thay vì tạo và hủy object liên tục, bạn có thể tạo ra một bể (pool) để lưu trữ các object, sau đó tắt hoặc ẩn chúng để giảm tải cho quá trình xử lý Khi cần, bạn chỉ việc kích hoạt lại và điều chỉnh thông số để sử dụng mà không phải tạo mới Mặc dù Object Pooling có độ phức tạp cao, nhưng nó mang lại lợi ích lớn trong việc tiết kiệm hiệu năng cho chương trình, đặc biệt khi khởi tạo game object phức tạp trong Unity tốn rất nhiều thời gian.
Một điểm lưu ý là pool sẽ không tự động xoá object cũ Cần phải cài đặt chức năng đó nếu số game object trong pool không có giới hạn.
Khi số lượng game object có giới hạn, thay vì khởi tạo theo nhu cầu, bạn có thể khởi tạo tĩnh ngay từ đầu chương trình Phương pháp này giúp giảm lag do quá trình khởi tạo, nhưng cũng có nhược điểm là nếu pool hết game object, sẽ không có game object mới nào được tạo ra.
Chúng em tham gia đồ án này với mong muốn xây dựng một kiến trúc chung cho Unity, nhằm tạo ra một giải pháp hiệu quả cho nhiều loại game Tuy nhiên, thực tế cho thấy rằng một kiến trúc hoàn hảo như vậy có thể không tồn tại Thay vào đó, chúng em có thể thu thập các mẫu thiết kế, kiến trúc và ý tưởng khác nhau, có thể hỗ trợ cho từng dự án game cụ thể Những tài nguyên này sẽ rất hữu ích, nhưng không thể thay thế cho việc làm việc cùng nhóm để xây dựng một kiến trúc vững chắc và hợp lý.
Trong quá trình nghiên cứu các kiến trúc game, chúng em đã thu thập nhiều kiến thức về cấu trúc game, cũng như nhận diện ưu điểm và hạn chế của Unity khi áp dụng các kiến trúc này Bên cạnh đó, chúng em cũng đã phát triển những ý tưởng thiết kế chương trình đặc biệt liên quan đến Unity.