Unity Screen Navigator
On the release of "Unity Screen Navigator" - Reasons for OSS and design intentions
Bài dịch README.md của Unity Screen Navigator (bài gốc – commit 426024b)
- README.md
- Lý do USN được công khai dưới dạng mã nguồn mở và ý định thiết kế
- Phương pháp tách biệt View và Logic
README.md
Tổng quan
Tính năng
- Tạo pages, modals, tabs và chuyển động của chúng một cách dễ dàng và linh hoạt.
- Quản lý screen và bộ nhớ từ khi load cho đến khi destroy.
- Quy trình tách biệt với các animator cho các hoạt ảnh chuyển đổi màn hình phức tạp.
- Thư viện được phân tách tốt không có các chức năng phụ (ví dụ: thư viện GUI, máy trạng thái).
- Và các tính năng tiêu chuẩn như history stacking và ngăn chặn nhấp chuột trong quá trình chuyển đổi.
Demo
Bạn có làm theo những bước sau để chơi thử
- Clone repo
- Mở và chạy scene DemoEntryPoint
Cài đặt
Yêu cầu
- Unity 2021.3 hoặc cao hơn
- uGUI (không hỗ trợ UIElements)
Cài đặt
Unity Package Manager
- Mở Package Manager (Window > Package Manager).
- Nhấn nút (+) > Add package from git URL.
- Nhập
https://github.com/Haruma-K/UnityScreenNavigator.git?path=/Assets/UnityScreenNavigatorvà nhấn Add.
Chỉnh sủa file Manifest
Mở Packages/manifest.json và thêm dòng sau:
{
"dependencies": {
"com.harumak.unityscreennavigator": "https://github.com/Haruma-K/UnityScreenNavigator.git?path=/Assets/UnityScreenNavigator"
}
}Nếu muốn cài phiên bản khác thì thêm số phiên bản vào cuối đường link (VD: #1.0.0) https://github.com/Haruma-K/UnityScreenNavigator.git?path=/Assets/UnityScreenNavigator#1.0.0
Chuyển màn hình cơ bản
Khái niệm về màn hình (screens) và chuyển tiếp (transitions)
USN chia màn hình (screens) thành 3 loại: Page, Modal và Sheet.
- Page là một màn hình được chuyển đổi theo trình tự. (Nhìn giống các screen)
- Modal là một màn hình được stack trong một của window. (Nhìn giống popup)
- Sheet được sừ dụng giống như các tab.
- Các screen có thể được lồng vào nhau.
Tạo page
Để tạo page. Đầu tiên, tạo Page Container bằng cách gắn component PageContainer vào một GameObject bên trong Canvas. Pages sẽ hiển thị fit với container nên hãy điều chỉnh kích thước cho phù hợp.
Tiếp theo, tạo Page bằng cách tạo gameObject mới (tương ứng với một page) bên trong PageContainer và thêm component Page. Đặt GameObject này trong thư mục Resource với tên bất kỳ.
Và gọi PageContainer.Push() với đường dẫn Resources để hiển thị page.
Ví dụ cho push page được đặt tại Assets/Resources/ExamplePage.prefab.
PageContainer pageContainer;
// Push the page named "ExamplePage".
var handle = pageContainer.Push("ExamplePage", true);
// Wait for the transition to finish.
yield return handle;
//await handle.Task; // You can also use await.
//handle.OnTerminate += () => { }; // You can also use callback.Gọi PageContainer.Pop() để deactive page và hiển thị page trước đó.
PageContainer pageContainer;
// Pop the active page.
var handle = pageContainer.Pop(true);
// Wait for the transition to finish.
yield return handle;Nếu muốn bỏ qua một trang nhất định khi gọi Pop(). có thể tắt stacking history bằng cách sử dụng tham số tuỳ chỉnh.
Tạo modal
Tương tự như tạo page, đầu tiên tạo một Modal Container bằng cách thêm component ModalContainer vào một gameObject bên trong Canvas.
Nhìn chung, các modal được thiết kế để che toàn bộ màn hình và chặn các click bằng phông nền của nó. Vì thế kích thước RectTransform của GameObject về cơ bản phải được đặt khớp với kích thước màn hình.
Tiếp theo, tạo Modal bằng cách thêm component Modal vào GameObject gốc của modal view. Game Object gốc này sẽ được điều chỉnh để fit với kích thước của Modal Container
Nếu bạn muốn tạo modal có margins, hãy tạo một GameObject con với kích thước nhỏ hơn.
Đặt Modal vào folder Resources với tên bất kỳ.
Và gọi ModalContainer.Push() với tham số là đường dẫn modal trong folder Resources để hiển thị modal.
Ví dụ cho hiển thị modal đặt tại Assets/Resources/ExampleModal.prefab
ModalContainer modalContainer;
// Push the modal named "ExampleModal".
var handle = modalContainer.Push("ExampleModal", true);
// Wait for the transition to finish.
yield return handle;
//await handle.Task; // You can also use await.
//handle.OnTerminate += () => { }; // You can also use callback.
Dùng ModalContainer.Pop() để deactive modal và show modal trước đó.
ModalContainer modalContainer;
// Pop the active modal.
var handle = modalContainer.Pop(true);
// Wait for the transition to finish.
yield return handle;Chú ý. Bạn có thể thay đổi phông nền của modal nếu muốn.
Tạo Sheet
Tương tự như trên, đầu tiên thêm component SheetContainer vào game object bên trong Canvas. Hãy căn chỉnh kích thước của nó vì sheet sẽ được hiển thị fit với Canvas.
Tiếp theo, thêm component Sheet vào game object gốc của sheet view. Đặt game object này vào folder Resources với tên bất kỳ.
Gọi SheetContainer.Register() với đường dẫn trong folder Resources để tạo sheet. Sau khi được tạo, bạn có thẻ thay đổi active sheet bằng cách gọi SheetContainer.Show(). Khi đó, nếu có một sheet đang hoạt động thì nó sẽ bị deactive.
Ví dụ, hiển thị sheet đặt tại Assets/Resources/ExampleSheet.prefab.
SheetContainer sheetContainer;
// Instantiate the sheet named "ExampleSheet"
var registerHandle = sheetContainer.Register("ExampleSheet");
yield return registerHandle;
// Show the sheet named "ExampleSheet"
var showHandle = sheetContainer.Show("ExampleSheet", false);
yield return showHandle;Lưu ý rằng khi nhiều sheet có cùng key được khởi tạo bằng phương thức Register() thì các sheet không được đảm bảo key. Trong trường hợp này, hãy sử dụng ID của sheet thay vì key.
SheetContainer sheetContainer;
// Instantiate the sheet named "ExampleSheet" and get the sheet id.
var sheetId = 0;
var registerHandle = sheetContainer.Register("ExampleSheet", x =>
{
sheetId = x.sheetId;
});
yield return registerHandle;
// Show the sheet with sheetId.
var showHandle = sheetContainer.Show(sheetId, false);
yield return showHandle;Sử dụng Hide() để ẩn một sheet thay vì chuyển sheet khác.
SheetContainer sheetContainer;
// Hide the active sheet.
var handle = sheetContainer.Hide(true);
// Wait for the transition to finish.
yield return handle;Làm thế nào để chờ một quá trình chuyển tiếp hoàn tất?
Mỗi phương thức (method) chuyển tiếp đều trả về AsyncProcessHandle. Sử dụng đối tượng này bạn có thể đợi các chuyển tiếp xử lý xong.
Bạn có thể dùng coroutines, phương thức bất đồng bộ, và callback
// Use coroutines
yield return pageContainer.Push("ExamplePage", true);
// Use async/await
await pageContainer.Push("ExamplePage", true).Task;
// Use callback
pageContainer.Push("ExamplePage", true).OnTerminate += () => { };Lấy một container bằng static methods
Mỗi container (PageContainer/ModalContainer/SheetContainer) đều có các phương thức tĩnh để có thể lấy được.
Sử dụng Container.Of() bạn có thể lấy được các container gắn trên Transform/RectTransform gần nhất.
var pageContainer = PageContainer.Of(transform);
var modalContainer = ModalContainer.Of(transform);
var sheetContainer = SheetContainer.Of(transform);Bạn cũng có thể dùng thuộc tính Name trong Inspector của container để lấy container bằng tên của nó. Trong trường hợp này, sử dụng Container.Find().
var pageContainer = PageContainer.Find("SomePageContainer");
var modalContainer = ModalContainer.Find("SomeModalContainer");
var sheetContainer = SheetContainer.Find("SomeSheetContainer");Animation chuyển màn hình
Cài đặt animation chung
Theo mặc định, có một animtion tiêu chuẩn cho mỗi loại màn hình.
Bạn có thể tạo một class kế thừa TransitionAnimationObject để custom lại animtion. Lớp này có các thuộc tính và phương thức để khai báo hành vi của animation.
// Duration (sec).
public abstract float Duration { get; }
// Initialize.
public abstract void Setup();
// Define the state at this time.
public abstract void SetTime(float time);Tham khảo SimpleTransitionAnimationObject để biết cách triển khai chi tiết.
Sau đó, khởi tạo ScriptableObject này và gắn nó vào UnityScreenNavigatorSettings. Nếu chưa có UnityScreenNavigatorSettings bạn có thể tạo Assets > Create > Screen Navigator Settings.
Cài animation cho mỗi màn hình
Bạn cũng có thể cài đặt animation khác nhau cho mỗi màn hình.
Mỗi Page, Modal, và Sheet đều có thuộc tính AnimationContainer. Bạn có thể gắn animation vào đó.
Bạn có thể thay đổi animation của màn hình này bằng cách chọn AssetType là ScriptableObject và gán TransitionAnimationObject đã tạo ở bước trên cho AnimationObject.
Bạn cũng có thể sử dụng MonoBehaviour thay cho ScriptablẹObject. Trong trường hợp này bạn tạo một lớp kế thừa TransitionAnimationBehaviour.
Xem SimpleTransitionAnimationBehaviour để biết cách triển khai chi tiết.
Sau đó có thể setup tương tự như trên. Chọn AssetType là MonoBehaviour, gán TransitionAnimationBehaviour vừa tạo ở trên vào AnimationBehaviour.
Thay đổi hiệu ứng chuyển cảnh dựa trên màn hình tương ứng.
Ví dụ, khi vào màn hình A và thoát màn hình B, màn hình B được gọi là “màn hình đối tác” của màn hình A.
Nếu bạn nhập tên của màn hình đối tác cho thuộc tính bên dưới thì animation sẽ chỉ apply khi tên này trùng khớp với tên của màn hình đối tác.
Mặc định, tên của prefab được sử dụng như tên màn hình.
Nếu bạn muốn đặt tên cho nó. Hãy bỏ tick UsePrefabNameAsIdentifier và nhập tên vào ô Identifier.
Ngoài ra, các biểu thức chính quy (regular expressions) có thể được sử dụng cho PartnerPageIdentifierRegex. Và nếu nhiều animation được đặt thì chúng sẽ được thực hiện từ trên xuống.
Hiệu ứng chuyển đổi màn hình và thứ tự vẽ
Trong animation chuyển tiếp của mộ màn hình với đối tác của nó, thứ tự vẽ có thể quan trọng. Ví dụ một animation che mất màn hình đối tác.
Nếu bạn muốn control thứ tự này hãy sử dụng thuộc tính RenderingOrder.
Trong quá trình chuyển màn hình, màn hình được vẽ theo thứ tự giảm dần của giá trị này.
Model không có thuộc tính RenderingOrder vì modal mới nhất luôn hiển thị bên trên.
Tạo animation chuyển tiếp đơn giản
Bạn có thể dùng SimpleTransitionAnimationObject như một triển khai đơn giản.
Bạn có thể tạo nó từ Assets > Create > Screen Navigator > Simple Transition Animation. Sau đó một ScriptableObject được tạo tương tự như hình bên dưới. Bạn có thể tuỳ chỉnh các thuộc tính trong cửa số Inspector.
Bạn cũng có thể dùng SimpleTransitionAnimationBehaviour như một triển khai MonoBehaviour. Nó được sử dụng bằng cách gán trực tiếp vào GameObject.
Giải thích các thuộc tính
Property Name | Description |
|---|---|
Delay | Delay trước khi chạy animtion (giây). |
Duration | Thời gian animation (giây). |
Ease Type | Loại ease |
Before Alignment | Vị trí tương đối so với container trước khi bắt đầu. |
Before Scale | Scale trước khi bắt đầu. |
Before Alpha | Độ trong suốt trước khi bắt đầu. |
After Alignment | Vị trí tương đối so với container sau khi kết thúc. |
After Scale | Scale khi kết thúc. |
After Alpha | Độ trong suốt khi kết thúc. |
Triển khai animation tương tác với màn hình đối tác
Bạn cũng có thể tạo animation tham chiếu đến trạng thái của màn hình đối tác. Trong ví dụ dưới đây hình ảnh của modal trước đó được phóng to trong trong khi chuyển tiếp liền mạch sang modal tiếp theo.

Để triển khai nó, đầu tiên tạo một lớp kế thừa từ TransitionAnimationObject hoặc TransitionAnimationBehaviour.
Sau đó, tham thiếu tới thuộc tính PartnerRectTransform để lấy màn hình đối tác.
Nếu màn hình đối tác không tồn tại PartnerRectTransform sẽ trả về null.
Xem CharacterImageModalTransitionAnimation để biết cách triển khai chi tiết.
Tạo animation với Timeline.
Bạn có thể sử dụng timeline để tạo animation. Chúng tôi khuyên bạn nên sử dụng timeline cho nhưng animation phức tạp.

Để triển khai, đầu tiên thêm TimelineTransitionAnimationBehaviour vào GameObject. Sau đó gắn PlayableDirector và TimelineAsset cho các thuộc tính.
Chú ý bỏ chọn PlayOnAwake của PlayableDirector
Cuối cùng, gán TimelineTransitionAnimationBehaviour vào AnimationContainer.
Ngoài ra, chúng tôi đề xuất bạn sử dụng UnityUIPlayables để tạo uGUI animation với Timeline.

Các sự kiện vòng đời (Lifecycle Events)
Sự kiện vòng đời của page
using System.Collections;
using UnityScreenNavigator.Runtime.Core.Page;
public class SomePage : Page
{
// Called just after this page is loaded.
public override IEnumerator Initialize() { yield break; }
// Called just before this page is released.
public override IEnumerator Cleanup() { yield break; }
// Called just before this page is displayed by the Push transition.
public override IEnumerator WillPushEnter() { yield break; }
// Called just after this page is displayed by the Push transition.
public override void DidPushEnter() { }
// Called just before this page is hidden by the Push transition.
public override IEnumerator WillPushExit() { yield break; }
// Called just after this page is hidden by the Push transition.
public override void DidPushExit() { }
// Called just before this page is displayed by the Pop transition.
public override IEnumerator WillPopEnter() { yield break; }
// Called just after this page is displayed by the Pop transition.
public override void DidPopEnter() { }
// Called just before this page is hidden by the Pop transition.
public override IEnumerator WillPopExit() { yield break; }
// Called just after this page is hidden by the Pop transition.
public override void DidPopExit() { }
}Bạn cũng có thể đăng ký thêm sự kiện bằng Page.AddLifecycleEvents() như sau.
// IPageLifecycleEvent is the interface that has lifecycle events described above.
// You can specify the execution priority with the second argument.
// Less than 0: executed before Page lifecycle event.
// Greater than 0: executed after Page lifecycle event.
IPageLifecycleEvent lifecycleEventImpl;
Page page;
page.AddLifecycleEvent(lifecycleEventImpl, -1);
// It is also possible to register only some lifecycle events as follows.
IEnumerator OnWillPushEnter()
{
// Some code.
yield break;
}
page.AddLifecycleEvent(onWillPushEnter: OnWillPushEnter);Và bạn cũng có thể móc nối các sự kiện từ container bằng cách chuyển một object triển khai interface IPageContainerCallbackReceiver tới PageContainer.AddCallbackReceiver().
public interface IPageContainerCallbackReceiver
{
// Called just before the Push transition is executed.
void BeforePush(Page enterPage, Page exitPage);
// Called just after the Push transition is executed.
void AfterPush(Page enterPage, Page exitPage);
// Called just before the Pop transition is executed.
void BeforePop(Page enterPage, Page exitPage);
// Called just after the Pop transition is executed.
void AfterPop(Page enterPage, Page exitPage);
}Chú ý nếu bạn triển khai IPageContainerCallbackReceiver với MonoBehaviour và gán nó vào GameObject của container, nó sẽ đăng ký với PageContainer mà không cần gọi PageContainer.AddCallbackReceiver().
Các sự kiện vòng đời của Modal
Bằng cách ghi đè các phương thức dưới đây trong class mà kế thừa lớp Modal. Bạn có thể viết lại các xử lý liên quan đến vòng đời của modal.
using System.Collections;
using UnityScreenNavigator.Runtime.Core.Modal;
public class SomeModal : Modal
{
// Called just after this modal is loaded.
public override IEnumerator Initialize() { yield break; }
// Called just before this modal is released.
public override IEnumerator Cleanup() { yield break; }
// Called just before this model is displayed by the Push transition.
public override IEnumerator WillPushEnter() { yield break; }
// Called just after this modal is displayed by the Push transition.
public override void DidPushEnter() { }
// Called just before this modal is hidden by the Push transition.
public override IEnumerator WillPushExit() { yield break; }
// Called just after this modal is hidden by the Push transition.
public override void DidPushExit() { }
// Called just before this modal is displayed by the Pop transition.
public override IEnumerator WillPopEnter() { yield break; }
// Called just after this modal is displayed by the Pop transition.
public override void DidPopEnter() { }
// Called just before this modal is hidden by the Pop transition.
public override IEnumerator WillPopExit() { yield break; }
// Called just after this modal is hidden by the Pop transition.
public override void DidPopExit() { }
}Bạn cũng có thể đăng ký thêm các sự kiện với Modal.AddLifecycleEvents()
// IModalLifecycleEvent is the interface that has lifecycle events described above.
// You can specify the execution priority with the second argument.
// Less than 0: executed before Modal lifecycle event.
// Greater than 0: executed after Modal lifecycle event.
IModalLifecycleEvent lifecycleEventImpl;
Modal modal;
Modal.AddLifecycleEvent(lifecycleEventImpl, -1);
// It is also possible to register only some lifecycle events as follows.
IEnumerator OnWillPushEnter()
{
// Some code.
yield break;
}
modal.AddLifecycleEvent(onWillPushEnter: OnWillPushEnter);Và bạn cũng có thể móc nối các sự kiện từ container bằng cách chuyển một object triển khai interface IModalContainerCallbackReceiver tới ModalContainer.AddCallbackReceiver().
public interface IModalContainerCallbackReceiver
{
// Called just before the Push transition is executed.
void BeforePush(Modal enterModal, Modal exitModal);
// Called just after the Push transition is executed.
void AfterPush(Modal enterModal, Modal exitModal);
// Called just before the Pop transition is executed.
void BeforePop(Modal enterModal, Modal exitModal);
// Called just after the Pop transition is executed.
void AfterPop(Modal enterModal, Modal exitModal);
}Chú ý rằng nếu bạn triển khai IModalContainerCallbackReceiver cho một MonoBehaviour và gán nó vào GameObject của Container, nó sẽ được đăng ký với ModalContainer mà không cần gọi ModalContainer.AddCallbackReceiver().
Các sự kiện vòng đời của Sheet
Tương tự như trên bạn có thể ghi đè các phương thức sau của lớp Sheet để viết lại các xử lý liên quan đến vòng đời của Sheet.
using System.Collections;
using UnityScreenNavigator.Runtime.Core.Sheet;
public class SomeSheet : Sheet
{
// Called just after this sheet is loaded.
public override IEnumerator Initialize() { yield break; }
// Called just before this sheet is released.
public override IEnumerator Cleanup() { yield break; }
// Called just before this sheet is displayed.
public override IEnumerator WillEnter() { yield break; }
// Called just after this sheet is displayed.
public override void DidEnter() { }
// Called just before this sheet is hidden.
public override IEnumerator WillExit() { yield break; }
// Called just after this sheet is hidden.
public override void DidExit() { }
}Bạn cũng có thể đăng ký thêm sự kiện với Sheet.AddLifecycleEvents().
// ISheetLifecycleEvent is the interface that has lifecycle events described above.
// You can specify the execution priority with the second argument.
// Less than 0: executed before Sheet lifecycle event.
// Greater than 0: executed after Sheet lifecycle event.
ISheetLifecycleEvent lifecycleEventImpl;
Sheet sheet;
Sheet.AddLifecycleEvent(lifecycleEventImpl, -1);
// It is also possible to register only some lifecycle events as follows.
IEnumerator OnWillEnter()
{
// Some code.
yield break;
}
sheet.AddLifecycleEvent(onWillEnter: OnWillEnter);Và bạn cũng có thể móc nối các sự kiện từ container bằng cách chuyển một object triển khai interface ISheetContainerCallbackReceiver tới SheetContainer.AddCallbackReceiver().
public interface ISheetContainerCallbackReceiver
{
// Called just before the Show transition is executed.
void BeforeShow(Sheet enterSheet, Sheet exitSheet);
// Called just after the Show transition is executed.
void AfterShow(Sheet enterSheet, Sheet exitSheet);
// Called just before the Hide transition is executed.
void BeforeHide(Sheet exitSheet);
// Called just after the Hide transition is executed.
void AfterHide(Sheet exitSheet);
}Và nếu bạn triển khai ISheetContainerCallbackReceiver với MonoBehaviour và gán nó với GameObject của container thì nó cũng sẽ được đăng ký với SheetContainer đó mà không cần gọi SheetContainer.AddCallbackReceiver().
Sử dụng phương thức async thay cho coroutines
Bạn có thể sử dụng phương thức bất đồng bộ thay cho coroutines để định nghĩa các sự kiện vòng đời như bên dưới.
using System.Threading.Tasks;
using UnityScreenNavigator.Runtime.Core.Page;
public class SomePage : Page
{
// Using asynchronous methods to define lifecycle events
public override async Task Initialize()
{
await Task.Delay(100);
}
}Để sử dụng phương thức bất đồng bộ, hãy thêm Scripting Define Symbols như sau.
Player Settings > Other Settings > Scripting Define Symbols- Thêm
USN_USE_ASYNC_METHODS
Chú ý rằng Scripting Define Symbols cần được set cho tất cả các nền tảng.
Load Screen Resource
Thay đổi phương thức load màn hình
Như đã nói ở trên, mặc định thì các màn hình sẽ được đặt trong folder Resources như một Prefab.
Nếu bạn muốn thay đổi phương thức tải. Đầu tiên hãy tạo Scriptalbe Object kế thừa AssetLoaderObject. AssetLoaderObject là một triển khai của IAssetLoader và có phương thức sau.
// Load the resource indicated by the key.
public abstract AssetLoadHandle<T> Load<T>(string key) where T : Object;
// Asynchronously load the resource indicated by the key.
public abstract AssetLoadHandle<T>
// Release the resource indicated by the handle.
public abstract void Release(AssetLoadHandle handle);Xem thêm ResourcesAssetLoader để biết chi tiết cách triển khai.
Sau khi tạo, gán nó vào thuộc tính AssetLoader của UnityScreenNavigatorSettings.
Bạn có thể tạo UnityScreenNavigatorSettings từ Assets > Create > Screen Navigator Settings.
Bạn cũng có thể cài IAssetLoader cho mỗi container bằng cách set thuộc tính AssetLoader cho mỗi Container.
Sử dụng Addressable Asset System
Một triển khai của IAssetLoader cho Addressable Asset System được cung cấp mặc định. Nếu bạn muốn load màn hình với address. Thực hiện các bước sau:
- Chọn
Assets > Create > Resource Loader > Addressable Asset Loader - Gán
ScriptableObjectvừa được tạo vào thuộc tínhAssetLoadercủaUnityScreenNavigatorSettings.
Tải đồng bộ (Load synchronously)
Bạn có thể truyền giá trị false cho tham số loadAsync của mỗi phương thức chuyển tiếp của container để tải đồng bộ các màn hình.
Ví dụ, PageContainer.Push() có thể được viết như sau:
PageContainer container;
// Load synchronously.
var handle = container.Push("FooPage", true, loadAsync: false);
// Wait for the end of the transition animation.
yield return handle;Ngoài ra bạn cũng có thể khởi tạo nó trong cùng frame với lệnh gọi phương thức chuyển đồi bằng cách sử dụng callback onLoad.
PageContainer container;
// Load synchronously and receive the callback after loading.
var handle = container.Push("FooPage", true, loadAsync: false, onLoad: x =>
{
// Initialize page. (Called in the same frame as Push)
x.page.Setup();
});
// Wait for the end of the transition animation.
yield return handle;Chú ý nếu bạn sử dụng AddressableAssetLoader và tải đồng bộ, thì bạn cần sử dụng Addressable phiên bản 1.17.4 hoặc cao hơn.
Ngoài ra, đây là một số cảnh báo về hiệu năng khi sử dụng Addressable Addressable’s specifications (trong trường hợp sử dụng Addressable cùng với tải đồng bộ).
Tải trước
Page và modal chỉ được tải sau khi một chuyển tiếp màn hình được được gọi. Khi tải một tài nguyên lớn nó có thể mất nhiều thời gian làm chuyển tiếp không còn mượt.
Trong trường hợp đó, việc tải trước tài nguyên là rất hữu ích.
Dưới đây là ví dụ tải trước với PageContainer.
const string pageName = "FooPage";
PageContainer container;
// Preload FooPage.
var preloadHandle = container.Preload(pageName);
// Wait for the end of the preloading.
yield return preloadHandle;
// Transition smoothly because FooPage has been preloaded.
container.Push(pageName, true);
// Release the preloaded FooPage.
container.ReleasePreloaded(pageName);Xem thêm HomePage in demo để biết cách triển khai chi tiết.
Khi Home được khởi tạo. Shop cũng được load và destroy cùng lúc.
Các tính năng khác
Đóng nhiều màn hình cùng lúc
Trong PageContainer và ModalContainer, bạn có thể đóng nhiều màn hình cùng lúc. Để làm vậy hãy chỉ định số màn hình cần đóng trong tham số thứ 2 của PageContainer.Pop() hoặc ModalContainer.Pop().
PageContainer pageContainer;
pageContainer.Pop(true, 2);
ModalContainer modalContainer;
modalContainer.Pop(true, 2);Bạn cũng có thể chỉ định PageID hoặc ModalID. PageID và ModalID có thể được lấy bằng cách sử dụng hàm callback onLoad của Push() như sau
PageContainer pageContainer;
pageContainer.Push("fooPage", true, onLoad: x =>
{
var pageId = x.pageId;
});
ModalContainer modalContainer;
modalContainer.Push("fooModal", true, onLoad: x =>
{
var modalId = x.modalId;
});Ngoài ra bạn có thể chỉ định bất kì ID nào bằng cách chỉ định tham số pageID hoặc modalID của hàm Push()
PageContainer pageContainer;
pageContainer.Push("fooPage", true, pageId: "MyPageID");
ModalContainer modalContainer;
modalContainer.Push("fooModal", true, modalId: "MyModalID");Đối với các page hoặc modal bị bỏ qua khi đóng nhiều màn hình. Các sự kiện vòng đời trước và sau khi chuyển tiếp sẽ không được gọi. Chỉ sự kiện trước khi destroy được gọi.
Trong PageContainer, animation của các page bị bỏ qua sẽ không chạy.
Trong ModalContainer, animation khi đóng các modal bị bỏ qua sẽ được phát đồng thời.
Không stack page trong lịch sử
Đó là một số page mà bạn không muốn quay lại, ví dụ như màn hình loading.
Trong trường hợp này, bạn có thể chặn page stack trong lịch sử bằng cách khai báo false cho tham số stack trong hàm PageContainer.Push().
Phiên bản này của page sẽ bị huỷ sau khi chuyển sang page tiếp theo nên do đó bị bỏ qua khi quay trở lại.
PageContainer container;
// Transition to FooPage without stacking it in the history.
yield return container.Push("FooPage", true, stack: false);
// Transition to BarPage, and FooPage is disposed.
yield return container.Push("BarPage", true);
// When Pop, it does not back to FooPage, but to the page before it.
yield return container.Pop(true);Xem thêm TopPage in demo để biết cách triển khai chi tiết.
Chuyển sang page loading mà không cần stacking.
Thay đổi phông nền của modal
Theo mặc định, phông nền là một màn hình đen trong suốt. Bạn có thể thay đổi nó trong settings.
Để thay đổi nó bạn cần thêm component ModalBackdrop cho phông nền của modal và kéo nó thành Prefab.
Sau đó, gán prefab này như một phông nên của modal.
Để thay đổi phông nền của cả ứng dụng, hãy gán nó vào ModalBackdropPrefab trong UnityScreenNavigatorSettings.
Bạn cũng có thể đặt phông nền cho mỗi ModalContainer bằng cách gán prefab vào OverrideBackdropPrefab của mỗi ModalContainer.
Đóng modal đang hiển thị bằng cách click vào phông nền.
Mặc định, phông nền không thể click.
Nếu bạn muốn đóng modal khi click vào phông nền, đầu tiên hãy tạo một phông nền mới thay cho phông nên mặc định theo các bước trên.
Sau đó, check CloseModalWhenClicked trong component ModalBackdrop.
Cho phép tương tác khi đang chuyển màn hình.
Từ khi bắt đầu cho đến khi kết thúc chuyển màn hình, mọi tương tác đều bị vô hiệu hoá.
Bạn có thể thay đổi settings này bằng cách thay đổi thuộc tính EnableInteractionInTransition và ControlInteractionsOfAllContainers của UnityScreenNavigatorSettings. Mặc định, EnableInteractionInTransition là false và ControlInteractionsOfAllContainers là true.
Để cho phép tương tác khi chuyển màn hình, set EnableInteractionInTransition thành true. Và nếu bạn muốn vô hiệu hoá chỉ với container hiện tại đang chuyển, hãy giữ nguyên EnableInteractionInTransition là false và set ControlInteractionsOfAllContainers thành false.
Tuy nhiên, không thể chuyển sang một màn hình khác khi đang chuyển trong một container. Do đó hãy kiểm soát thời gian chuyển đổi một cách thích hợp nếu bạn bật tương tác.
Tắt mask cho container
Mặc định, một phần của màn hình bên ngoài container bị che đi.
Nếu bạn muốn hiển thị màn hình bên ngoài container disable component RectMask2D của GameObject container.
Lấy thông tin của animation đang chạy
Bạn có thể lấy thông tin của animation đang chạy từ các thuộc tính của lớp Page, Modal, Sheet.
Thuộc tính | Mô tả |
IsTransitioning | Có đang trong quá trình chuyển màn hình hay không. |
TransitionAnimationType | Loại animation, nếu không có hiệu ứng chuyển sẽ trả về null |
TransitionAnimationProgress | Tiến trình của animation. |
TransitionAnimationProgressChanged | Sự kiện khi tiếng tình của animation thay đổi. |
Sử dung Prefab đã load khi tải màn hình.
PreloadedAssetLoaderObject cho phép bạn tải trực tiếp các prefab đã được tải trước thay vì sử dung Resource hoặc Addressable khi tải các màn hình. Bạn có thể sử dụng bằng cách nhập key và prefab vào ScriptableObject tạo từ Assets > Create > Resource Loader > Preloaded Asset Loader.
Tôi cũng đã cung cấp triển khai PreloaderAssetLoader cho runtime.
Thay đổi hành vi của phông nền.
Bạn có thể hay đổi hành vi của phông nền bằng cách thay đổi BackdropStrategy từ ModalContainer.
Giá trị | Mô tả |
|---|---|
Generate Per Modal | Tạo một phông nền cho môi modal. |
Only First Backdrop | Tạo một phông nên cho modal đầu tiên và không thêm phông nền cho bất kỳ modal nào tiếp theo. |
Change Order Before Animation | Thay đổi thứ tự render của phông nên đầu tiên được tạo và sử dụng lại nó khi tạo modal thứ 2. (Điều chỉnh thứ tự render trước animation) |
Change Order After Animation | Thay đổi thứ tự render của phông nên đầu tiên được tạo và sử dụng lại nó khi tạo modal thứ 2. (Điều chỉnh thứ tự render sau animation) |
FAQ
Làm sao để tạo screen với Scene thay vì Prefab?
Bạn có thể tải màn hình được đặt trong Scene bằng cách triển khai AssetLoader. Triển khai interface IAssetLoader để tải Scene chứa màn hình được yêu cầu và trẻ về GameObject của màn hình. Xem Thay đổi phương thức load màn hình để biết thêm chi tiết.
Làm sao để tách biệt view và logic?
Tham khảo blog sau (viết bằng tiếng Nhật) https://light11.hatenadiary.com/entry/2022/01/11/193925.
Làm sao để chuyển data giữa các màn hình?
Đầu tiên, trong một ví dụ, data được chuyển đến màn hình khi demo scene được tải xong. UnityScreenNavigator/Assets/Demo/Scripts/CharacterModal.cs
Tuy nhiên, có nhiều cách để truyền dữ liệu. Ví dụ có thể bạn muốn sử dụng một DI Container (Dependency Injection) để set data. Do đó thư viện này không triển khai hoặc áp đặt một cách cụ thể nào?
Làm sao để tái sử dụng page và modal?
Đóng một page hoặc modal sẽ ngay lập tức destroy chúng và không thể tái sử dụng.
Bản chất của mong muốn tái sử dụng có thể chia thành 2 loại sau:
- Không muốn tải lại tài nguyên mỗi lần.
- Muốn duy trì trạng thái của màn hình.
Trong trường hợp này, vấn để thời gian tải có thể được giải quyết bằng tải trước. Đối với việc duy trì trạng thái, để dễ bảo trì, trạng thái và giao diện nên được tách rời để có thể được tái tạo.
Ngoài ra, nhìn chung, từ quan điểm người dùng, chuyển đổi màn hình kiểu tab là kiểu nên giữ nguyên trạng thái. Trong thư viện này cũng vậy, trạng thái luôn được bảo toàn giữa các lần chuyển đổi Sheet để triển khai các tab. Xem tạo sheet để biết thêm chi tiết.
Và cuối cùng nếu tải sử dụng, người dùng có thể sẽ cần tự quản lý vòng đời. Nói cách khác, khi không cần thiết nữa người dùng phải gọi hàm Cleanup để huỷ các instance và dọn dẹp bộ nhớ.
Giấy phép
Phần mềm này được phát hành theo giấy phép MIT License.
Bạn có thể tự do sử dụng trong phạm vi của giấy phép.
Tuy nhiên, các thông báo bản quyền và giấy phép sau đây phải được bao gồm khi sử dụng.
https://github.com/Haruma-K/UnityScreenNavigator/blob/master/LICENSE.md
Lý do USN được công khai dưới dạng mã nguồn mở và ý định thiết kế
Lời nói đầu
Ngày 26/10/2021, Haruki Yano đã release Unity Screen Navigator dưới dạng mã nguồn mở.

Những vấn đề kỹ thuật đã được tác giả viết trong file README.md. Vậy nên trong bài viết này tác giả sẽ tập trung vào lý do mà USN được công khai dưới dạng mã nguồn mở và các vấn đề liên quan đến thiết kế. Ngoài ra, lưu ý rằng bài viết có thể chứa nhiều ý kiến cá nhân của tác giả.
Unity Screen Navigator (USN) là gì?
USN là một thư viện để thực hiện các chức năng chuyển đổi màn hình, hoạt ảnh chuyển đổi màn hình, quản lý lịch sử chuyển đổi và quản lý vòng đời các màn hình trong Unity.
Tính năng:
- Chuyển đổi màn hình dễ dàng và linh hoạt.
- Quản lý vòng đời màn hình và bộ nhớ.
- Có thể triển khai các hoạt ảnh phức tạp.
- Các tính năng khác như quản lý lịch sử, ngăn chặn click...
Tại sao USN được công khai dưới dạng mã nguồn mở?
Lý do mà tác giả tham gia các hoạt động OSS không chỉ là để nâng cao kỹ năng chuyên môn hay phát triển sự nghiệp mà hơn thế, Haruki Yano luôn băn khoăn về thực trạng các cá nhân và công ty đều đang tự mình phát triển những thứ phổ biến và giống hệt nhau.
Dĩ nhiên, những bí quyết và công nghệ hiếm có thể tạo ra lợi thế cạnh tranh thì không nên công khai, nhưng Haruki Yano nghĩ những thứ phổ biến và có tính ứng dụng chung thì nên được chia sẻ rộng rãi hơn. Làm như vậy các công ty và cá nhân có thể được hưởng lợi từ việc giảm bớt công sức đầu tư, nhưng lợi ích cũng chỉ dừng lại ở đó mà thôi.
Haruki Yano có một mong muốn rằng, thay vì phải lo lắng về những điều nhỏ nhặt như vậy thì mọi ngưỡi nên gọi "lợi thế cạnh tranh" là khả năng tạo ra những tựa game thú vị hơn và có giá trị cao hơn.
Một lần nữa Haruki Yano tin rằng các hoạt động OSS mang lại nhiều lợi ích cho cả cá nhân lẫn doanh nghiệp.
Tại sao lại là USN?
Đơn giản là vì Haruki Yano muốn dùng nó.
Bản thân Haruki Yano cũng là người luôn muốn tận dụng những gì có sẵn, nên ông đã tìm kiếm với hy vọng rằng chắc chắn sẽ có một thư viện chuyển màn hình nào đó thật sự tốt. Tuy nhiên, kể cả các sản phẩm trả phí, số lượng thư viện liên quan đến chuyển màn hình được công bố rộng rãi lại rất ít.
Haruki Yano nghĩ rằng do các yêu cầu kỹ thuật quá đa dạng khiến cho việc tạo một thư viện chung có thể đáp ứng được tất cả trở nên khó khăn. Thêm vào đó, ông cảm giác rằng các game di động của châu Á thường có các kiểu chuyển màn hình phức tạp, nên như cầu có lẽ không đồng đều mà tập trung ở một số khu vực nhất định.
Cũng với lý do tương tự, Haruki Yano nghĩ rằng có thể mong đợi một engine phổ biến như Unity sẽ bổ sung thêm các tính năng như thế này.
Từ đó, Haruki Yano quyết định sẽ tự mình xây dựng USN. Điểm hay của việc tự phát triển là có thể tự do quyết định các yêu cầu kỹ thuật. Ông cho rằng nếu mình giới hạn các yêu cầu ở một mức độ nhất định thì trong phạm vi đó ông có thể tạo ra một thư viện phổ biến.
Hơn nữa, Haruki Yano nghĩ rằng một thư viện như USN cũng sẽ mang lại giá trị đủ lớn cho cộng đồng nên ông đã quyết định phát triển nó với mục tiêu biến nó thành mã nguồn mở.
Ý nghĩa của việc xác định và tài liệu hóa các quy tắc chuyển màn hình.
Việc đặt ra các điều kiện tiên quyết cũng có lợi ích là các thông số kỹ thuật của thư viện sẽ trở thành các quy tắc cho việc chuyển màn hình.
Ví dụ, hãy xem xét một dự án mà ở đó việc chuyển màn hình do UI designer quyết định và programmer là người triển khai. Nếu không có quy tắc nào được đặt ra programmer sẽ phải lường trước tất cả những thay đổi trong tương lai và thiết kế một hệ thống tổng quát hơn so với những yêu cầu đã nhận. Tuy nhiên, việc những thay đổi đó vượt ra ngoài dự tính vẫn thường xuyên xảy ra.
Nếu có các quy tắc ngay từ đầu thì cả, UI Designer và Programmer chỉ cần làm việc trong khuôn khổ đó. Hơn nữa, nếu các quy tắc này được áp dụng như một chuẩn chung cho nhiều dự án thì khi chuyển sang một dự án mới dev cũng không cần phải xác định hay tìm hiểu lại các quy tắc.
Ngoài ra, dù đã có quy tắc nhưng nếu chúng không được thể hiện một cách rõ ràng, dễ hiểu để dev nắm bắt thì quy tác cũng sẽ trở nên vô nghĩa.
Chỉ khi các quy tắc này được tài liệu hóa và truyền đạt một cách bài bản thì các cuộc thảo luận về việc phát triển ứng dụng dựa trên các quy tắc mới có thể diễn ra.
Vì vậy, Haruki Yano tin rằng việc xác định và tài liệu hóa các quy tắc sẽ mang lại lợi ích to lớn cho quá trình phát triển.
Thư viện chuyển màn hình và mức độ trừu tượng.
Khi tiến hành triển khai, Haruki Yano đã rất băn khoăn về việc nên xây dựng thư viện này với cấu trúc và mức độ trừu tượng như thế nào.
Nếu muốn khái quát hóa một cách triệt để thì một thư viện chuyển màn hình sẽ trở thành một thư viện "chuyển cái gì đó bằng cách nào đó tới phần nào đó ở màn hình".
Ở đây, thứ gì đó có thể là Prefab, Scene,... còn bằng cách nào đó không chỉ dừng lại ở animation mà có thể material, effect...
Ta hoàn toàn có thể triển khai chức năng "chuyển cái gì đó bằng cách nào đó tới phần nào đó ở màn hình" như một lớp thực thi ở tầng thấp rồi xây dựng các tính năng khác trên đó, nhưng Haruki Yano quyết định không làm vậy vì nó sẽ trở nên khó hiểu và hơn hết là bản thân Haruki Yano không cần đến nó.
Kết quả là Haruki Yano quyết định xây dựng một thư viện cho uGUI, chủ yếu hỗ trợ prefab và được thiết kế ở mức mà các scene có thể sử dụng bằng cách mở rộng thêm. Haruki Yano cũng quyết định sẽ không xét đến những hiệu ứng chuyển cảnh đặc thù như material animation hay post-effect.
Sau cùng, tác giả nghĩ rằng ông đã đạt được mức độ trừu tượng vừa phải, có thể đáp ứng được khoảng 80% các yêu cầu kỹ thuật thông thường. Tất nhiên con số 80% cũng chỉ là áng chừng.
Sự chú trọng vào việc tách biệt workflow.
Một điểm nữa mà Haruki Yano chú trọng là tách biệt workflow.
Haruki Yano không thích luồng làm việc mà programmer sử dụng các thư viện Tween để tạo các animation. Khi làm việc với UI designer hoặc animator chuyên nghiệp, Haruki Yano vừa kinh ngạc trước chất lượng và sự tỉ mỉ trong các tác phẩm của họ vừa cảm thấy rằng programmer không nên bước chân vào lĩnh vực đó với kiến thức và kỹ năng nửa vời.
Ngoài ra, workflow mà UI designer, animator đưa ra hướng dẩn cho programmer thực hiện cũng trở thành lãng phí và gây căng thẳng cho cả 2 bên. Để tạo ra một sản phẩm tốt, giao tiếp là điều quan trọng, những việc xây dựng một workflow giúp loại bỏ những trao đổi không cần thiết cũng quan trọng không kém.
Dĩ nhiên, không thể tránh khỏi trường hợp mà programmer cần phải làm animation, nhưng workflow vẫn cần tách biệt nhiều nhất có thể.
Với các lý do dó, Haruki Yano đã thiết kế thư viện này với các yêu cầu cho phép tách biệt rõ ràng workflow.
Phương pháp tách biệt View và Logic
Ý nghĩa của việc tách việt View và Logic.
Nhìn chung, giao diện người dùng (UI) của game thường rất phức tạp với nhiều yếu tố và trạng thái quan trọng. Hơn nữa, chúng còn sử dụng nhiều animation, đòi hỏi sự tinh chỉnh tỉ mỉ và thường xuyên phải làm lại do thay đổi yêu cầu.
Trong bối cảnh đó, để xây dựng UI một cách hiệu quả và dễ bảo trì, chúng ta cần phải xây dựng một workflow cho phép kiểm tra ngay lập tức các UI đã tạo, chỉnh sửa rồi lại kiểm tra.
Áp dụng điều này vào bối cảnh chuyển màn hình "out-game" (các phần ngoài gameplay chính như menu, shop...) điều đó có nghĩa là chúng ta cần xây dựng một môi trường cho phép dùng dữ liệu giả (dummy data) để kiểm tra hoạt động ngay lập tức ở cấp độ từng màn hình mà không cần phải chạy ứng dụng từ đầu.
Để làm được điều đó, cần phải tách biệt View khỏi Logic và xây dựng một nền tảng cho phép truyền dummy data vào View.
Vì vậy trong bài viết này, Haruki Yano giới thiệu về phương pháp tách biệt View và Logic bằng cách sử dụng USN
Mô tả màn hình sẽ tạo trong bài viết này.
Trong bài viết này chúng ta sẽ tạo một màn hình với 3 ảnh được bố trí như sau:
- Ảnh bên trái (FIXED) là hình ảnh luôn được hiển trị ở trang này.
- Ảnh ở giữa (LOCAL) sẽ ngẫu nhiên tải và hiển thị một hình ảnh từ LOCAL 01 đến LOCAL 03 được lưu trên máy
- Ảnh bên phải (REMOTE) sẽ tải và hiển thị ngẫu nhiên một hình ảnh từ REMOTE 01 đến REMOTE 03 được lấy trên mạng về.

Mỗi hình ảnh là một button, khi nhấn vào sẽ mở ra một modal tương ứng.

Đầu tiên, viết toàn bộ vào View.
Trước tiên hãy thử triển khai mô tả trên bằng cách viết tất cả vào View.
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using UnityScreenNavigator.Runtime.Core.Modal;
using UnityScreenNavigator.Runtime.Core.Sheet;
namespace ViewLogicSeparation.Scripts
{
public class MainSheet : Sheet
{
[SerializeField] private Button _fixedButton;
[SerializeField] private Button _localButton;
[SerializeField] private Button _remoteButton;
[SerializeField] private RawImage _fixedImage;
[SerializeField] private RawImage _localImage;
[SerializeField] private RawImage _remoteImage;
// Initialization process
public override IEnumerator Initialize()
{
_fixedImage.texture = Resources.Load<Texture>("tex_button_fixed");
_fixedButton.onClick.AddListener(OnFixedButtonClicked);
_localButton.onClick.AddListener(OnLocalButtonClicked);
_remoteButton.onClick.AddListener(OnRemoteButtonClicked);
yield break;
}
// Cleanup process
public override IEnumerator Cleanup()
{
_fixedImage.texture = null;
_fixedButton.onClick.RemoveListener(OnFixedButtonClicked);
_localButton.onClick.RemoveListener(OnLocalButtonClicked);
_remoteButton.onClick.RemoveListener(OnRemoteButtonClicked);
yield break;
}
// Processing before this screen is displayed
public override IEnumerator WillEnter()
{
_localImage.texture = Resources.Load<Texture>($"tex_button_local_{Random.Range(1, 4):D2}");
// Download and get the texture
var url = $"http://foo/bar/tex_button_remote_{Random.Range(1, 4):D2}.png";
var uwr = UnityWebRequestTexture.GetTexture(url);
yield return uwr.SendWebRequest();
_remoteImage.texture = ((DownloadHandlerTexture)uwr.downloadHandler).texture;
}
// Processing before this screen is hidden
public override void DidExit()
{
_localImage.texture = null;
_remoteImage.texture = null;
}
private void OnFixedButtonClicked()
{
// When the Fixed button is clicked, open Modal01
ModalContainer.Find("Main").Push("Modal01", true);
}
private void OnLocalButtonClicked()
{
// When the Local button is clicked, open Modal02
ModalContainer.Find("Main").Push("Modal02", true);
}
private void OnRemoteButtonClicked()
{
// When the Remote button is clicked, open Modal03
ModalContainer.Find("Main").Push("Modal03", true);
}
}
}Tạo View và ModalContainer một cách phù hợp sau đó gán các tham chiếu của chúng vào các trường [SerializeField]
Tiếp theo, chỉ cần dùng SheetContainer.Show() để hiển thị sheet này là bạn có thể xác nhận rằng nó hoạt động bình thường
Những vấn đề của cách triển khai trên
Bây giờ, hãy giả sử rằng màn hình chúng ta đã tạo ở phần trước chỉ là một trong số rất nhiều màn hình của một ứng dụng lớn, và đó là một màn hình mà phải đi qua nhiều màn hình mới có thể chuyển tới được.
Khi muốn kiểm tra hoạt động của màn hình này, nếu lần nào cũng phải đi từ màn hình đầu tiên của ứng dụng thì sẽ rất tốn thời gian.
Hơn nữa, ví dụ nếu như màn hình đăng nhập không hoạt động bình thường, chúng ta thậm chí còn không thể đến được màn hình mục tiêu.
Đây là một vấn đề vền mặt thiết kế do đã không cân nhắc đến workflow và quy trình phát triển. Có thể tưởng tượng rằng nó sẽ trở thành một món nợ kỹ thuật lớn khi quy mô phát triển ngày càng tăng. Ngay cả khi đã viết unit test hoàn hảo cho logic thì phần view như thế này vẫn là một giải pháp nửa vời.
Để ngăn chặn tình huống này, chúng ta sẽ xây dựng một môi trường cho phép đặt Prefab đã gắn MainScreen ở trên vào một Scene riêng lẻ và chạy nó một cách độc lập.
Tuy nhiên, khi kiểm tra hoạt động một cách độc lập, cách triển khai MainScreen hiện tại có những vấn đề sau:
- Không thể kiểm tra hoạt động nếu tất cả LOCAL 1 - 3 không tồn tại
- Không thể kiểm tra hoạt động nếu các modal đích bị lỗi hoặc chưa được triển khai.
- Không thể kiểm tra hoạt động nếu taatsa cả các ảnh được chỉ định bởi URL (REMOTE 1 - 3) không tồn tại.
Điều này có nghĩa là việc kiểm tra đang bị ảnh hưởng bởi môi trường xung quanh. Do đó, trong phần tiếp theo chúng ta sẽ tách View và Logic để việc kiểm tra các hoạt động của màn hình diễn ra một cách độc lập hơn.
Tách biệt View và Logic
Trước tiên, chúng ta sẽ bắt đầu tách phần logic đang cản trở việc kiểm tra hoạt động ra khỏi View.
Để giải quyết vấn đề đã nêu trên, chúng ta sẽ thiết kế sao cho URL và image path của hình ảnh được cung cấp từ bên ngoài, Khi người dùng nhấp chuột, nó chỉ thông báo một sự kiện ra bên ngoài.
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;
using UnityScreenNavigator.Runtime.Core.Sheet;
namespace ViewLogicSeparation.Scripts
{
public class MainSheet : Sheet
{
[SerializeField] private Button _fixedButton;
[SerializeField] private Button _localButton;
[SerializeField] private Button _remoteButton;
[SerializeField] private RawImage _fixedImage;
[SerializeField] private RawImage _localImage;
[SerializeField] private RawImage _remoteImage;
public event UnityAction FixedButtonClicked;
public event UnityAction LocalButtonClicked;
public event UnityAction RemoteButtonClicked;
private string _localImagePath;
private string _remoteImageUrl;
// Set the path for the LOCAL image and the URL for the REMOTE image from the outside.
public void Setup(string localImagePath, string remoteImageUrl)
{
_localImagePath = localImagePath;
_remoteImageUrl = remoteImageUrl;
}
public override IEnumerator WillEnter()
{
_localImage.texture = Resources.Load<Texture>(_localImagePath);
// Download and get the texture
var uwr = UnityWebRequestTexture.GetTexture(_remoteImageUrl);
yield return uwr.SendWebRequest();
_remoteImage.texture = ((DownloadHandlerTexture)uwr.downloadHandler).texture;
}
// Initialization process
public override IEnumerator Initialize()
{
_fixedImage.texture = Resources.Load<Texture>("tex_button_fixed");
// Just notify the click event
_fixedButton.onClick.AddListener(FixedButtonClicked);
_localButton.onClick.AddListener(LocalButtonClicked);
_remoteButton.onClick.AddListener(RemoteButtonClicked);
yield break;
}
// Cleanup process
public override IEnumerator Cleanup()
{
_fixedImage.texture = null;
_fixedButton.onClick.RemoveListener(FixedButtonClicked);
_localButton.onClick.RemoveListener(LocalButtonClicked);
_remoteButton.onClick.RemoveListener(RemoteButtonClicked);
yield break;
}
// Processing before this screen is hidden
public override void DidExit()
{
_localImage.texture = null;
_remoteImage.texture = null;
}
}
}Tiếp theo, chúng ta sẽ tạo ra một Presenter để truyền path và URL vào view này, cũng như để nhận các sự kiện từ View.
Trong chuyển đồi màn hình, các sự kiện vòng đời của mỗi màn hình có thể được xem như một loại sự kiện đến từ View. Presenter sẽ bắt (hook) vào các sự kiện vòng đời của màn hình này để thực hiện các xử lý phù hợp tại mỗi thời điểm.
Trong USN, chúng ta thực hiện điều này bằng cách thêm một ISheetLifecycleEvent vào mỗi màn hình thông qua phương thức AddLifecycleEvent() như sau.
using System;
using System.Collections;
using UnityScreenNavigator.Runtime.Core.Modal;
using UnityScreenNavigator.Runtime.Core.Sheet;
using Random = UnityEngine.Random;
namespace ViewLogicSeparation.Scripts
{
public class MainSheetPresenter : ISheetLifecycleEvent, IDisposable
{
private readonly MainSheet _sheet;
public MainSheetPresenter(MainSheet sheet)
{
_sheet = sheet;
// Adding this Presenter's lifecycle events to the screen.
// If a value less than 0 is given as the second argument, it will be executed before the screen's lifecycle events.
// If a value of 1 or more is given as the second argument, it will be executed after the screen's lifecycle events.
_sheet.AddLifecycleEvent(this, -1);
_sheet.FixedButtonClicked += OnFixedButtonClicked;
_sheet.LocalButtonClicked += OnLocalButtonClicked;
_sheet.RemoteButtonClicked += OnRemoteButtonClicked;
}
public void Dispose()
{
_sheet.RemoveLifecycleEvent(this);
_sheet.FixedButtonClicked -= OnFixedButtonClicked;
_sheet.LocalButtonClicked -= OnLocalButtonClicked;
_remoteButton.onClick.RemoveListener(OnRemoteButtonClicked);
}
public IEnumerator Initialize()
{
yield break;
}
public IEnumerator WillEnter()
{
// Set the paths and URLs of the images to be used in the Sheet.
var localImagePath = $"tex_button_local_{Random.Range(1, 4):D2}";
var remoteImageUrl = $"http://foo/bar/tex_button_remote_{Random.Range(1, 4):D2}.png";
_sheet.Setup(localImagePath, remoteImageUrl);
yield break;
}
public void DidEnter()
{
}
public IEnumerator WillExit()
{
yield break;
}
public void DidExit()
{
}
public IEnumerator Cleanup()
{
yield break;
}
private void OnFixedButtonClicked()
{
// When the Fixed button is clicked, open Modal01.
ModalContainer.Find("Main").Push("Modal01", true);
}
private void OnLocalButtonClicked()
{
// When the Local button is clicked, open Modal02.
ModalContainer.Find("Main").Push("Modal02", true);
}
private void OnRemoteButtonClicked()
{
// When the Remote button is clicked, open Modal03.
ModalContainer.Find("Main").Push("Modal03", true);
}
}
}Sau đó, chỉ cần khởi tại Presenter này trong khi cung cấp cho nó MainSheet là bạn có thể xác nhận rằng nó hoạt động bình thường.

Lưu ý, vì mục đích của chúng ta là tách biệt khỏi View nên tác giả đã không suy nghĩ phức tạp thêm và viết tất cả các phần xử lý vào Presenter
Chạy thử với dữ liệu giả (dummy data)
Bây giờ, khi chúng ta đã có thể tách biệt View và Logic để chạy kiểm thử và kiểm tra màn hình một cách độc lập hãy kiểm tra nó bằng dummy data.
Lần này, chúng ta sẽ cung cấp một vài đường dẫn cố định của ảnh giả, mỗi khi nhấn nút chúng ta sẽ cho hiển thị một modal giả tương ứng.
Dưới đây là một Presenter dùng cho việc thử nghiệm.
using System;
using System.Collections;
using UnityScreenNavigator.Runtime.Core.Modal;
using UnityScreenNavigator.Runtime.Core.Sheet;
namespace ViewLogicSeparation.Scripts
{
public class FakeMainSheetPresenter : ISheetLifecycleEvent, IDisposable
{
private readonly MainSheet _sheet;
public FakeMainSheetPresenter(MainSheet sheet)
{
_sheet = sheet;
_sheet.AddLifecycleEvent(this, -1);
_sheet.FixedButtonClicked += OnFixedButtonClicked;
_sheet.LocalButtonClicked += OnLocalButtonClicked;
_sheet.RemoteButtonClicked += OnRemoteButtonClicked;
}
public void Dispose()
{
_sheet.RemoveLifecycleEvent(this);
_sheet.FixedButtonClicked -= OnFixedButtonClicked;
_sheet.LocalButtonClicked -= OnLocalButtonClicked;
_sheet.RemoteButtonClicked -= OnRemoteButtonClicked;
}
public IEnumerator Initialize()
{
yield break;
}
public IEnumerator WillEnter()
{
// Set the paths and URLs of the images to be used in the Sheet.
var localImagePath = "tex_button_dummy";
var remoteImageUrl = $"http://foo/bar/tex_button_dummy.png";
_sheet.Setup(localImagePath, remoteImageUrl);
yield break;
}
public void DidEnter()
{
}
public IEnumerator WillExit()
{
yield break;
}
public void DidExit()
{
}
public IEnumerator Cleanup()
{
yield break;
}
private void OnFixedButtonClicked()
{
OpenFakeModal();
}
private void OnLocalButtonClicked()
{
OpenFakeModal();
}
private void OnRemoteButtonClicked()
{
OpenFakeModal();
}
private void OpenFakeModal()
{
ModalContainer.Find("Main").Push("FakeModal", true);
}
}
}Khi khởi tạo Presenter này truyền vào MainSheet (đóng vai trò là View), kết quả thực thi như sau:

Như vậy, giờ đây chúng ta đã có thể sử dụng dữ liệu giả để kiểm tra hoạt động của màn hình kể cả khi hình ảnh local và remote chưa có sẵn hoặc khi các modal chưa được làm xong.
Lời kết
Vậy là Haruki Yano đã tổng hợp về phương pháp tách biệt View và Logic bằng cách sử dụng USN.
Lưu ý rằng ví dụ trong bài viết chỉ là một trường hợp và không có nghĩa đây là câu trả lời đúng và tối ưu. Ví dụ, trong bài viết này tác giả đã tách các popup thành một View riêng biệt, nhưng tùy vào yêu cầu mà việc tích hợp nó vào View như một phần của màn hình có thể sẽ tốt hơn. Chúng ta cũng có thể xây dựng một môi trường kiểm thử hoạt động từng cụm gồm nhiều màn hình thay vì từng màn hình riêng lẻ.
Ngoài ra, các ứng dụng thực tế còn phức tạp hơn nhiều và việc xây dựng một môi trường hoạt động sử dụng dummy data cũng tốn rất nhiều công sức. Cũng có thể có người cho rằng nên ưu tiên triển khai trước mắt hơn là xây dựng môi trường.
Tuy nhiên nếu công sức xây dựng môi trường < công sức tiết kiệm được thì dĩ nhiên chúng ta nên làm. Và Haruki Yano nghĩ rằng chính việc đưa ra phán đoán đó mới là một công việc quan trọng.
Đừng để bị các lý thuyết về kiến trúc chi phối mà rơi vào chủ nghĩa nguyên tắc, hãy nhìn về tương lai và tự mình đưa ra các phán đoán tổng thể.