# Movement

Loạt bài hướng dẫn về điều khiển di chuyển nhân vật.

# Trượt một Sphere

Đây là phần đầu tiên của loạt tutorial về cách điều khiển **movement** của một character. Cụ thể, chúng ta sẽ làm sphere trượt dựa trên player input.

Tutorial này được thực hiện bằng Unity 2019.2.9f1. Nội dung giả định rằng bạn đã có kiến thức cơ bản với Unity.

### Kiểm soát vị trí (Controlling Position)

Nhiều game xoay quanh một character cần di chuyển để hoàn thành mục tiêu nào đó. Nhiệm vụ của người chơi là dẫn dắt character. Game action cho phép bạn điều khiển trực tiếp bằng cách lái character, thường là nhấn phím hoặc xoay stick. Game point-and-click yêu cầu bạn chỉ định position đích rồi character tự động di chuyển tới đó. Game programming yêu cầu bạn viết các instruction để character thực thi. Và còn nhiều kiểu khác.

Trong loạt tutorial này, chúng ta sẽ tập trung vào cách điều khiển một character trong 3D action game. Trước tiên hãy bắt đầu đơn giản bằng việc làm một sphere trượt trên một hình chữ nhật phẳng nhỏ. Khi đã nắm vững, chúng ta có thể tăng độ phức tạp trong tương lai.

#### Thiết lập Scene

Bắt đầu với một 3D project mới theo cấu hình mặc định. Lúc này chúng ta chưa cần gì từ Package Manager, bạn có thể dùng bất kỳ render pipeline nào.

`<span class="editor-theme-code">SampleScene</span>` mặc định có một camera và một directional light; chúng ta sẽ giữ lại cả hai. Tạo một plane đại diện cho mặt đất và thêm một sphere, đều đặt tại gốc tọa độ. Sphere mặc định có radius 0.5, vì vậy hãy đặt tọa độ Y của nó thành 0.5 để trông như đang nằm trên ground plane.

![Scene hierarchy.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-gkc7tujd.png)

Scene hierarchy.

Chúng ta chỉ giới hạn movement 2D trên mặt đất, vì vậy hãy đặt camera phía trên plane và hướng xuống để có góc nhìn tốt về play area trong Game window. Đồng thời đặt `<span class="editor-theme-code">Projection</span>` mode thành `<span class="editor-theme-code">Orthographic</span>`. Cách này loại bỏ perspective, giúp quan sát movement 2D mà không bị biến dạng.

![Orthographic camera nhìn từ trên xuống.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-ymuse6u8.png)

Orthographic camera nhìn từ trên xuống.

Thứ duy nhất còn làm vướng tầm nhìn là shadow của sphere. Hãy bỏ nó bằng cách đặt `<span class="editor-theme-code">Shadow Type</span>` thành `<span class="editor-theme-code">None</span>`, hoặc `<span class="editor-theme-code">No Shadows</span>` tùy phiên bản Unity.

![Light không tạo shadow.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-bd7jyhsv.png)

Light không tạo shadow.

Tạo material cho ground và sphere rồi cấu hình theo ý bạn. Tôi đặt sphere màu đen và ground màu xám nhạt trầm. Chúng ta cũng sẽ trực quan hóa movement bằng một trail, vì vậy hãy tạo thêm material cho nó. Tôi sẽ dùng một unlit material màu đỏ nhạt. Cuối cùng, chúng ta cần một `<span class="editor-theme-code">MovingSphere</span>` script để triển khai movement.

![Các asset trong project.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-rvurtifd.png)

Các asset trong project.

Script có thể bắt đầu dưới dạng một extension rỗng của `<span class="editor-theme-code">MonoBehaviour</span>`.

```csharp
using UnityEngine;

public class MovingSphere : MonoBehaviour { }
```

![Game view.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-rfbejt2w.png)

Game view.

Thêm cả `<span class="editor-theme-code">TrailRenderer</span>` và component `<span class="editor-theme-code">MovingSphere</span>` của chúng ta vào sphere. Giữ nguyên mọi thứ còn lại.

![Sphere object cùng các component.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-dmvzkhbs.png)

Sphere object cùng các component.

Gán trail material cho phần tử đầu tiên và duy nhất trong `<span class="editor-theme-code">Materials</span>` array của component `<span class="editor-theme-code">TrailRenderer</span>`. Nó không cần cast shadow, dù điều này không quan trọng vì chúng ta đã tắt shadow. Ngoài ra, hãy giảm `<span class="editor-theme-code">Width</span>` từ 1.0 xuống một giá trị hợp lý hơn như 0.1 để tạo một đường mảnh.

![Trail renderer.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-jehakgco.png)

Trail renderer.

Dù chưa viết code cho movement, chúng ta vẫn có thể xem trước bằng cách vào play mode rồi di chuyển sphere trong Scene window.

![Movement trail.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-46jhxhgb.png)

Movement trail.

#### Đọc Player Input

Để di chuyển sphere, chúng ta phải đọc các input command của người chơi. Việc này được thực hiện trong `<span class="editor-theme-code">Update</span>` method của `<span class="editor-theme-code">MovingSphere</span>`. Player input là 2D nên có thể lưu trong một `<span class="editor-theme-code">Vector2</span>` variable. Ban đầu, ta đặt cả component X và Y của nó bằng 0, sau đó dùng chúng để đặt position của sphere trong plane XZ. Vì vậy component Y của input trở thành component Z của position. Position Y vẫn bằng 0.

```csharp
using UnityEngine;

public class MovingSphere : MonoBehaviour {

	void Update () {
		Vector2 playerInput;
		playerInput.x = 0f;
		playerInput.y = 0f;
		transform.localPosition = new Vector3(playerInput.x, 0.5f, playerInput.y);
	}
}
```

Cách đơn giản nhất để lấy directional input từ người chơi là gọi `<span class="editor-theme-code">Input.GetAxis</span>` với tên của một axis. Unity mặc định định nghĩa cả input axis `<span class="editor-theme-code">Horizontal</span>` và `<span class="editor-theme-code">Vertical</span>`; bạn có thể kiểm tra chúng trong phần `<span class="editor-theme-code">Input</span>` của Project Settings. Ta sẽ dùng giá trị horizontal cho X và vertical cho Y.

```csharp
playerInput.x = Input.GetAxis("Horizontal");
		playerInput.y = Input.GetAxis("Vertical");
```

Cấu hình mặc định liên kết các axis này với phím mũi tên và WASD. Các giá trị input cũng được tinh chỉnh để phím hoạt động gần giống controller stick. Bạn có thể điều chỉnh các thiết lập này theo ý muốn, nhưng tôi giữ nguyên mặc định.

<video controls="" height="400" src="" width="400"></video>

Dùng phím mũi tên hoặc WASD.

Cả hai axis còn có một định nghĩa thứ hai, liên kết chúng với input từ joystick hoặc controller stick bên trái. Cách này cho input mượt hơn nhiều, nhưng tôi sẽ dùng phím cho mọi animation ngoại trừ animation tiếp theo.

<video controls="" height="400" src="" width="400"></video>

Dùng controller stick.

#### Normalize Input Vector

Các axis trả về 0 khi ở trạng thái nghỉ và −1 hoặc 1 tại hai cực. Vì ta dùng input để đặt position của sphere nên position bị giới hạn trong một hình chữ nhật có cùng range. Ít nhất điều này đúng với key input vì các phím độc lập với nhau. Với stick, các dimension liên kết với nhau và thông thường ta bị giới hạn ở khoảng cách tối đa 1 tính từ origin theo mọi direction, do đó position nằm trong một hình tròn.

Ưu điểm của controller input là độ dài tối đa của input vector luôn bằng 1 bất kể direction. Vì vậy movement có thể nhanh như nhau theo mọi direction. Với phím thì không như vậy: giá trị tối đa là 1 khi nhấn một phím nhưng là √2 khi nhấn đồng thời hai phím, nghĩa là movement theo đường chéo nhanh nhất.

Giá trị tối đa khi dùng phím là √2 theo định lý Pythagoras. Các giá trị axis xác định độ dài hai cạnh góc vuông, còn vector tổng hợp là cạnh huyền. Vì vậy magnitude của input vector là `<span class="editor-theme-code">√(x²+y²)</span>`.

Ta có thể đảm bảo độ dài vector không bao giờ vượt quá 1 bằng cách chia input vector cho magnitude của nó. Kết quả luôn là unit vector, trừ khi độ dài ban đầu bằng 0, khi đó kết quả không xác định. Quá trình này được gọi là normalize một vector. Ta có thể thực hiện bằng cách gọi `<span class="editor-theme-code">Normalize</span>` trên vector; vector sẽ tự scale và trở thành zero vector nếu kết quả lẽ ra không xác định.

```csharp
playerInput.x = Input.GetAxis("Horizontal");
		playerInput.y = Input.GetAxis("Vertical");
		playerInput.Normalize();
```

[![embedded-image-x51fomxf.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-x51fomxf.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/controlling-position/keys-normalized.mp4)

Key input đã được normalize.

#### Giới hạn Input Vector

Luôn normalize input vector sẽ giới hạn position trên đường tròn, trừ khi input ở trạng thái neutral thì ta kết thúc tại origin. Các đường nối giữa origin và đường tròn biểu diễn một frame đơn lẻ khi hình tròn nhảy từ tâm ra đường tròn hoặc quay lại.

Kiểu input tất-cả-hoặc-không này đôi khi hữu ích, nhưng hãy cho phép cả mọi position bên trong đường tròn. Ta chỉ điều chỉnh input vector khi magnitude của nó vượt quá 1. Một cách thuận tiện là gọi static method `<span class="editor-theme-code">Vector2.ClampMagnitude</span>` thay cho `<span class="editor-theme-code">Normalize</span>`, truyền vào vector và giá trị tối đa 1. Kết quả là một vector được giữ nguyên hoặc được scale xuống mức tối đa đã cung cấp.

```csharp
//playerInput.Normalize();
		playerInput = Vector2.ClampMagnitude(playerInput, 1f);
```

[![embedded-image-3qraeues.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-3qraeues.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/controlling-position/constrained.mp4)

Key input đã được giới hạn.

### Kiểm soát vận tốc (Controlling Velocity)

Cho đến giờ, chúng ta dùng trực tiếp input để đặt position của sphere. Điều này có nghĩa là khi input vector `<span class="editor-theme-code">i</span>` thay đổi thì position `<span class="editor-theme-code">p</span>` của sphere lập tức đổi thành cùng giá trị. Do đó `<span class="editor-theme-code">p=i</span>`. Đây không phải motion đúng nghĩa mà là teleportation. Cách tự nhiên hơn để điều khiển sphere là xác định position tiếp theo `<span class="editor-theme-code">p₁</span>` bằng cách cộng displacement vector `<span class="editor-theme-code">d</span>` vào position cũ `<span class="editor-theme-code">p₀</span>`, do đó `<span class="editor-theme-code">p₁=p₀+d</span>`.

#### Relative Movement

Khi dùng `<span class="editor-theme-code">d=i</span>` thay cho `<span class="editor-theme-code">p=i</span>`, mối quan hệ giữa input và position trở nên ít trực tiếp hơn. Điều này loại bỏ các ràng buộc lên position vì sau update đầu tiên, position trở thành tương đối với chính nó thay vì với origin. Vì thế position được mô tả bởi infinite iterative sequence `<span class="editor-theme-code">pₙ₊₁=pₙ+d</span>` với `<span class="editor-theme-code">p₀</span>` được định nghĩa là starting position.

```csharp
Vector3 displacement = new Vector3(playerInput.x, 0f, playerInput.y);
		transform.localPosition += displacement;
```

![Relative movement.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-kzhy4uoq.png)

Relative movement.

#### Velocity

Sphere của chúng ta thực sự có thể đi đến bất kỳ đâu, nhưng nó di chuyển nhanh đến mức khó điều khiển. Nguyên nhân là input vector được cộng vào ở mỗi update. Frame rate càng cao, sphere càng đi nhanh. Để kết quả nhất quán, ta không muốn frame rate ảnh hưởng đến input. Nếu dùng input cố định, ta muốn displacement cố định bất kể frame rate có thể dao động.

Trong trường hợp này, một frame biểu diễn một duration: lượng time `<span class="editor-theme-code">t</span>` đã trôi qua giữa thời điểm bắt đầu frame trước và frame hiện tại; ta có thể truy cập giá trị này qua `<span class="editor-theme-code">Time.deltaTime</span>`. Do đó displacement thực tế là `<span class="editor-theme-code">d=it</span>` và ta đã giả định sai rằng `<span class="editor-theme-code">t</span>` là hằng số.

Displacement được đo bằng Unity unit, giả định tương ứng với một meter. Nhưng ta nhân input với một duration tính bằng second. Để thu được meter, input phải được đo bằng meter per second. Vì vậy input vector biểu diễn velocity: `<span class="editor-theme-code">v=i</span>` và `<span class="editor-theme-code">d=vt</span>`.

```csharp
Vector3 velocity = new Vector3(playerInput.x, 0f, playerInput.y);
		Vector3 displacement = velocity * Time.deltaTime;
		transform.localPosition += displacement;
```

![Kiểm soát velocity độc lập với frame rate.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-mdhvwypp.png)

Kiểm soát velocity độc lập với frame rate.

#### Speed

Input vector tối đa có magnitude bằng 1, tương ứng với velocity một meter per second, tức 3.6 kilometer per hour hay khoảng 2.24 mile per hour. Mức này không nhanh lắm.

Ta có thể tăng maximum velocity bằng cách scale input vector. Scale factor biểu diễn maximum speed, tức velocity không có direction. Thêm một `<span class="editor-theme-code">maxSpeed</span>` field có attribute `<span class="editor-theme-code">SerializeField</span>` với giá trị mặc định 10 và gắn cho nó attribute `<span class="editor-theme-code">Range</span>` với range chẳng hạn 1–100.

```csharp
[SerializeField, Range(0f, 100f)]
	float maxSpeed = 10f;
```

Nhân input vector với max speed để tìm desired velocity.

```csharp
Vector3 velocity =
			new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
```

![inspector](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-vo88fwej.png)  
[![embedded-image-ehcryry1.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-ehcryry1.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/controlling-velocity/controlling-velocity.mp4)

Max speed được đặt thành 10.

#### Acceleration

Vì chúng ta kiểm soát trực tiếp velocity nên nó có thể thay đổi tức thời. Chỉ có filtering do input system áp dụng làm thay đổi chậm lại đôi chút. Trong thực tế, velocity không thể thay đổi ngay lập tức. Một thay đổi cần effort và time, tương tự như thay đổi position. Rate of change của velocity được gọi là acceleration `<span class="editor-theme-code">a</span>`, dẫn đến `<span class="editor-theme-code">vₙ₊₁=vₙ+at</span>` với `<span class="editor-theme-code">v₀</span>` là zero vector. Deceleration đơn giản là acceleration ngược hướng với velocity hiện tại nên không cần xử lý đặc biệt.

Hãy xem điều gì xảy ra nếu dùng input vector để kiểm soát acceleration thay vì trực tiếp kiểm soát velocity. Việc này yêu cầu theo dõi velocity hiện tại, vì vậy hãy lưu nó trong một field.

```csharp
Vector3 velocity;
```

Input vector giờ xác định acceleration trong `<span class="editor-theme-code">Update</span>`, nhưng tạm thời vẫn hãy nhân nó với `<span class="editor-theme-code">maxSpeed</span>` và diễn giải giá trị này thành max acceleration. Sau đó cộng nó vào velocity trước khi tính displacement.

```csharp
Vector3 acceleration =
			new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
		velocity += acceleration * Time.deltaTime;
		Vector3 displacement = velocity * Time.deltaTime;
```

![Velocity thay đổi mượt.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-uplng8yq.png)

Velocity thay đổi mượt.

#### Desired Velocity

Kiểm soát acceleration thay vì velocity tạo ra motion mượt hơn nhiều, nhưng cũng làm giảm khả năng kiểm soát sphere. Nó giống như ta đang lái xe thay vì đi bộ. Hầu hết game cần kiểm soát velocity trực tiếp hơn, vì vậy hãy quay lại cách tiếp cận đó. Tuy nhiên, việc áp dụng acceleration thực sự tạo ra motion mượt hơn.

![Acceleration thay đổi velocity, rồi velocity thay đổi position.](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-npdx0xeq.png)

Acceleration thay đổi velocity, rồi velocity thay đổi position.

Ta có thể kết hợp hai cách tiếp cận bằng việc trực tiếp kiểm soát target velocity và áp dụng acceleration lên actual velocity cho đến khi nó khớp desired velocity. Sau đó có thể tinh chỉnh responsiveness của sphere bằng cách điều chỉnh maximum acceleration. Hãy thêm một serializable field cho giá trị đó.

```csharp
[SerializeField, Range(0f, 100f)]
	float maxAcceleration = 10f;
```

Trong `<span class="editor-theme-code">Update</span>`, giờ ta dùng input vector để xác định desired velocity và không còn điều chỉnh velocity theo cách cũ.

```csharp
Vector3 desiredVelocity =
			new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
		//velocity += acceleration * Time.deltaTime;
```

Thay vào đó, trước tiên ta tìm maximum speed change bằng cách nhân max acceleration với `<span class="editor-theme-code">t</span>`. Đây là mức velocity tối đa có thể thay đổi trong update này.

```csharp
Vector3 desiredVelocity =
			new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
		float maxSpeedChange = maxAcceleration * Time.deltaTime;
```

Trước tiên chỉ xét component X của velocity. Nếu nó nhỏ hơn desired value thì cộng maximum change.

```csharp
float maxSpeedChange = maxAcceleration * Time.deltaTime;
		if (velocity.x < desiredVelocity.x) {
			velocity.x += maxSpeedChange;
		}
```

Điều này có thể gây overshoot; ta ngăn lại bằng cách lấy minimum giữa giá trị đã tăng và desired value. Có một `<span class="editor-theme-code">Mathf.Min</span>` method có thể dùng ở đây.

```csharp
if (velocity.x < desiredVelocity.x) {
			//velocity.x += maxSpeedChange;
			velocity.x =
				Mathf.Min(velocity.x + maxSpeedChange, desiredVelocity.x);
		}
```

Ngược lại, velocity có thể lớn hơn desired value. Khi đó ta trừ max change rồi lấy maximum giữa kết quả và desired value thông qua `<span class="editor-theme-code">Mathf.Max</span>`.

```csharp
if (velocity.x < desiredVelocity.x) {
			velocity.x =
				Mathf.Min(velocity.x + maxSpeedChange, desiredVelocity.x);
		}
		else if (velocity.x > desiredVelocity.x) {
			velocity.x =
				Mathf.Max(velocity.x - maxSpeedChange, desiredVelocity.x);
		}
```

Ta cũng có thể thực hiện toàn bộ việc này bằng `<span class="editor-theme-code">Mathf.MoveTowards</span>` method tiện dụng, truyền vào current value, desired value và maximum allowed change. Thực hiện riêng cho cả component X và Z.

```csharp
float maxSpeedChange = maxAcceleration * Time.deltaTime;
		//if (velocity.x < desiredVelocity.x) {
		//	velocity.x =
		//		Mathf.Min(velocity.x + maxSpeedChange, desiredVelocity.x);
		//}
		//else if (velocity.x > desiredVelocity.x) {
		//	velocity.x =
		//		Mathf.Max(velocity.x - maxSpeedChange, desiredVelocity.x);
		//}
		velocity.x =
			Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);
		velocity.z =
			Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);
```

![inspector](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-mlt8aq33.png)  
[![embedded-image-lwhw9dpv.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-lwhw9dpv.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/controlling-velocity/controlling-target-velocity.mp4)

Cả max speed và acceleration đều được đặt thành 10.

Giờ ta có thể tinh chỉnh maximum acceleration để đạt được điểm cân bằng mong muốn giữa motion mượt và responsiveness.

### Giới hạn vị trí (Constraining Position)

Bên cạnh việc kiểm soát velocity của character, một phần quan trọng trong game là giới hạn nơi nó có thể đến. Scene đơn giản của chúng ta có một plane đại diện cho mặt đất. Hãy buộc sphere luôn nằm trên plane.

#### Giữ bên trong hình vuông

Thay vì dùng chính plane, ta chỉ cần tạo allowed area thành một serializable field của sphere. Có thể dùng một `<span class="editor-theme-code">Rect</span>` struct value cho mục đích này. Gán default value khớp với plane mặc định bằng cách gọi constructor method với −5 cho hai argument đầu và 10 cho hai argument cuối. Chúng xác định bottom-left corner và size.

```csharp
[SerializeField]
	Rect allowedArea = new Rect(-5f, -5f, 10f, 10f);
```

Ta giới hạn sphere bằng cách giới hạn new position trước khi gán nó cho `<span class="editor-theme-code">transform.localPosition</span>`. Vì vậy trước tiên hãy lưu nó trong một variable.

```csharp
//transform.localPosition += displacement;
		Vector3 newPosition = transform.localPosition + displacement;
		transform.localPosition = newPosition;
```

Ta có thể gọi `<span class="editor-theme-code">Contains</span>` trên allowed area để kiểm tra một point có nằm bên trong hoặc trên edge hay không. Nếu new position không thỏa điều kiện, hãy đặt nó về current position, qua đó triệt tiêu movement trong update này.

```csharp
Vector3 newPosition = transform.localPosition + displacement;
		if (!allowedArea.Contains(newPosition)) {
			newPosition = transform.localPosition;
		}
		transform.localPosition = newPosition;
```

Khi truyền một `<span class="editor-theme-code">Vector3</span>` vào `<span class="editor-theme-code">Contains</span>`, method sẽ kiểm tra tọa độ XY, không đúng trong trường hợp này. Vì vậy hãy truyền một `<span class="editor-theme-code">Vector2</span>` mới chứa tọa độ XZ.

```csharp
if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) {
			newPosition = transform.localPosition;
		}
```

![inspector](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-5qlgdn2l.png)  
![game](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-k4wcdzvq.png)

Dừng lại tại các edge của plane.

Sphere giờ không thể thoát ra ngoài và sẽ dừng khi cố làm vậy. Kết quả bị giật vì movement bị bỏ qua trong một số frame, nhưng ta sẽ xử lý ngay sau đây. Trước hết, hãy lưu ý sphere có thể đi đến khi nằm ngay trên edge của plane. Nguyên nhân là ta chỉ giới hạn position mà không tính đến radius. Sẽ đẹp hơn nếu toàn bộ sphere luôn nằm trong allowed area. Ta có thể sửa code để tính đến radius, nhưng cách khác là chỉ cần thu nhỏ allowed area. Như vậy là đủ cho scene đơn giản này.

Dịch corner của area lên 0.5 và giảm size đi 1 ở cả hai dimension.

![inspector](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-39abklmi.png)  
![game](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-dbiqfwxc.png)

Dừng lại khi chạm edge của plane.

#### Đặt Position chính xác

Ta có thể loại bỏ movement bị giật bằng cách clamp new position vào allowed area thay vì bỏ qua nó. Có thể thực hiện bằng cách gọi `<span class="editor-theme-code">Mathf.Clamp</span>` với một value cùng minimum và maximum được phép. Dùng property `<span class="editor-theme-code">xMin</span>` và `<span class="editor-theme-code">xMax</span>` của area cho X, còn `<span class="editor-theme-code">yMin</span>` và `<span class="editor-theme-code">yMax</span>` cho Z.

```csharp
if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) {
			//newPosition = transform.localPosition;
			newPosition.x =
				Mathf.Clamp(newPosition.x, allowedArea.xMin, allowedArea.xMax);
			newPosition.z =
				Mathf.Clamp(newPosition.z, allowedArea.yMin, allowedArea.yMax);
		}
```

[![embedded-image-kqw3e5xq.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-kqw3e5xq.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/constraining-position/sticking-to-edges.mp4)

Bám vào các edge.

#### Loại bỏ Velocity

Sphere giờ có vẻ bám vào các edge. Sau khi chạm edge, ta trượt dọc theo nó nhưng phải mất một lúc mới có thể rời khỏi edge. Nguyên nhân là velocity của sphere vẫn hướng vào edge. Ta phải đổi direction bằng acceleration hướng ra khỏi edge; việc này mất nhiều hay ít time tùy vào max acceleration.

Nếu sphere là một ball và edge của area là một wall thì khi chạm wall, nó phải dừng lại. Điều này thực sự xảy ra. Nhưng nếu wall đột ngột biến mất, ball sẽ không lấy lại velocity trước đó. Momentum đã mất, energy được truyền đi trong collision và có thể gây damage. Vì vậy phải loại bỏ velocity khi chạm edge. Tuy nhiên sphere vẫn có thể trượt dọc edge, nên chỉ component velocity hướng về edge đó cần bị loại bỏ.

Để đặt component velocity phù hợp về 0, ta phải kiểm tra xem có vượt bounds theo cả hai hướng của mỗi dimension hay không. Lúc này ta cũng có thể tự clamp position vì đang thực hiện đúng các kiểm tra giống như `<span class="editor-theme-code">Mathf.Clamp</span>` và `<span class="editor-theme-code">Contains</span>`.

```csharp
//if (!allowedArea.Contains(new Vector2(newPosition.x, newPosition.z))) {
			//newPosition.x =
			//	Mathf.Clamp(newPosition.x, allowedArea.xMin, allowedArea.xMax);
			//newPosition.z =
			//	Mathf.Clamp(newPosition.z, allowedArea.yMin, allowedArea.yMax);
		//}
		if (newPosition.x < allowedArea.xMin) {
			newPosition.x = allowedArea.xMin;
			velocity.x = 0f;
		}
		else if (newPosition.x > allowedArea.xMax) {
			newPosition.x = allowedArea.xMax;
			velocity.x = 0f;
		}
		if (newPosition.z < allowedArea.yMin) {
			newPosition.z = allowedArea.yMin;
			velocity.z = 0f;
		}
		else if (newPosition.z > allowedArea.yMax) {
			newPosition.z = allowedArea.yMax;
			velocity.z = 0f;
		}
```

[![embedded-image-fjk8zqwp.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-fjk8zqwp.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/constraining-position/velocity-elimination-at-edges.mp4)

Không còn bám vào edge.

#### Nảy

Velocity không phải lúc nào cũng bị loại bỏ trong collision. Nếu sphere là một ball nảy hoàn hảo thì nó sẽ đảo direction trong dimension tương ứng. Hãy thử làm như vậy.

```csharp
if (newPosition.x < allowedArea.xMin) {
			newPosition.x = allowedArea.xMin;
			velocity.x = -velocity.x;
		}
		else if (newPosition.x > allowedArea.xMax) {
			newPosition.x = allowedArea.xMax;
			velocity.x = -velocity.x;
		}
		if (newPosition.z < allowedArea.yMin) {
			newPosition.z = allowedArea.yMin;
			velocity.z = -velocity.z;
		}
		else if (newPosition.z > allowedArea.yMax) {
			newPosition.z = allowedArea.yMax;
			velocity.z = -velocity.z;
		}
```

[![embedded-image-ae9tbxls.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-ae9tbxls.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/constraining-position/bouncing-off-edges.mp4)

Nảy khỏi các edge.

Sphere giờ giữ lại momentum và chỉ đổi direction khi chạm wall. Nó chậm lại đôi chút vì sau khi nảy, velocity không còn khớp với desired velocity. Để có cú nảy tốt nhất, người chơi phải điều chỉnh input ngay lập tức.

#### Bounciness

Không cần giữ lại toàn bộ velocity khi đảo chiều. Một số vật nảy nhiều hơn vật khác. Vì vậy hãy cho phép cấu hình bằng cách thêm một `<span class="editor-theme-code">bounciness</span>` field, mặc định là 0.5 với range 0–1. Nhờ đó sphere có thể nảy hoàn hảo, không nảy chút nào hoặc ở bất kỳ mức nào giữa hai trạng thái.

```csharp
[SerializeField, Range(0f, 1f)]
	float bounciness = 0.5f;
```

Đưa bounciness vào new velocity value khi chạm edge.

```csharp
if (newPosition.x < allowedArea.xMin) {
			newPosition.x = allowedArea.xMin;
			velocity.x = -velocity.x * bounciness;
		}
		else if (newPosition.x > allowedArea.xMax) {
			newPosition.x = allowedArea.xMax;
			velocity.x = -velocity.x * bounciness;
		}
		if (newPosition.z < allowedArea.yMin) {
			newPosition.z = allowedArea.yMin;
			velocity.z = -velocity.z * bounciness;
		}
		else if (newPosition.z > allowedArea.yMax) {
			newPosition.z = allowedArea.yMax;
			velocity.z = -velocity.z * bounciness;
		}
```

![inspector](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-9xv9yp3f.png)  
[![embedded-image-qc30nz1k.png](https://knowledge.thanhdv.com/uploads/images/gallery/2026-08/embedded-image-qc30nz1k.png)](https://catlikecoding.com/unity/tutorials/movement/sliding-a-sphere/constraining-position/bouncing-half-velocity.mp4)

Bounciness được đặt thành 0.5.

Cách này không mô phỏng physics thực tế, vốn phức tạp hơn nhiều. Nhưng kết quả đã bắt đầu trông giống thật và như vậy là đủ cho hầu hết game. Ngoài ra, movement của chúng ta chưa thật chính xác. Các phép tính chỉ đúng nếu sphere chạm edge đúng vào cuối movement trong một frame. Khả năng cao điều đó không xảy ra, nghĩa là sphere lẽ ra phải lập tức di chuyển ra xa edge một chút. Trước tiên cần tính remaining time rồi dùng nó cùng new velocity trong dimension tương ứng. Tuy nhiên, điều này có thể dẫn đến lần nảy thứ hai, khiến mọi thứ phức tạp hơn. May mắn là ta không cần độ chính xác như vậy để tạo ảo giác thuyết phục về một sphere đang nảy xung quanh.

Tutorial tiếp theo là [Physics](https://catlikecoding.com/unity/tutorials/movement/physics/).