Scripts: - Start: public void PlayGame { SceneManager.LoadSceneSceneManager.GetActiveScene.buildIndex + 1; } Khi nhấn vào nút Play, trò chơi sẽ tự động load từ scene Start sang scene Gam
Trang 1HỌC VIỆN CÔNG NGHỆ BƯU CHÍNH VIỄN THÔNG
- -BÁO CÁO Trò chơi: 2048
Giảng viên hướng dẫn : Đỗ Thị Liên
Sinh viên thực hiện : Nguyễn Đức Quang - B18DCPT179
Nguyễn Tùng Lâm – B18DCPT124 Nguyễn Chí Hiếu – B18DCPT084
Lớp
Nhóm
: D18PTDPT2 : Nhóm 16 – Kíp 5 – Thứ 3
Hà Nội, tháng 12 năm 2021
Giới thiệu game:
2048 là một trò chơi nổi tiếng vì sự đơn giản nhưng có thể khiên người chơi tham gia đến hàng giờ Mục tiêu là 'hợp nhất' các ô có giá trị giống hệt nhau với nhau, để giá trị của chúng được
Trang 2nhân đôi Khi người chơi vuốt theo hướng mong muốn, các vật phẩm sẽ được di chuyển đến đó
và một vật phẩm mới sẽ được tạo ra Nếu người chơi đạt đến số 2048 thì họ sẽ thắng trò chơi.
Game Objects:
- Main Camera: Là camera chính để hiển thị trò chơi
- Script Holder: Chứa các scripts của trò chơi.
- Canvas: Chứa các UI của trò chơi.
- ScoreText: Dòng ghi số điểm đạt được của người chơi.
- RestartButton: Nút bắt đầu lại trò chơi.
- EventSystem: Xử lý các sự kiện trong trò chơi.
- Background: Chứa background của trò chơi.
- Prefabs: Có các Game Object 2, 4, 8,…, 1024, blank.
Scripts:
- Start:
public void PlayGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
Khi nhấn vào nút Play, trò chơi sẽ tự động load từ scene Start sang scene GameScene
- Input Methods
Nhóm đã triển khai hai phương pháp để lấy thông tin đầu vào của người dùng trong trò chơi: Qua các phím mũi tên của bàn phím, và vuốt trên màn hình cảm ứng (hoặc chuột) Nhóm đã triển khai một phép liệt kê để lấy thông tin đầu vào của người dùng và một giao diện được triển khai theo từng phương thức nhập liệu được sử dụng.
public enum InputDirection
{
Left, Right, Top, Bottom
}
public interface IInputDetector
{
InputDirection? DetectInputDirection();
}
• Input Methods qua bàn phím:
public class ArrowKeysDetector : MonoBehaviour, IInputDetector
{
Trang 3public InputDirection? DetectInputDirection()
{
if (Input.GetKeyUp(KeyCode.UpArrow))
return InputDirection.Top;
else if (Input.GetKeyUp(KeyCode.DownArrow))
return InputDirection.Bottom;
else if (Input.GetKeyUp(KeyCode.RightArrow))
return InputDirection.Right;
else if (Input.GetKeyUp(KeyCode.LeftArrow))
return InputDirection.Left;
else
return null;
}
}
• Input Methods qua cảm ứng:
public enum State
{
SwipeNotStarted,
SwipeStarted
}
Tạo một enum đơn giản để giữ trạng thái của việc vuốt:
public class SwipeDetector : MonoBehaviour, IInputDetector
{
private State state = State.SwipeNotStarted;
private Vector2 startPoint;
private DateTime timeSwipeStarted;
private TimeSpan maxSwipeDuration = TimeSpan.FromSeconds(1);
private TimeSpan minSwipeDuration = TimeSpan.FromMilliseconds(100);
Sử dụng một số trường để giúp ta kế thừa Thời lượng vuốt tối đa là 1 giây và tối thiểu là
100 mili giây.
public InputDirection? DetectInputDirection()
{
if (state == State.SwipeNotStarted)
{
if (Input.GetMouseButtonDown(0))
{
timeSwipeStarted = DateTime.Now;
state = State.SwipeStarted;
startPoint = Input.mousePosition;
}
}
Nếu người dùng chạm (hoặc nhấp bằng chuột) vào màn hình, chúng ta sẽ bắt đầu quá trình vuốt.
else if (state == State.SwipeStarted)
{
Trang 4if (Input.GetMouseButtonUp(0))
{
TimeSpan timeDifference = DateTime.Now - timeSwipeStarted;
if (timeDifference <= maxSwipeDuration && timeDifference >=
minSwipeDuration)
{
Vector2 mousePosition = Input.mousePosition;
Vector2 differenceVector = mousePosition - startPoint;
float angle = Vector2.Angle(differenceVector, Vector2.right); Vector3 cross = Vector3.Cross(differenceVector, Vector2.right);
Khi người dùng kết thúc quá trình vuốt, ta kiểm tra xem thời lượng vuốt có nằm trong giới hạn thời gian mong muốn hay không Nếu đúng như vậy, chúng ta phải tìm góc vuốt qua phương thức Vector2.Angle Ta cũng sử dụng phương pháp Vector3.Cross (tính tích
số chéo) để xác định hướng của chênh lệch.
if (cross.z > 0)
angle = 360 - angle;
state = State.SwipeNotStarted;
if ((angle >= 315 && angle < 360) || (angle >= 0 && angle <= 45)) return
InputDirection.Right; else if (angle > 45 && angle <= 135) return
InputDirection.Top; else if (angle > 135 && angle <= 225)
return InputDirection.Left;
else
return InputDirection.Bottom;
}
}
}
return null;
Nếu z trong vectơ cross là dương, điều đó có nghĩa là chúng ta cần phải tính toán lại góc,
vì góc đúng là góc đối diện (360-) với góc mà chúng ta có Cuối cùng, ta xác định góc đó thuộc về một trong bốn hướng và ta trả về giá trị liệt kê chính xác Không cần phải nói, nếu người dùng chưa thực hiện bất kỳ thao tác vuốt nào hoặc không nằm trong giới hạn thời gian được chấp nhận, trò chơi sẽ trả về giá trị rỗng.
- Globals:
public static classGlobals
{
public readonly staticint Rows = 4;
public readonly staticint Columns = 4;
public static readonlyfloat AnimationDuration = 0.05f;
}
Lớp Globals chứa các biến tĩnh về Rows, Columns và AnimationDuration
- ItemMovementDetails:
Trang 5Lớp ItemMovementDetails được sử dụng để mang các chi tiết liên quan đến một đối tượng sắp được di chuyển và / hoặc sao chép Thuộc tính NewRow / NewColumn chứa thông tin về vị trí của ô trong mảng, trong khi thuộc tính GOToAnimateScale và
GOToAnimatePosition chứa thông tin về các đối tượng trò chơi sắp được di chuyển và / hoặc thu nhỏ Quy trình bình thường là di chuyển một ô (thay đổi vị trí của nó), nhưng nếu ô này sẽ hợp nhất với một ô khác, thì điều này cũng sẽ thay đổi tỷ lệ của nó (và sau
đó biến mất)
public class ItemMovementDetails
{
public GameObject GOToAnimateScale { get; set; }
public GameObject GOToAnimatePosition { get; set; }
public int NewRow { get; set; }
public int NewColumn { get; set; }
public ItemMovementDetails(int newRow, intnewColumn, GameObject
goToAnimatePosition, GameObject goToAnimateScale)
{
NewRow = newRow;
NewColumn = newColumn;
GOToAnimatePosition = goToAnimatePosition;
GOToAnimateScale = goToAnimateScale;
}
}
- Item:
Thuộc tính Value chứa giá trị của ô (ví dụ: 2,4,8,16, v.v.)
Thuộc tính Row và Column chứa các giá trị hàng và cột tương ứng của mảng mà ô này thuộc về
Thuộc tính GO chứa một tham chiếu đến Unity GameObject mà Ô này đề cập đến
Giá trị WasJustDuplicated chứa thông tin liệu ô này có được sao chép trong chuyển động / vuốt này hay không.
public class Item
{
public int Value { get; set; }
public int Row { get; set; }
public int Column { get; set; }
public GameObject GO { get; set; }
public bool WasJustDuplicated { get; set; }
}
- ItemArray:
Trang 6public class ItemArray
{
//the array, exposed only internally in the class
private Item[,] matrix = new Item[Globals.Rows, Globals.Columns];
public Item this[int row, intcolumn]
{
get
{
return matrix[row, column];
}
set
{
matrix[row, column] = value;
}
}
Lớp ItemArray chứa một thành viên riêng, một mảng ô hai chiều được gọi là ma trận Nó cũng cho thấy một index để cung cấp quyền truy cập vào mảng này Nếu một ô chiếm một vị trí trong mảng, thì ô ma trận [hàng, cột] chứa một tham chiếu đến nó Nếu không, ma trận [hàng, cột] là null.
//searches for a random null column and returns it via output variables - public void GetRandomRowColumn(outint row, out intcolumn)
{
do
{
row = random.Next(0, Globals.Rows);
column = random.Next(0, Globals.Columns);
} while (matrix[row, column] != null);
}
Phương thức này tìm nạp một ô không rỗng trong mảng Nó được sử dụng để tạo một ô mới sau mỗi lần vuốt.
private void ResetWasJustDuplicatedValues()
{
for (int row = 0; row < Globals.Rows; row++)
for (int column = 0; column < Globals.Columns; column++)
{
if (matrix[row, column] != null && matrix[row,
column].WasJustDuplicated)
matrix[row, column].WasJustDuplicated = false;
}
}
Phương thức này được gọi sau mỗi lần vuốt và đặt tất cả các giá trị WasJustDuplicated thành false.
private ItemMovementDetails AreTheseTwoItemsSame(
int originalRow, int originalColumn, inttoCheckRow, int toCheckColumn)
{
Trang 7if (toCheckRow < 0 || toCheckColumn < 0 || toCheckRow >= Globals.Rows || toCheckColumn >= Globals.Columns)
return null;
if (matrix[originalRow, originalColumn] != null && matrix[toCheckRow, toCheckColumn] != null
&& matrix[originalRow, originalColumn].Value == matrix[toCheckRow, toCheckColumn].Value
&& !matrix[toCheckRow, toCheckColumn].WasJustDuplicated)
{
//double the value, since the item is duplicated
matrix[toCheckRow, toCheckColumn].Value *= 2;
matrix[toCheckRow, toCheckColumn].WasJustDuplicated = true;
//make a copy of the gameobject to be moved and then disappear var GOToAnimateScaleCopy = matrix[originalRow, originalColumn].GO; //remove this item from the array
matrix[originalRow, originalColumn] = null;
return new ItemMovementDetails(toCheckRow, toCheckColumn,
matrix[toCheckRow, toCheckColumn].GO, GOToAnimateScaleCopy);
}
else
{
return null;
}
}
Phương thức này kiểm tra xem hai ô được truyền dưới dạng đối số (thông qua lập chỉ ô cột / hàng của chúng) có cùng giá trị hay không Đầu tiên, nó kiểm tra xem các chỉ ô được thông qua có nằm ngoài giới hạn hay không Sau đó, nó sẽ kiểm tra xem ô ở vị trí mảng này có phải là giá trị rỗng hay không và nếu nó không chỉ bị trùng lặp (tức là nó không bị trùng lặp sau lần vuốt hiện tại) Nếu tất cả các kiểm tra này là đúng, thì
- sao chép giá trị ô đầu tiên và đặt trường WasJustDuplicated thành true
- xóa ô thứ hai khỏi mảng sau khi ta giữ một tham chiếu đến nó, để tạo hiệu ứng cho nó
- trả về một thể hiện mới của lớp ItemMovementDetails, mang thông tin của ô để có vị trí của nó và ô để có hoạt ảnh theo tỷ lệ (và cuối cùng, biến mất).
private ItemMovementDetails MoveItemToNullPositionAndCheckIfSameWithNextOne (int oldRow, int newRow, intitemToCheckRow, int oldColumn, int newColumn, int itemToCheckColumn)
{
//we found a null item, so we attempt the switch ;)
//bring the first not null item to the position of the first null one matrix[newRow, newColumn] = matrix[oldRow, oldColumn];
matrix[oldRow, oldColumn] = null;
//check if we have the same value as the next one
Trang 8ItemMovementDetails imd2 = AreTheseTwoItemsSame(newRow, newColumn,
itemToCheckRow,
itemToCheckColumn);
if (imd2 != null)//we have, so add the item returned by the method
{
return imd2;
}
else//they are not the same, so we'll just animate the current item to its new position
{
return
new ItemMovementDetails(newRow, newColumn, matrix[newRow,
newColumn].GO, null);
}
}
Phương pháp này di chuyển mặt hàng đến nơi mà nó phải đến (dựa trên việc kiểm tra giá trị) Nó chỉ định ô vào vị trí mới của nó và "vô hiệu hóa" ô cũ Hơn nữa, nó kiểm tra xem vật phẩm bên cạnh có cùng giá trị hay không Trò chơi có hai phương pháp để di chuyển các ô Một cái được gọi khi vuốt theo chiều ngang và một cái cho những cái vuốt dọc
public List<ItemMovementDetails> MoveHorizontal(HorizontalMovement
horizontalMovement)
{
ResetWasJustDuplicatedValues();
var movementDetails = new List<ItemMovementDetails>();
//the relative column we will compare with
//if swipe is left, we will compare with the previous one (the -1
position)
int relativeColumn = horizontalMovement == HorizontalMovement.Left ? -1 : 1;
//to get the column indexes, to do the loop below
var columnNumbers = Enumerable.Range(0, Globals.Columns);
//for left swipe, we will traverse the columns in the order 0,1,2,3
//for right swipe, we want the reverse order
if (horizontalMovement == HorizontalMovement.Right)
{
columnNumbers = columnNumbers.Reverse();
}
Phương thức bắt đầu bằng cách đặt lại tất cả các giá trị WasJustDuplicated Sau đó, tùy thuộc vào chuyển động sang trái hay phải, chúng ta nhận được –1 hoặc 1 Điều này sẽ giúp xác định ô cần so sánh Nếu vuốt sang trái, chúng ta di chuyển tất cả các ô sang trái,
vì vậy chúng ta cần so sánh từng ô với ô trước đó (–1 một), để kiểm tra độ giống nhau
Ta sử dụng phương thức Enumerable.Range để lấy các index cột Phương thức này sẽ trả
về một danh sách chứa [0,1,2,3,…, Globals.Columns-1] Nếu vuốt sang phải, thì chúng ta
Trang 9đảo ngược thứ tự của danh sách columnNumbers Điều này là do chúng ta cần lặp lại các cột theo hướng chính xác Nếu vuốt sang trái, trò chơi sẽ bắt đầu bằng cách kiểm tra cột đầu tiên để tìm giá trị rỗng, sau đó là cột thứ hai, v.v Đây là lý do tại sao trò chơi muốn
di chuyển ô không phải null đầu tiên sang vị trí rỗng đầu tiên, bắt đầu từ bên trái Nếu vuốt sang phải, ta cần thực hiện thao tác này theo hướng ngược lại Nên ta phải đảo ngược danh sách columnNumbers.
for (int row = Globals.Rows - 1; row >= 0; row )
{ //we're doing foreach instead of for in order to traverse the columns
//in the appropriate order
foreach (int column incolumnNumbers)
{
//if the item is null, continue checking for non-null items
if (matrix[row, column] == null) continue;
//since we arrived here, we have a non-null item
//first we check if this item has the same value as the previous one //previous one's position depends on whether the relativeColumn
variable is -1 or 1, depending on the swipe
ItemMovementDetails imd = AreTheseTwoItemsSame(row, column, row, column + relativeColumn);
if (imd != null)
{
//items have the same value, so they will be "merged"
movementDetails.Add(imd);
//continue the loop
//the new duplicated item may be moved on a subsequent loop
continue;
}
Ở đây sẽ bắt đầu vòng lặp của trò chơi Trò chơi sẽ kiểm tra tất cả các hàng Sau đó, ta lặp qua tất cả các cột, lấy các chỉ ô từ danh sách columnNumbers Trong khi duyệt qua từng hàng, trước tiên, trò chơi kiểm tra từng ô xem có rỗng không Nếu nó là rỗng, ta tiếp tục kiểm tra ô tiếp theo (bằng cách kiểm tra cột tiếp theo - tiếp theo có nghĩa là –1 hoặc 1, tùy thuộc vào lần vuốt Khi đến cột không rỗng, ta kiểm tra xem cột này có giống với cột một bên cạnh nó Một lần nữa, “bên cạnh” có nghĩa là –1 hoặc 1, tùy thuộc vào việc vuốt sang trái hay phải Nếu các ô này giống nhau, thì ta thêm thông tin này vào danh sách MovementDetails và tiếp tục vòng lặp trong cột tiếp theo.
//matrix[row,column] is the first not null item
//move it to the first null item space
int columnFirstNullItem = -1;
//again, this is to help on the foreach loop that follows
//for a left swipe, we want to check the columns 0 to [column-1]
//for a right swipe, we want to check columns [Globals.Columns-1] to column+1 int numberOfItemsToTake = horizontalMovement == HorizontalMovement.Left
? column : Globals.Columns – column;
bool emptyItemFound = false;
Trang 10//keeping it for documentation/clarity
//this for loop would run for a left swipe ;)
//for (columnFirstNullItem = 0; columnFirstNullItem < column;
columnFirstNullItem++)
foreach (var tempColumnFirstNullItem in
columnNumbers.Take(numberOfItemsToTake))
{
//keep a copy of the index on the potential null item position
columnFirstNullItem = tempColumnFirstNullItem;
if (matrix[row, columnFirstNullItem] == null)
{
emptyItemFound = true;
break;//exit the loop
}
}
Nếu các ô này không giống nhau, thì chúng ta phải di chuyển ô mà chúng ta hiện đang tham chiếu đến vị trí rỗng đầu tiên Đối với thao tác vuốt sang trái, nếu ô là [hàng, cột], thì các vị trí có thể duy nhất là từ [hàng, 0] đến [hàng, cột-1], do đó chúng ta sẽ cần các ô cột đầu tiên từ danh sách Số cột Đối với thao tác vuốt sang phải, các vị trí duy nhất có thể là từ [hàng, Globals.Columns-1] đến [hàng, cột + 1], vì vậy chúng ta cần
Globals.Columns đầu tiên - các ô cột từ danh sách Cột Số bị đảo ngược Ta thực hiện một vòng lặp trong các cột này (sử dụng phương pháp Take LINQ) giữ tham chiếu đến từng
số cột (thông qua biến columnFirstNullItem) và kiểm tra từng ô nếu nó là rỗng Nếu tìm thấy một, ta thoát khỏi vòng lặp.
//we did not find an empty/null item, so we cannot move current item
if (!emptyItemFound)
{
continue;
}
ItemMovementDetails newImd =
MoveItemToNullPositionAndCheckIfSameWithNextOne
(row, row, row, column, columnFirstNullItem, columnFirstNullItem +
relativeColumn);
movementDetails.Add(newImd);
}
}
return movementDetails;
}
Nếu ta không tìm thấy ô rỗng, thì ô hiện được tham chiếu đã ở đúng vị trí của nó, vì vậy
ta để nguyên như vậy Nếu chúng ta làm vậy, thì chúng ta di chuyển ô hiện được tham chiếu đến vị trí rỗng và chúng ta tạo một thể hiện của lớp ItemMovementDetails, để mang thông tin hoạt ảnh Ở cuối phương thức MoveHoriz ngang, ta trả về danh sách MovementDetails, chứa thông tin cho tất cả các hoạt ảnh phải được thực hiện.