Làm game Android trong 2 ngày - Day 1

Bài này mình lấy từ http://vietgamedev.net/, nhưng có bổ sung code ngay trong bài cho dễ nghiên cứu


bởi 
Sau một thời gian dài mày mò hết engine này đến framework nọ, mình quyết định quay về với thứ ngôn ngữ của tự nhiên để phát triển ứng dụng chính thức đầu tay của mình và tung lên Google Play. 

Trước tiên mời các bạn xem qua em nó


Và cả link download nữa cho những ai muốn dùng thử


Khà khà, thế là thủ đoạn PR trá hình đã hoàn tất. 

Bây giờ chúng ta quay lại chủ đề chính.

App này được phát triển trong 2 ngày tính từ lúc lên ý tưởng cho đến publish thành công lên Google Play. Và bài viết này nhằm mục đích chia sẽ lại quá trình phát triển đứa con mì ăn liền này. Mình sẽ ko trình bày quá chi tiết về kĩ thuật mà chỉ nói sơ qua những điểm chính cần lưu ý. Source code sẽ được đính kèm để các bạn tham khảo chi tiết hơn.

Tất nhiên mình không phải là một nhà phát triển hoành tráng nào đó hay là một success story đáng ngưỡng mộ nào kia để ngồi đây ba hoa về những kinh nghiệm thành công hay dạy đời thiên hạ. 

Thực tế là tính từ lúc đưa lên store đến giờ (thời điểm viết bài) đã tròn 48 tiếng đồng hồ và sản phẩm này thất bại ngoài mong đợi, thế nên mình tự tin nhận định rằng câu chuyện của mình có lẽ là "fail story"  rất đáng để mọi người đọc qua và tránh gặp phải 04 chi tiết xin mời đọc tiếp ạ

(Đọc đến đây thể nào cũng có người bảo: "móa cái thằng, thất bại rồi mà nó cũng còn ba hoa được, thành công chắc nó nổ vang trời đây"  )

Mở bài dài dòng quá nên mình bắt đầu luôn đây.

Ngày thứ 1 - Sáng thứ 7

8h sáng: Lục đục chạy ra ACB mở tài khoản, gồm có: 1 thẻ VISA Debit, 1 tài khoản ngân hàng link đến thẻ ATM và thẻ VISA kia, lệ phí thì free và nộp vào 700k làm vốn.

8h30: Cầm thẻ về và điền thông tin tài khoản vô Google Wallet, active lại đơn đặt hàng Google Developer Account đã bị reject cách đây gần 1 năm vì chưa thanh toán, thấp thỏm chờ đợi

8h45: Giao dịch thành công, Developer Console đã được kích hoạt, sướng, bay vào và dạo quanh xem nhà mới có gì hay ho không

9h: Nghĩ ngay đến việc làm một cái gì đó để mở hàng, thực ra trước đây đã có vài cái app đang publish trên Appstore.VN nhưng lần này muốn bắt đầu làm mới một cái gì đó, ngồi suy nghĩ và tìm ý tưởng, tiêu chí đặt ra là phải làm một cái gì đó vừa nhanh vừa dễ, vì đã máu rồi, mà tính mình thì hay nản, chọn ý tưởng khó quá làm một hồi lại nản thì chỉ tổ phí thời gian. 

9h30: Một ý tưởng lóe lên, hội tụ đủ các yếu tố đã đặt ra, một game luyện trí nhớ bằng cách lật từng cặp hình. Ờ, ý tưởng tốt đấy, để xem có thể vẽ rồng vẽ rắn được gì với cái ý tưởng này không, thử sáng tạo một chút nào...

9h45: ...

10h: ...

10h15: mịt mù vãi, hay là bỏ không làm nữa nhỉ

10h20: đệt, lại quảng cáo, lần này là cái gì đây, Jiê A Vê ai đồ à   à, đúng rồi, Japanese thì Japanese. Nghĩ lại thì mình học tiếng Nhật khổ nhất là nhớ bảng chữ, làm 1 trò luyện chữ cũng hay đây

10h30: sột, soẹt, ý tưởng hoàn thành (động cơ không trong sáng tí nào 18 Wink

10h31: bắt tay vào làm luôn và ngay.

Xử lý layout dạng lưới

Layout: main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:background="@drawable/bg">
    <LinearLayout android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content"
                  android:id="@+id/cellPanel">
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
        <LinearLayout
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="65dp" android:id="@+id/cellPack">
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1"/>
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
            <com.colrist.Hiramem.HiraButton
                    android:text="Btn"
                    android:layout_height="wrap_content"
                    android:layout_width="0dp"
                    android:layout_weight="1" />
        </LinearLayout>
    </LinearLayout>
    <LinearLayout android:orientation="horizontal"
                  android:layout_width="fill_parent"
                  android:layout_height="110dp"
                  android:layout_weight="1"
                  android:background="@drawable/bottom"
                  android:visibility="visible" android:layout_alignParentLeft="true"
                  android:layout_alignParentTop="false"
                  android:layout_alignParentBottom="true" android:id="@+id/controlPanel">
        <ImageButton
                android:layout_width="0dp"
                android:layout_height="fill_parent"
                android:id="@+id/imageButton"
                android:layout_weight="1"
                android:background="@drawable/random"/>
        <TextView
                android:layout_width="0dp"
                android:layout_height="fill_parent"
                android:text="-"
                android:layout_weight="1"
                android:id="@+id/textView" android:textColor="@android:color/background_dark" android:textSize="30dp"
                android:gravity="center" android:background="@drawable/kana"/>
        <ImageButton
                android:layout_width="0dp"
                android:layout_height="fill_parent"
                android:layout_weight="1"
                android:id="@+id/imageButton1" android:background="@drawable/reset"/>
    </LinearLayout>
</RelativeLayout>


Sau một hồi suy nghĩ về cấu trúc ý tưởng, phương hướng thực hiện thì mình quyết định Layout editor của IntelliJ IDEA để bố trí giao diện cho game.

Tạo một custom View để làm nút bấm vì mình không thích dùng Button có sẵn của Android, quá xấu 18


Về phần Custom View dùng để làm nút nhấn. Ở đây mình tạo một Class tên là HiraButton và override hàm OnDraw để thực hiện việc vẽ một hình vuông màu xanh, có 2 trạng thái. Lúc bình thường (not isSelected) thì là hình vuông màu xanh, lúc được chọn (isSelected) thì sẽ to ra một tí và hiện lên 1 kí tự được gán cho nó.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (Enabled) {
            Paint p = new Paint();
            if (!isSelected) {
                p.setColor(Color.parseColor("#3773D2"));
                canvas.drawRect(5, 5, getWidth() - 5, getHeight() - 5, p);
            } else {
                p.setColor(Color.parseColor("#6096eb"));
                canvas.drawRect(1, 1, getWidth() - 1, getHeight() - 1, p);
                p.setColor(Color.WHITE);
                p.setTextSize(getWidth()*.65f);
                canvas.drawText(text, getWidth() / 2 - p.measureText(text) / 2, getHeight() / 2 + p.measureText(text) / 1.5f, p);
            }
        }
    }

Đến đây thì mình gặp phải một vấn đề. 

Theo như ý tưởng của trò chơi, trên màn hình sẽ xuất hiện một lưới nhiều ô vuông, và mình không biết cách tạo lưới như thế nào cho đúng quy cách. (Mình chưa viết app cho Android dùng native java bao giờ)

Theo như trên màn hình nhìn thấy thì có GridLayout giải quyết tốt vấn đề này nhưng nó chỉ hỗ trợ từ phiên bản Android 4.0 trở đi, trong khi đó mình xác định game này sẽ phục vụ tốt cho cả các máy đang chạy 2.2


Thế nên sẽ không dùng được cái grid layout này. Sau một hồi tìm tòi thì mình đưa ra một giải pháp, đó là dùng các Linear Layout lồng ghép nhau. Cụ thể thì hướng giải quyết như sau:

Đầu tiên, tạo một Linear Layout có hướng là Vertical để chứa từng dòng, tạm đặt tên nó là cellPanel


Sau đó là tạo ra các LinearLayout con đặt ở từng dòng, các layout con này có hướng là horizontal, tạm đặt tên là cellPack


Kết hợp lại chúng ta sẽ có được một lưới như mong muốn. Và gắp từng đối tượng CustomView HiraButton đã tạo thả vào từng cellPack.

Sau đó tạo tiếp 2 Image button để làm 2 nút chức năng và 1 textView ở giữa để hiển thị kí tự đang được chọn. Layout hoàn chỉnh như sau:


Thế là khó khăn thứ nhất đã vượt qua nhưng tiêu tốn mất một khoảng thời gian kha khá.

Tiếp đến vấn đề thứ 2 nảy sinh.

Bubble Event - truyền một event từ lớp con đến lớp cha

Yêu cầu của trò chơi thì khi click vào một ô, ô được click sẽ sáng lên (Selected) và tất cả các ô còn lại sẽ là Non Selected. Như vậy, phương hướng giải quyết sẽ là thế nào đây? Việc xử lý này chắc chắn phải nằm ở Activity chính chứ không phải xử lý ở từng button một, mục đích để cho chương trình có cấu trúc thống nhất và dễ quản lý hơn.

Nghĩ ngay đến việc cần có một danh sách các nút, và danh sách này phải được truy cập ở bất kì đâu trong chương trình để phục vụ cho nhiều trường hợp xử lý, mình viết một class để lưu trữ các biến static để có thể sử dụng lại ở bất cứ đâu.

Đặt tên class là GlobalVars (cái tên nói lên công dụng của nó rồi nên ko cần giải thích gì thêm 04)

1
2
3
4
public class GlobalVars {
    public static ArrayList buttonList = new ArrayList();
    public static HiraButton currentClick = null;
}

buttonList là danh sách các button hiện có trên màn hình vì số lượng nút được tạo ra tùy thuộc vào kích thước của màn hình đang chạy. Và tất nhiên sẽ cần 1 đối tượng button riêng (currentClick) để xử lý.

Tiếp theo, làm sao để add các nút vào danh sách buttonList khi mà chúng ta tạo nó ra bằng cách add vào layout chứ không phải bằng code? giải pháp là add nó vào ngay trong constructor của từng nút.

1
2
3
4
public HiraButton(Context context) {
        super(context);
        GlobalVars.buttonList.add(this);
    }

Vậy là mỗi khi một nút HiraButton được add lên màn hình, nó sẽ tự động được thêm vào danh sách buttonList kia.

Bây giờ đến việc xử lý tắt mở các nút khi một nút bất kì được nhấn.

Cách giải quyết được đặt ra như sau: Khi 1 button được nhấn -> Truyền sự kiện click lên Activity -> Tại Activity, quét tất cả các button (buttonList) và set cho nó là Not Selected -> Set button hiện tại (currentClick) là Selected.

Và để truyền sự kiện click từ một View lên Activity, chúng ta chỉ cần gọi một cách đơn giản như sau:

Tại HiraButton.java

1
2
3
4
5
6
7
8
@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Enabled) {
            GlobalVars.currentClick = this;
            ((Activity)getContext()).onTouchEvent(event);
            return true;
        } else return false;
    }

Set đối tượng currentClick là chính cái view hiện tại để lên Activity còn biết thằng nào đang được nhấn, và gọi sự kiện onTouchEvent của Activity cha.

Và ở xử lý onTouchEvent của Activity cha thì tiến hành set selected, not selected như quy trình đã ghi ở trên:

Hiramem.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // Close all cells
                if (!GlobalVars.buttonList.isEmpty()) {
                    for (int i = ; i < GlobalVars.buttonList.size(); i++) {
                        if (i < maxProcess) {
                            GlobalVars.buttonList.get(i).isSelected = false;
                            GlobalVars.buttonList.get(i).invalidate();
                        }
                    }
                }
                // Open only current cell
                if (GlobalVars.currentClick != null) {
                    GlobalVars.currentClick.isSelected = true;
                    GlobalVars.currentClick.invalidate();
                }
            } break;
        }
        return super.onTouchEvent(event);    
    }

Dành cho bạn nào chưa biết. Thì việc đưa đoạn xử lý trên kia vào khối switch case là để ngăn sự kiện touch down được gọi đi gọi lại nhiều lần khi người dùng giữ ngón tay vào khi touch.

Add thêm hình ảnh vào cho sinh động

Ở giai đoạn này thì không có vấn đề gì ghê gớm cả. Chỉ đơn giản là set thuộc tính background thành một hình ảnh nào đó đặt trong thư mục drawable của project. 

Layout sau khi đã được mông má nhìn đẹp trai hơn hẳn. 18

 

Xử lý game với nhiều độ phân giải khác nhau

Vấn đề đau thương cuối cùng của ngày đầu tiên đó là vấn đề tương thích giữa các loại màn hình.

Có hàng trăm loại máy Android khác nhau và vì thế cũng có hàng tá kích thước màn hình khác nhau. Không ai đủ sức để thiết kế từng phiên bản riêng biệt cho từng kích cỡ màn hình khác nhau được, vì thế giải pháp đặt ra là tìm một phương pháp tính toán xử lý phù hợp nhất để nó có thể hoạt động tốt trên nhiều loại màn hình nhất có thể.

Vì bắt tay vào thiết kế game này ở độ phân giải 320x480 (độ phân giải của trình giả lập đang được chọn) nên khi thử chuyển sang test trên điện thoại (800x1280) thì tá hỏa khi phát hiện ra mọi thứ vỡ tung một cách thê thảm.

Nguyên nhân chính là khi sang một màn hình có chiều rộng dài hơn hoặc ngắn hơn thì hình ảnh sẽ bị bóp méo.

Vậy nên phải tiến hành resize các thành phần trên màn hình lại ngay khi game được khởi động. Đây là một thao tác xử lý khá ẩu, mang tính tình thế, chữa cháy và hậu quả là hiện tại ở một số máy sẽ xảy ra tình trạng Memory Overflow không khởi động lên được, tắt ngay khi khởi động.

Để thay đổi kích thước của một item trên màn hình, chúng ta cần tác động vào LayoutParams của item đó. Cụ thể muốn thay đổi kích thước của panel có 3 nút tròn dưới đáy màn hình như hình trên thì xử lý như sau:

1
2
3
LinearLayout controlPanel = (LinearLayout)findViewById(R.id.controlPanel);
controlPanel.getLayoutParams().height = height;
controlPanel.requestLayout();

Dùng hàm getLayoutParams() để thay đổi các thuộc tính về độ rộng, độ cao rồi sau đó gọi hàm requestLayout() để thực hiện các thay đổi.

Ngày thứ 1 kết thúc vào lúc 11:00 PM

Sau đó là thời gian chiến Dota, Facebook, Youtube các thứ đến tầm 1h thì đi ngủ, đánh dấu sự kết thúc của ngày cuối tuần ngập chìm trong code 18

Nhận xét