Lưu điểm số, thông tin của game hay trạng thái lần chơi gần nhất là một điều quan trọng trong bất kì game nào. Người chơi không muốn mỗi lần chơi game ra phải chơi lại từ đầu giống như lúc vừa cài đặt, chơi lại từng màn đã qua.
Unity hỗ trợ sẵn tính năng lưu lại thông tin và trạng thái trong game thông qua lớp PlayerPrefs
. Bài viết này sẽ hướng dẫn cách thao tác với PlayerPrefs
để thực hiện tính năng "save game" với Unity.
Lưu trạng thái game
PlayerPrefs
hỗ trợ lưu trữ 3 kiểu dữ liệu cơ bản: Int
, Float
, và String
.
Tương ứng với 3 phương thức là SetInt
, SetFloat
, SetString
.
Cả 3 phương thức này đều nhận vào:
- 2 tham số trong đó tham số đầu tiên là tên đại điện cho giá trị được lưu trữ.
- Tham số còn lại là nội dung cần lưu trữ.
Cần ghi nhớ key này để có thể lấy lại được nội dung đã lưu trữ về sau.
Mỗi khi thực hiện các hàm set thì dữ liệu chỉ thực sự được lưu xuống đĩa khi gọi hàm Save()
.
Dưới đây là cách dùng PlayerPrefs
để lưu thông tin người chơi.
PlayerPrefs.SetString("username", "PhamNgocPhuoc"); PlayerPrefs.SetString("password", "stdio1235"); PlayerPrefs.SetInt("level", 10);
Truy xuất thông tin
Sau khi đã sử dụng PlayerPrefs
để lưu trữ thông tin, sử dụng các phương thức GetInt
, GetFloat
, GetString
để truy xuất các nội dụng đã lưu trữ.
Cũng giống như các phương thức Set
ở trên, khi dùng các phương thức Get
phải lựa chọn hàm tương ứng với kiểu dữ liệu đã ghi trước đó.
Các hàm Get(X)
nhận vào 1 tham số là tên của key
và có kiểu trả về X
(với X
là Int
, Float
hoặc String
).
Truy xuất các thông số đã lưu trữ ở trên như sau:
String _userName = PlayerPrefs.GetString("username"); String _password = PlayerPrefs.GetString ("password"); Int _level = PlayerPrefs.GetInt("level");
Nếu truyền vào 1 Key
chưa tồn lại, ứng mỗi phương thức Get
sẽ trả về cho 1 giá trị mặc định.
GetString
thì giá trị đó là 1 chuỗi trỗng""
.GetFloat
là0.0f
.GetInt
là0
.
Một số phương thức hỗ trợ
DeleteAll | Xóa tất cả các key và giá trị đã được lưu trữ. |
DeleteKey | Xóa 1 key cụ thể. |
GetFloat | Trả về giá trị float tương ứng với key nếu key tồn tại. |
GetInt | Trả về giá trị int tương ứng với key nếu key tồn tại. |
GetString | Trả về giá trị string tương ứng với key nếu key tồn tại. |
HasKey | Trả về true nếu key tồn tại. |
Save | Lưu trữ tất cả dữ liệu được chỉnh sửa xuống đĩa. |
SetFloat | Lưu giá trị float theo key vào bộ nhớ chính. |
SetInt | Lưu giá trị int theo key vào bộ nhớ chính. |
SetString | Lưu giá trị string theo key vào bộ nhớ chính. |
Đường dẫn tệp tin lưu trữ
Nơi lưu trữ dữ liệu của PlayerPrefs
ở các nền tảng khác nhau:
- Mac OS: thư mục ~/Library/Preferences trong tập tin unity.[company name].[product name].plist.
- Windows: lưu trữ ở registry dưới đường dẫn HKCU\Software\[company name]\[product name].
- Linux: lưu trữ ở đường dẫn ~/.config/unity3d/[CompanyName]/[product name].
- Windows Store Apps: %userprofile%\AppData\Local\Packages\[ProductPackageId]>\LocalState\playerprefs.dat.
- WebPlayer:
PlayerPrefs
được lưu trữ trong tệp tin nhị phân với đường dẫn:- Mac OS X: ~/Library/Preferences/Unity/WebPlayerPrefs/
- Windows: %APPDATA%\Unity\WebPlayerPrefs/
Bảo mật
Nếu truy cập vào trong đường dẫn lưu trữ PlayerPrefs
thì có thể nhận thấy rằng các thông tin lưu trữ trong đó đều ở dạng nguyên gốc như lúc truyền vào. Điều này làm cho các thông tin này dễ dàng bị sửa đổi bất hợp pháp dẫn đến sai lệch thông tin của game hay đánh cắp thông tin người dùng (username, password, email, ...).
Dưới đây là màn chơi của game AngryBird được viết bằng Unity trong đó giá trị High Score
được dùng để đánh dấu lại số điểm cao nhất từng đạt được của màn chơi đó sử dụng PlayerPrefs
và thông tin mà PlayerPrefs
lưu trữ. Giá trị này có thể dễ dàng được thay đổi bằng tay và dẫn đến game cũng bị ảnh hưởng theo.
Giá trị High Score
trước khi bị chỉnh sửa:
Nơi lưu trữ giá trị High Score
trên môi trường Windows:
Giá trị High Score
sau khi đã chỉnh sửa:
Để hạn chế điều này có thể sử dụng các phương thức Encrypt hoặc Decrypt có sẵn trong C# để mã hóa dữ liệu trước khi lưu trữ xuống.
Bài viết hiện thực đơn giản 1 lớp dùng cho việc mã hoá và giải mã tập tin như sau:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace EncryptStringSample { public static class StringCipher { // This constant string is used as a "salt" value for the PasswordDeriveBytes function calls. // This size of the IV (in bytes) must = (keysize / 8). Default keysize is 256, so the IV must be // 32 bytes long. Using a 16 character string here gives us 32 bytes when converted to a byte array. private static readonly byte[] initVectorBytes = Encoding.ASCII.GetBytes("tu89geji340t89u2"); // This constant is used to determine the keysize of the encryption algorithm. private const int keysize = 256; public static string Encrypt(string plainText, string passPhrase) { byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText); using (PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null)) { byte[] keyBytes = password.GetBytes(keysize / 8); using (RijndaelManaged symmetricKey = new RijndaelManaged()) { symmetricKey.Mode = CipherMode.CBC; using (ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes)) { using (MemoryStream memoryStream = new MemoryStream()) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); cryptoStream.FlushFinalBlock(); byte[] cipherTextBytes = memoryStream.ToArray(); return Convert.ToBase64String(cipherTextBytes); } } } } } } public static string Decrypt(string cipherText, string passPhrase) { byte[] cipherTextBytes = Convert.FromBase64String(cipherText); using(PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null)) { byte[] keyBytes = password.GetBytes(keysize / 8); using(RijndaelManaged symmetricKey = new RijndaelManaged()) { symmetricKey.Mode = CipherMode.CBC; using(ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes)) { using(MemoryStream memoryStream = new MemoryStream(cipherTextBytes)) { using(CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { byte[] plainTextBytes = new byte[cipherTextBytes.Length]; int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); } } } } } } } }
Cách sử dụng và thao tác với lớp StringCipher
:
using System; using System.Linq; namespace EncryptStringSample { class Program { static void Main(string[] args) { Console.WriteLine("Please enter a password to use:"); string password = Console.ReadLine(); Console.WriteLine("Please enter a string to encrypt:"); string plaintext = Console.ReadLine(); Console.WriteLine(""); Console.WriteLine("Your encrypted string is:"); string encryptedstring = StringCipher.Encrypt(plaintext, password); Console.WriteLine(encryptedstring); Console.WriteLine(""); Console.WriteLine("Your decrypted string is:"); string decryptedstring = StringCipher.Decrypt(encryptedstring, password); Console.WriteLine(decryptedstring); Console.WriteLine(""); Console.ReadLine(); } } }
Áp dụng mã hóa đối với trường hợp phía trên, lúc này giá trị HighScore sẽ được mã hóa thành một chuỗi kí tự không quy tắc.
Chuỗi ký tự này có thể đảm bảo thông tin không bị đánh cắp hoặc chỉnh sửa trái phép nhưng chỉ ở mức tương đối an toàn. Các hacker vẫn có thể phát hiện ra thuật toán mã hóa đang dùng, mã hóa 1 chuỗi kết quả khác tương tự và thay thế vào đó. Nhưng dù sao thì đây vẫn là 1 cách tương đối tốt để tự bảo mật dữ liệu cho game.