+8

Tập làm game "Flappy X" trong unity

Lời mở đầu

  • Đầu xuân năm mới xin kính chúc bạn đọc một năm RỒNG thật nhiều thành công trong cuộc sống và sự nghiệp, quan trọng là ông bà nào vẫn còn đang FA thì sẽ sớm tìm được một nửa của mình. Và đúng với tinh thần "new year, new me" thì bài viết lần này sẽ là một chủ đề hoàn toàn mới (so với những bài viết của mình trước đây), đó là tập làm game với Unity.
  • Và con game chúng ta sẽ cùng nhau làm ở đây là Flappy X, một người anh em "khác cha, khác ông nội" của Flappy Bird - một trò chơi từng làm mưa làm gió một thời trên khắp thế giới khoảng năm 2013.
  • Và đây sẽ là kết quả demo mà mình thu được (show trước để các bạn còn có động lực làm theo 😂😂) https://www.loom.com/share/beeac1a04a584688bc01346c4e5e4429?sid=f410d238-100f-4631-b113-34564413d1fa
  • Và không làm mất thời gian của các bạn nữa, chúng ta cùng bắt đầu thôi nào

LƯU Ý: để tránh cho bài viết quá dài thì trong bài viết này, chúng ta sẽ tập trung vào phần gameplay là chính nhé các bạn.

Nội dung

Chuẩn bị

  • Trước khi bắt đầu thì các bạn sẽ cần chuẩn bị hình ảnh cho những đối tượng ở trong game (gọi chung là textures).

    • Background:
    • Mặt đất:
    • Chướng ngại vật:
    • Nhân vật X:
  • Thông thường các bạn sẽ có 2 cách để chuẩn bị textures:

    • C1: tự thiết kế: các bạn có thể tự thiết kế texture theo ý của bạn thân thông qua những phần mềm như (...). Cách này tuy hơi mất công và thời gian nhưng sẽ đảm bảo được dấu ấn cá nhân của bạn trong game và "có thể" yên tâm về vấn đề bản quyền.
    • C2: sử dụng textures có sẵn: nếu chỉ cần tập trung vào làm game thì bạn hoàn toàn có thể download những tài nguyên được thiết kế sẵn và chia sẻ trên internet. Tuy nhiên, khi sử dụng bạn cần kiểm tra kỹ về vấn đề bản quyền, cũng như là về văn hoá/pháp luật, nhất là nếu bạn có ý định làm game thương mại. Dưới đây có một số website chia sẻ tài nguyên hình ảnh/âm thanh cho game miễn phí mà các bạn có thể sử dụng.
  • Và mình sẽ chọn cách 2, tức là dùng luôn texture của game flappy bird và bạn có thể tải nó về ở đây https://github.com/samuelcust/flappy-bird-assets

"Chiến" thôi

1. Tạo đối tượng:

1.1. Tạo animation: đơn giản thì animation chỉ là chuyển đổi giữa các bức ảnh khác nhau. Để tạo animation rất đơn giản, bạn chỉ cần bôi đen những bức ảnh muốn tạo thành animation rồi kéo chúng vào Scene (VD chọn 3 ảnh redbird-upflap, redbird-midflap, redbird-downflap để tạo thành animation vỗ cánh)

  • Khi này các bạn sẽ thấy có 2 file được tạo ra:

    • animation: là một hình ảnh động mô tả hoạt động của đối tượng trong game (ở đây là hành động vỗ cánh)
    • animator: có chức năng điều khiển các animation của đối tượng trong game nếu đối tượng đó có nhiều animation khác nhau (chạy/nhảy/...). Còn ở trong game này thì chúng ta chỉ có một animation duy nhất là vỗ cánh nên có thể tạm bỏ qua phần này.
  • Sau đó, chúng ta sẽ click vào animation đó để tuỳ chỉnh chỉ số "sample" ở trong cửa sổ animation, để thay đổi thời gian chuyển đổi giữa các khung hình với nhau để tạo thành một hình ảnh động (số càng to thì hình ảnh vỗ cánh càng nhanh, số càng nhỏ thì vỗ cánh càng chậm)

1.2. Tạo hiệu ứng vật lý: 1.2.1. Thêm component RigidBody 2D (để quản lý vật lý) vào cho đối tượng (ở cửa sổ inspector, chọn "add component"): trong game này thì chúng ta sẽ cần quan tâm đến phần gravity scale để tạo ra trọng lực kéo con chim rơi xuống đất (số càng nhỏ thì rơi xuống càng chậm, số càng lớn rơi xuống càng nhanh).

1.2.2. Thêm component collider: để quản lý va chạm của một vật, tức là tạo ra khung hình bao quanh một đối tượng, về bản chất thì 2 đối tượng được tính là chạm vào nhau khi mà collider của 2 đối tượng chạm vào nhau. Unity cung cấp rất nhiều loại collider, các bạn có thể chọn một loại sao cho phù hợp với hình ảnh đối tượng để cho việc va chạm được chính xác nhất có thể. ở đây thì mình sẽ sử dụng Box Collider 2D.

1.3. Tạo script cho đối tượng

  • Ở cửa sổ inspector, chọn "add component", sau đó chọn new script và đặt tên cho nó là bird.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class bird : MonoBehaviour
{
    Rigidbody2D rg; //gọi component Rigidbody2D để quản lý đối tượng
    public GameObject gameOverObj; //tạo object GameOver

    public float speed; //khai báo tốc độ để dùng trong hàm update
    // Start is called before the first frame update
    private void Start() 
    {
        Time.timeScale = 1;
        rg = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    private void Update()
    {
        //kiểm tra xem người dùng có đang bấm chuột trái hoặc phím cách hay không
        //nếu có thì sẽ tạo ra 1 lực nâng đối tượng lên bằng hàm AddForce
        if(Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
        {
            //Vector2.up là tác động 1 lực theo chiều thẳng đứng lên
            rg.AddForce(Vector2.up * speed); 
        }
    }
    
    //hàm gameover, sẽ set thời gian về 0 để dừng game
    void GameOver() 
    {
        Time.timeScale = 0;
        gameOverObj.SetActive(true); //hiện panel GameOver
    }

    //hàm OnCollisionEnter2D sẽ gọi khi 2 collider va chạm với nhau 
    //lúc này sẽ gọi vào hàm game over
    private void OnCollisionEnter2D(Collision2D collision)
    {
        GameOver();
    }
    
    public void playAgain()
    {
        SceneManager.LoadScene(0); //khi gọi hàm thì sẽ trả về Scence số 0 (do game của chúng ta chỉ có duy nhất 1 scence)
    }
}

  • Ở cửa sổ inspector của Unity, tuỳ chỉnh giá trị speed để điều chỉnh xem mỗi lần bấm nút thì con chim sẽ bay lên được bao nhiêu (giá trị càng lớn thì bay được càng cao)

2. Thêm mặt đất:

  • Ở danh sách tài nguyên ảnh, kéo thả hình ảnh base.png vào trong scene và đặt xuống dưới khung hình. Sau đó chúng ta sẽ phải kéo dài mặt đất ra, nếu như chỉ kéo thả như bình thường thì các bạn sẽ thấy là bức ảnh sẽ bị vỡ và nhìn rất xấu. Vì vậy để có thể kéo dài mặt đất ra mà không ảnh hưởng đến chất lượng hình ảnh thì trước khi kéo, chúng ta sẽ phải thay đổi Draw Mode (ở trong Sprite Renderer) từ Simple thành Tiled. Lúc này khi chúng ta kéo dài nó ra thì hình ảnh sẽ lặp lại thay vì là bản thân nó bị kéo ra dẫn đến vỡ ảnh.

  • Tương tự như với đối tượng con chim, chúng ta sẽ phải thêm Box Collider 2D cho mặt đất để cho con chim dừng lại khi chạm vào mặt đất chứ không rơi xuyên qua.

3. Thêm chướng ngại vật (đường ống)

  • Tương tự như thêm mặt đất, chúng ta cũng sẽ kéo thả pipe-green vào trong scene để thêm nó vào khung hình. Sau đó thì click vào trong file pipe-green ở cửa sổ project và tuỳ chỉnh đơn vị pixels per unit (số càng to thì kích thước càng nhỏ), ở đây mình sẽ để là 80.

  • Tương tự như vậy chúng ta sẽ copy ra một cái pipe-green nữa và xoay nó ngược lại (Rotation Z = 180) để tạo ra cái ống từ trên xuống. Và đừng quên là thêm component BoxCollider2D cho đường ống để xử lý va chạm nhé.

  • Sau đó ta gom nhóm 2 cái pipe-greenpipe-green-reverse với nhau làm con của PipeObject để có thể quản lý chung.

  • Và bài toán của chúng ta trong khi chơi là con chim sẽ tiến lên phía trước. Nhưng thay vào đó thì chúng ta có thể cho ống nước di chuyển lùi về sau thì cũng sẽ cho ra một kết quả tương tự. Và để làm như thế thì chúng ta sẽ tạo ra một script cho thằng PipeObject để di chuyển vị trí của nó sang bên trái.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class pipe : MonoBehaviour
{
    public float speed;
    // Update is called once per frame
    void Update()
    {
        //dịch chuyển ống nước sang trái trau một khoảng thời gian
        transform.position += Vector3.left * speed * Time.deltaTime;
    }
}

  • Tất nhiên là chúng ta sẽ ko chơi game chỉ nhảy qua 1 ống rồi đúng ko? Vì vậy chúng ta sẽ phải tạo ra một game object mới, gọi là SpawnPipe và viết cho nó một script spawn để tự động sinh ra những pipe object tiếp theo sau một khoảng thời gian nhất định.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class spawn : MonoBehaviour
{
    public GameObject pipe; //khởi tạo một GameObject mới
    public float maxTime; //khoảng thời gian giữa mỗi lần sinh ra 1 ống nước
    float timer;
    public float height; //tạo khoảng random range từ -height đến height
    
    private void Start()
    {
        timer = maxTime; //tạo ngay 1 đường ống khi bắt đầu game
    }
    private void Update()
    {
        if (timer > maxTime)
        {
            //transform.position.y + Random.Range(-height,height) dùng để thay đổi ngẫu nhiên
            //vị trí của ống nước theo trục y (trục dọc)
            GameObject temp = Instantiate(pipe, new Vector3(transform.position.x, transform.position.y + Random.Range(-height,height),0), Quaternion.identity);
            Destroy(temp, 10f); //xoa cac ong nc cu sau 10s

            timer = 0;
        }

        timer += Time.deltaTime;
    }
}

  • Kéo thả thằng PipeObject từ Hierarchy xuống cửa sổ Project để tạo ra một prefab asset (dùng để tạo ra những object giống hệt nhau)

  • Lúc này chúng ta sẽ kéo thả thằng prefab PipeObject từ cửa sổ Project/Asset vào phần GameObject Pipe của script trong cửa sổ Inspector. Khi này script sẽ nhận PipeObject là GameObject để tự động sinh ra sau một khoảng thời gian maxTime.

4. Thêm background

  • Để tránh trường hợp background bị khoảng trống ở 2 bên nếu thay đổi tỉ lệ màn hình (như hình bên dưới) thì thay vì kéo thả thằng background-day vào như bình thường thì chúng ta sẽ tạo ra một đối tượng UI->Canvas.

  • Sau đó, chúng ta sẽ thiết lập cho thằng canvas như sau:
    • Render Mode: chọn sang Camera
    • Kéo Main Camera từ Hierarchy vào phần Render Camera.
  • Khi này, background sẽ tự động scale theo tỉ lệ màn hình cũng như là vừa khít với camera của chúng ta.

  • Tiếp theo là tạo một Image ở trong Canvas, đặt tên là background và thiết lập trong cửa sổ Inspector như sau:
    • Rect Transform: thiết lập thành stretch theo cả 2 trục
    • Kéo bức ảnh background-day từ cửa sổ asset và thả vào phần Source Image.

5. Tạo màn hình gameover

  • Trong canvas chúng ta sẽ tạo ra một UI -> Panel và đặt tên cho nó là GameOver. Bên trong nó sẽ có một ảnh hiện chữ GameOver và 1 button Retry. Sau đo thì thêm sự kiện onClick cho nút retry này, ở đây chúng ta sẽ thêm vào:
    • GameObject là redbird-upflap
    • Và function thì sẽ gọi vào hàm playAgain ở trong scipt bird.cs;

  • Sau đó, hãy nhớ bỏ tick ở cửa sổ Inspector của panel GameOver đi để nó ẩn đi trong quá trình chơi.

  • Khi nào xảy ra va chạm thì mới show panel GameOver lên. Để làm được như vậy thì chúng ta sẽ kéo GameOver panel vào làm Game Over Obj trong script bird.cs.

6. Tính điểm

6.1. Tạo text hiển thị điểm

  • Để làm được phần hiển thị điểm số, chúng ta sẽ tạo ra 1 canvas nữa tương tự như background. Sau đó thì tạo một TextMeshPro ở trong (đặt tên là Score) để hiển thị điểm số trên màn hình. Sau khi tuỳ chỉnh về màu sắc, font chữ, vị trí phù hợp thì ta sẽ thêm một script đặt tên là show.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class show : MonoBehaviour
{
    public int score;

    public TextMeshProUGUI text;

    public void AddScore()
    {
        score++;
        text.text = score.ToString();
    }
}

  • Sau đó đừng quên kéo object Score vào phần text của script show nhé.

6.2. Tạo logic cộng điểm

  • Hiểu đơn giản thì mỗi lần con chim bay qua khoảng trống giữa 2 đường ống thì chúng ta sẽ cộng thêm 1 điểm.
    • B1: Vào trong thằng PipeObject prefab, tạo ra một GameObject mới (ScoreSpace)
    • B2: Thêm component BoxCollider2D cho nó, tích vào ô Is trigger và điều chỉnh vùng collider vào giữa 2 đường ống. Tại sao lại phải chọn vào Is trigger thì mình sẽ giải thích một chút. Trong Unity có 2 loại va chạm:
      • Collision: là 2 vật chạm vào nhau nhưng không thể xuyên qua (như con chim với mặt đất/đường ống)
      • Trigger: là 2 vật chạm vào nhau và có thể xuyên được qua nhau (con chim bay xuyên qua ScoreSpace để tính điểm)
    • B3: Thêm script Score cho thằng ScoreSpace

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class score : MonoBehaviour
{
    private void OnTriggerExit2D(Collider2D collision) 
    {
        FindObjectOfType<show>().AddScore(); //gọi vào hàm AddScore ở trong script show
    }
}

Tổng kết

  • Sản phẩm chỉ nhằm mục đích học tập nên vẫn còn khá thô sơ, có gì chưa đúng mong mọi người góp ý để mình cải thiện thêm.
  • Cảm ơn các bạn đã quan tâm và dành thời gian ủng hộ bài viết. Chúc các bạn năm mới thật nhiều thành công và may mắn trong cuộc sống cũng như là công việc.

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí