Làm thế nào họ làm điều đó: Hàng triệu viên gạch ở Terraria


45

Tôi đã làm việc với một công cụ trò chơi tương tự như Terraria , chủ yếu là một thử thách và trong khi tôi đã tìm ra hầu hết trong số đó, tôi dường như không thể quấn đầu xung quanh cách họ xử lý hàng triệu viên gạch có thể tương tác / thu hoạch được Trò chơi đã có lúc. Tạo khoảng 500.000 ô, tức là 1/20 so với Terraria , trong công cụ của tôi khiến tốc độ khung hình giảm từ 60 xuống còn khoảng 20, ngay cả khi tôi vẫn chỉ hiển thị các ô trong chế độ xem. Nhắc bạn, tôi không làm gì với gạch, chỉ giữ chúng trong bộ nhớ.

Cập nhật : Mã được thêm vào để hiển thị cách tôi làm mọi thứ.

Đây là một phần của lớp, xử lý các ô và vẽ chúng. Tôi đoán thủ phạm là phần "foreach", nó lặp đi lặp lại mọi thứ, thậm chí cả các chỉ mục trống.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (Tile tile in this.Tiles)
        {
            if (tile != null)
            {
                if (tile.Position.X < -this.Offset.X + 32)
                    continue;
                if (tile.Position.X > -this.Offset.X + 1024 - 48)
                    continue;
                if (tile.Position.Y < -this.Offset.Y + 32)
                    continue;
                if (tile.Position.Y > -this.Offset.Y + 768 - 48)
                    continue;
                tile.Draw(spriteBatch, gameTime);
            }
        }
    }
...

Ngoài ra, đây là phương thức Tile.Draw, cũng có thể thực hiện với một bản cập nhật, vì mỗi Ngói sử dụng bốn lệnh gọi đến phương thức SpriteBatch.Draw. Đây là một phần của hệ thống tự động điền của tôi, có nghĩa là vẽ từng góc tùy thuộc vào các ô lân cận. texture_ * là hình chữ nhật, được đặt một lần ở mức tạo, không phải mỗi lần cập nhật.

...
    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        if (this.type == TileType.TileSet)
        {
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position, texture_tl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 0), texture_tr, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(0, 8), texture_bl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 8), texture_br, this.BlendColor);
        }
    }
...

Bất kỳ phê bình hoặc đề xuất cho mã của tôi đều được chào đón.

Cập nhật : Đã thêm giải pháp.

Đây là phương thức Level.Draw cuối cùng. Phương thức Level.TileAt chỉ cần kiểm tra các giá trị được nhập, để tránh các ngoại lệ OutOfRange.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        Int32 startx = (Int32)Math.Floor((-this.Offset.X - 32) / 16);
        Int32 endx = (Int32)Math.Ceiling((-this.Offset.X + 1024 + 32) / 16);
        Int32 starty = (Int32)Math.Floor((-this.Offset.Y - 32) / 16);
        Int32 endy = (Int32)Math.Ceiling((-this.Offset.Y + 768 + 32) / 16);

        for (Int32 x = startx; x < endx; x += 1)
        {
            for (Int32 y = starty; y < endy; y += 1)
            {
                Tile tile = this.TileAt(x, y);
                if (tile != null)
                    tile.Draw(spriteBatch, gameTime);

            }
        }
    }
...

2
Bạn có hoàn toàn tích cực không, bạn chỉ hiển thị những gì trong chế độ xem của máy ảnh, có nghĩa là mã để xác định nội dung chính xác?
Cooper

5
Tốc độ giảm từ 60 đến 20 khung hình / giây, chỉ vì bộ nhớ được phân bổ? Điều đó rất khó xảy ra, phải có điều gì đó không đúng. Nền tảng là gì? Là hệ thống trao đổi bộ nhớ ảo vào đĩa?
Maik

6
@Drackir Trong trường hợp này, tôi đã nói sai nếu thậm chí có một lớp gạch, một mảng các số nguyên có độ dài phù hợp nên làm và khi có nửa triệu đối tượng, OO không phải là trò đùa. Tôi cho rằng có thể làm với các đối tượng, nhưng vấn đề sẽ là gì? Một mảng số nguyên là đơn giản để xử lý.
aaaaaaaaaaaa

3
Ôi! Lặp lại tất cả các ô gọi Vẽ bốn lần trên mỗi ô đang xem. Chắc chắn có một số cải tiến có thể có ở đây ....
thedaian

11
Bạn không cần tất cả các phân vùng ưa thích được nói đến bên dưới để chỉ hiển thị những gì đang xem. Đó là một tilemap. Nó đã được phân vùng thành một lưới thông thường. Chỉ cần tính toán ô ở góc trên bên trái của màn hình và dưới cùng bên phải và vẽ mọi thứ trong hình chữ nhật đó.
Blecki

Câu trả lời:


56

Bạn có đang lặp qua tất cả 500.000 ô khi kết xuất không? Nếu vậy, điều đó có thể sẽ gây ra một phần vấn đề của bạn. Nếu bạn lặp qua nửa triệu ô khi kết xuất và nửa triệu ô khi thực hiện đánh dấu 'cập nhật' cho chúng, thì bạn sẽ lặp qua một triệu ô mỗi khung.

Rõ ràng, có nhiều cách xung quanh điều này. Bạn có thể thực hiện đánh dấu cập nhật của mình trong khi cũng hiển thị, do đó tiết kiệm cho bạn một nửa thời gian để lặp qua tất cả các ô đó. Nhưng điều đó liên kết mã kết xuất và mã cập nhật của bạn với nhau thành một hàm và nói chung là BAD IDEA .

Bạn có thể theo dõi các ô trên màn hình và chỉ lặp qua (và kết xuất) các ô đó. Tùy thuộc vào những thứ như kích thước gạch của bạn và kích thước màn hình, điều này có thể dễ dàng cắt giảm số lượng gạch bạn cần lặp lại và điều đó sẽ tiết kiệm khá nhiều thời gian xử lý.

Cuối cùng, và có lẽ là lựa chọn tốt nhất (hầu hết các game thế giới lớn làm điều này), là chia địa hình của bạn thành các khu vực. Chia thế giới thành các khối, ví dụ, các ô 512x512 và tải / dỡ các vùng khi người chơi đến gần hoặc xa hơn một vùng. Điều này cũng giúp bạn không phải vòng qua các ô ở xa để thực hiện bất kỳ loại 'cập nhật' nào.

(Rõ ràng, nếu công cụ của bạn không thực hiện bất kỳ loại đánh dấu cập nhật nào trên các ô, bạn có thể bỏ qua phần của câu trả lời này có đề cập đến những điều đó.)


5
Tuy nhiên, phần khu vực có vẻ thú vị, nếu tôi nhớ lại chính xác, đó là cách minecraft thực hiện, tuy nhiên, nó không hoạt động với cách địa hình ở Terraria phát triển ngay cả khi bạn không ở gần nó, như cỏ lan rộng, xương rồng phát triển và tương tự. Chỉnh sửa : Tất nhiên trừ khi họ áp dụng thời gian đã trôi qua kể từ lần cuối khu vực này hoạt động và sau đó thực hiện tất cả các thay đổi sẽ xảy ra trong khoảng thời gian đó. Điều đó thực sự có thể làm việc.
William Mariager

2
Minecraft chắc chắn sử dụng các khu vực (và các trò chơi Ultima sau này đã làm, và tôi khá chắc chắn nhiều trò chơi Bethesda đã làm). Tôi không chắc chắn về cách Terraria xử lý địa hình, nhưng họ có thể áp dụng thời gian đã trôi qua cho một khu vực "mới" hoặc họ lưu trữ toàn bộ bản đồ trong bộ nhớ. Loại thứ hai sẽ yêu cầu sử dụng RAM cao, nhưng hầu hết các máy tính hiện đại có thể xử lý được điều đó. Có thể có những lựa chọn khác chưa được đề cập.
thedaian

2
@MindWorX: Thực hiện tất cả các cập nhật khi tải lại sẽ không hoạt động - giả sử bạn có cả đống khu vực cằn cỗi và sau đó là cỏ. Bạn đi xa trong nhiều năm và sau đó đi về phía cỏ. Khi khối với cỏ tải, nó bắt kịp và nó lan rộng trong khối đó nhưng nó sẽ không lan vào các khối gần hơn đã được tải. Những thứ như vậy tiến NHIÊU chậm hơn so với các trò chơi, mặc dù - kiểm tra chỉ một nhóm nhỏ của gạch trong bất kỳ một chu kỳ cập nhật và bạn có thể làm cho địa hình xa xôi sống mà không cần tải xuống hệ thống.
Loren Pechtel

1
Thay thế (hoặc bổ sung) để "bắt kịp" khi một khu vực gần người chơi, thay vào đó bạn có thể có một mô phỏng riêng lặp đi lặp lại trên từng khu vực xa xôi và cập nhật chúng với tốc độ chậm hơn. Bạn có thể dành thời gian cho mỗi khung hình này hoặc để nó chạy trên một luồng riêng biệt. Vì vậy, ví dụ, bạn có thể cập nhật vùng mà trình phát nằm trong cộng với 6 vùng liền kề mỗi khung hình, nhưng cũng cập nhật thêm 1 hoặc 2 vùng bổ sung.
Lewis Wakeford

1
Đối với bất kỳ ai thắc mắc, Terraria lưu trữ toàn bộ dữ liệu gạch của mình trong một mảng 2d (kích thước tối đa là 8400x2400). Và sau đó sử dụng một số phép toán đơn giản để quyết định phần nào sẽ hiển thị.
FrenchyNZ

4

Tôi thấy một sai lầm lớn ở đây không được xử lý bởi bất kỳ câu trả lời nào. Tất nhiên bạn không bao giờ nên vẽ và lặp đi lặp lại nhiều gạch hơn thì bạn cũng cần. Điều ít rõ ràng hơn là cách bạn thực sự xác định gạch. Như tôi có thể thấy bạn đã tạo ra một lớp gạch, tôi luôn luôn làm điều đó nhưng đó là một sai lầm rất lớn. Bạn có thể có tất cả các loại hàm trong lớp đó và điều đó tạo ra rất nhiều xử lý không cần thiết.

Bạn chỉ nên lặp đi lặp lại những gì thực sự cần thiết để xử lý. Vì vậy, hãy suy nghĩ về những gì bạn thực sự cần cho gạch. Để vẽ, bạn chỉ cần một họa tiết, nhưng bạn không muốn lặp lại trên một hình ảnh thực tế vì những hình ảnh đó là quá trình lớn. Bạn chỉ có thể tạo một int [,] hoặc thậm chí là một byte không dấu [,] (nếu bạn không mong đợi nhiều hơn 255 kết cấu gạch). Tất cả những gì bạn cần làm là lặp lại các mảng nhỏ này và sử dụng một công tắc hoặc câu lệnh if để vẽ kết cấu.

Vậy bạn cần cập nhật những gì? Các loại, sức khỏe và thiệt hại dường như đủ. Tất cả những thứ này có thể được lưu trữ theo byte. Vậy tại sao không tạo một cấu trúc như thế này cho vòng lặp cập nhật:

struct TileUpdate
{
public byte health;
public byte type;
public byte damage;
}

Bạn thực sự có thể sử dụng loại để vẽ gạch. Vì vậy, bạn có thể tách cái đó (tạo một mảng của riêng nó) khỏi cấu trúc để bạn không lặp lại các trường sức khỏe và thiệt hại không cần thiết trong vòng rút thăm. Để cập nhật mục đích, bạn nên xem xét một khu vực rộng hơn sau đó chỉ là màn hình của bạn để thế giới trò chơi cảm thấy sống động hơn (các thực thể thay đổi vị trí khỏi màn hình) nhưng đối với bản vẽ của những thứ bạn chỉ cần lát có thể nhìn thấy.

Nếu bạn giữ cấu trúc trên, nó chỉ mất 3 byte cho mỗi ô. Vì vậy, để tiết kiệm và mục đích bộ nhớ này là lý tưởng. Đối với tốc độ xử lý, nó không thực sự quan trọng nếu bạn sử dụng int hoặc byte hoặc thậm chí int dài nếu bạn có hệ thống 64 bit.


1
Vâng, các lần lặp lại sau của động cơ của tôi đã phát triển để sử dụng điều đó. Chủ yếu là để giảm tiêu thụ bộ nhớ và tăng tốc độ. Các loại ByRef chậm so với một mảng chứa đầy các cấu trúc ByVal. Tuy nhiên, thật tốt khi bạn thêm nó vào câu trả lời.
William Mariager

1
Yeah vấp phải nó trong khi tìm kiếm một số thuật toán ngẫu nhiên. Ngay lập tức nhận thấy bạn nơi sử dụng lớp gạch của riêng bạn trong mã của bạn. Đó là một lỗi rất phổ biến khi bạn mới. Rất vui khi thấy bạn vẫn hoạt động kể từ khi bạn đăng bài này 2 năm trước.
Madmenyo

3
Bạn không cần healthhoặc damage. Bạn có thể giữ một bộ đệm nhỏ của các vị trí gạch được chọn gần đây nhất và thiệt hại trên mỗi vị trí. Nếu một ô mới được chọn và bộ đệm đã đầy thì hãy di chuyển vị trí cũ nhất từ ​​nó trước khi thêm ô mới. Điều này giới hạn số lượng gạch bạn có thể khai thác cùng một lúc nhưng dù sao cũng có giới hạn nội tại đối với điều này (đại khái #players * pick_tile_size). Bạn có thể giữ danh sách này cho mỗi người chơi nếu điều đó làm cho nó dễ dàng hơn. Kích thước không quan trọng đối với tốc độ; kích thước nhỏ hơn có nghĩa là nhiều ô hơn trong mỗi bộ đệm CPU.
Sean Middleditch

@SeanMiddleditch Bạn đúng nhưng đó không phải là vấn đề. Vấn đề là dừng lại và suy nghĩ những gì bạn thực sự cần để vẽ các ô trên màn hình và thực hiện các tính toán của bạn. Vì OP đã sử dụng cả một lớp nên tôi đã làm một ví dụ đơn giản, đơn giản đi một chặng đường dài trong tiến trình học tập. Bây giờ hãy mở khóa chủ đề của tôi về mật độ pixel, rõ ràng bạn là một lập trình viên cố gắng nhìn câu hỏi đó bằng con mắt của một nghệ sĩ CG. Vì trang web này cũng dành cho CG cho các trò chơi afaik.
Madmenyo

2

Có các kỹ thuật mã hóa khác nhau mà bạn có thể sử dụng.

RLE: Vì vậy, bạn bắt đầu với tọa độ (x, y) và sau đó đếm xem có bao nhiêu ô giống nhau tồn tại cạnh nhau (chiều dài) dọc theo một trong các trục. Ví dụ: (1,1,10,5) có nghĩa là bắt đầu từ tọa độ 1,1, có 10 ô xếp cạnh nhau của loại gạch 5.

Mảng lớn (bitmap): mỗi phần tử của mảng giữ trên loại gạch nằm trong khu vực đó.

EDIT: Tôi vừa bắt gặp câu hỏi xuất sắc này ở đây: Hàm hạt ngẫu nhiên để tạo bản đồ?

Bộ tạo tiếng ồn Perlin có vẻ giống như một câu trả lời hay.


Tôi không thực sự chắc chắn rằng bạn đang trả lời câu hỏi đang được hỏi, vì bạn đang nói về mã hóa (và tiếng ồn perlin), và câu hỏi dường như giải quyết các vấn đề về hiệu suất.
thedaian

1
Sử dụng mã hóa phù hợp (chẳng hạn như nhiễu perlin), sau đó bạn không thể lo lắng về việc giữ tất cả các ô đó trong bộ nhớ .. thay vào đó bạn chỉ cần yêu cầu bộ mã hóa (bộ tạo nhiễu) cho bạn biết những gì được cho là xuất hiện tại tọa độ cho trước. Có sự cân bằng ở đây giữa chu kỳ CPU và việc sử dụng bộ nhớ và bộ tạo nhiễu có thể giúp thực hiện hành động cân bằng đó.
Sam Axe

2
Vấn đề ở đây là nếu bạn đang tạo một trò chơi cho phép người dùng sửa đổi địa hình bằng cách nào đó, chỉ cần sử dụng trình tạo tiếng ồn để "lưu trữ" thông tin đó sẽ không hoạt động. Thêm vào đó, việc tạo ra tiếng ồn khá tốn kém, như hầu hết các kỹ thuật mã hóa. Bạn nên tiết kiệm chu kỳ CPU cho các công cụ chơi trò chơi thực tế (vật lý, va chạm, ánh sáng, v.v.) Tiếng ồn Perlin rất tốt cho việc tạo địa hình và mã hóa là tốt để lưu vào đĩa, nhưng có cách tốt hơn để xử lý bộ nhớ trong trường hợp này.
thedaian

Tuy nhiên, ý tưởng rất tuyệt vời, giống như thedaian, địa hình được tương tác với người dùng, điều đó có nghĩa là tôi không thể truy xuất thông tin một cách toán học. Tôi sẽ nhìn vào nhiễu dù sao đi nữa, để tạo ra địa hình cơ sở.
William Mariager

1

Bạn có thể nên phân vùng tilemap như đã đề xuất. Ví dụ, với cấu trúc Quadtree để loại bỏ bất kỳ xử lý tiềm năng nào (ví dụ: thậm chí chỉ đơn giản là lặp qua) các ô không cần thiết (không hiển thị). Bằng cách này, bạn chỉ xử lý những gì có thể cần xử lý và tăng kích thước của tập dữ liệu (bản đồ ô vuông) không gây ra bất kỳ hình phạt hiệu suất thực tế nào. Tất nhiên, giả sử rằng cây được cân bằng tốt.

Tôi không muốn nghe có vẻ buồn tẻ hoặc bất cứ điều gì bằng cách lặp lại "cũ", nhưng khi tối ưu hóa, luôn nhớ sử dụng các tối ưu hóa được hỗ trợ bởi công cụ / trình biên dịch của bạn, bạn nên thử nghiệm chúng một chút. Và vâng, tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Tin tưởng vào trình biên dịch của bạn, nó biết rõ hơn bạn trong hầu hết các trường hợp, nhưng luôn luôn, luôn luônđo hai lần và không bao giờ dựa vào guesstimates. Đây không phải là việc triển khai nhanh thuật toán nhanh nhất miễn là bạn không biết nút cổ chai thực sự ở đâu. Đó là lý do tại sao bạn nên sử dụng một trình lược tả để tìm các đường dẫn chậm nhất (nóng) của mã và tập trung vào việc loại bỏ (hoặc tối ưu hóa) chúng. Kiến thức cấp thấp về kiến ​​trúc mục tiêu thường rất cần thiết để vắt kiệt mọi thứ mà phần cứng cung cấp, vì vậy hãy nghiên cứu các bộ nhớ CPU đó và tìm hiểu dự đoán nhánh là gì. Xem những gì trình hồ sơ của bạn cho bạn biết về bộ nhớ cache / lượt truy cập nhánh / lỗi. Và như sử dụng một số dạng cấu trúc dữ liệu cây cho thấy, tốt hơn là nên có cấu trúc dữ liệu thông minh và thuật toán câm, hơn là cách khác. Dữ liệu đến trước, khi nói đến hiệu suất. :)


+1 cho "tối ưu hóa sớm là gốc rễ của mọi tội lỗi" Tôi phạm tội về điều này :(
thedaian

16
-1 một tứ giác? Quá mức một chút? Khi tất cả các ô có cùng kích thước trong trò chơi 2D như thế này (chúng dành cho terraria), cực kỳ dễ dàng để tìm ra phạm vi ô nào trên màn hình - đó chỉ là phần trên cùng bên trái (trên màn hình) cho gạch dưới cùng bên phải.
BlueRaja - Daniel Pflughoeft

1

Có phải đó là tất cả về quá nhiều cuộc gọi rút thăm? Nếu bạn đặt tất cả các kết cấu ô bản đồ của mình vào một hình ảnh duy nhất - tập bản đồ ô, sẽ không có chuyển đổi kết cấu trong khi kết xuất. Và nếu bạn gộp tất cả các ô của mình vào một Lưới duy nhất, nó sẽ được rút ra trong một lệnh gọi.

Về quảng cáo động ... Có lẽ cây tứ giác không phải là một ý tưởng tồi. Giả sử rằng gạch đang được đặt vào các lá và các nút không có lá chỉ là các lưới được ghép từ các con của nó, gốc nên chứa tất cả các gạch được ghép thành một lưới. Loại bỏ một ô yêu cầu cập nhật các nút (lưới đảo ngược) lên đến gốc. Nhưng ở mỗi cấp độ cây, chỉ có 1/4 số lưới được ghép lại, không nên nhiều như vậy, lưới 4 * cây_height có tham gia không?

Ồ và nếu bạn sử dụng cây này trong thuật toán cắt, bạn sẽ kết xuất không phải luôn là nút gốc mà là một số con của nó, vì vậy bạn thậm chí không phải cập nhật / khởi động lại tất cả các nút lên đến gốc, nhưng lên đến nút (không phải lá) dựng hình tại thời điểm này.

Chỉ là suy nghĩ của tôi, không có mã, có thể là vô nghĩa.


0

@ariances đúng. Vấn đề là mã vẽ. Bạn đang xây dựng một mảng gồm 4 * 3000 + vẽ các lệnh quad (24000+ vẽ các lệnh đa giác ) trên mỗi khung. Sau đó, các lệnh này được xử lý và chuyển sang GPU. Điều này là khá xấu.

Có một số giải pháp.

  • Kết xuất các khối lớn (ví dụ: kích thước màn hình) của gạch thành một kết cấu tĩnh và vẽ nó trong một cuộc gọi.
  • Sử dụng các shader để vẽ các ô mà không gửi hình học cho GPU mỗi khung hình (tức là lưu trữ bản đồ ô trên GPU).

0

Những gì bạn cần làm, là chia thế giới thành các khu vực. Tạo địa hình tiếng ồn Perlin có thể sử dụng một hạt giống chung, do đó, ngay cả khi thế giới không được tạo ra trước, hạt giống sẽ tạo thành một phần của tiếng ồn, giúp nối địa hình mới hơn vào các phần hiện có. Bằng cách này, bạn không cần phải tính toán nhiều hơn một bộ đệm nhỏ trước chế độ xem của người chơi tại một thời điểm (một vài màn hình xung quanh màn hình hiện tại).

Về mặt xử lý những thứ như cây mọc ở những khu vực cách xa màn hình hiện tại của người chơi, bạn có thể có bộ hẹn giờ chẳng hạn. Các bộ định thời này sẽ lặp đi lặp lại thông qua các tệp lưu trữ thông tin về các nhà máy, vị trí của chúng, v.v. Bạn chỉ cần đọc / cập nhật / lưu các tệp trong bộ định thời. Khi người chơi đến những nơi đó trên thế giới một lần nữa, động cơ sẽ đọc trong các tệp như bình thường và trình bày dữ liệu thực vật mới hơn trên màn hình.

Tôi đã sử dụng kỹ thuật này trong một trò chơi tương tự tôi đã làm năm ngoái, để thu hoạch và canh tác. Người chơi có thể đi xa khỏi các cánh đồng và khi trở về, các vật phẩm đã được cập nhật.


-2

Tôi đã suy nghĩ về cách xử lý vô số khối đó và điều duy nhất xuất hiện trong đầu tôi là Mẫu thiết kế Flykg. Nếu bạn không biết, tôi khuyên bạn nên đọc về nó: có thể giúp ích rất nhiều khi tiết kiệm bộ nhớ và xử lý: http://en.wikipedia.org/wiki/Fly weight_potype


3
-1 Câu trả lời này quá chung chung không hữu ích và liên kết bạn cung cấp không trực tiếp giải quyết vấn đề cụ thể này. Mẫu Flykg là một trong nhiều, nhiều cách để quản lý các tập dữ liệu lớn một cách hiệu quả; bạn có thể giải thích cách bạn sẽ áp dụng mô hình này trong bối cảnh cụ thể của việc lưu trữ các ô trong một trò chơi giống như Terraria không?
postgoodism
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.