Skip to content

Learn C++ with Raylib

Cài đặt Raylib library thông qua VCPKG và sử dụng VS2022 để tạo 1 project cho Raylib. Ngoài ra mình có thể dùng VSCode với các Template để chỉnh sẵn để dùng cho C++ theo khoá học trên Udemy. Mình sẽ dùng VSCode để cho gọn nhẹ.

Created date: 2022-02-12

2 Dàn ý cho Game

#include "raylib.h"

int main()
{
    const int windowWidth{ 1280 };
    const int windowHeight{ 720 };
    // Khai báo màn hình của Game trong Raylib
    InitWindow(windowWidth, windowHeight,"RPG Raylib Game"); // Đây là thư viện của Raylib, vì Raylib được viết bằng C nên không có OOP, gồm các class như trong C++, vì vậy không cần phải viết như raylib::initwindow... 
    // Vì vậy tạm thời mình cứ nghĩ là raylib đã được dùng namespace như trong C++ mà mình đã học mấy ngày qua
    SetTargetFPS(60);

    Texture2D map = LoadTexture("nature_tileset/OpenWorldMap24x24.png");

    while (!WindowShouldClose())
    {
        BeginDrawing();
        ClearBackground(WHITE);

        Vector2 mapPos{0.0,0.0};

        DrawTextureEx(map, mapPos,0.0,4.0, WHITE);
        EndDrawing();

    }
    CloseWindow();
}

3 Vectors, di chuyển trong Game

3.1 Hệ toạ độ

Trong Raylib, hệ toạ độ là

Vì khi render Map tiled của Game thì chỉ hiển thị 1 góc nhỏ của full map như sau.

Zoom Game 2 lần

Trong khi full map thì

Full Map

3.2 Movement for Map tiled

Yêu cầu:

  • Khi ấn phím A thì nhân vật di chuyển qua phải
  • Vì là topdown game nên nhân vật sẽ đứng lại 1 chỗ. Màn hình background (map) sẽ di chuyển sang trái

–> Tạo một vector variable cho cho Phím A. Tương tự S,D,W

Trong While loop khai báo như sau

        Vector2 direction{};

        if (IsKeyDown(KEY_A))
        {
            direction.x -=1.0;
        }

Sau đó thay đổi mapPos để thấy được movement

        Vector2 direction{};

        if (IsKeyDown(KEY_A)) direction.x -=1.0;
        if (IsKeyDown(KEY_D)) direction.x +=1.0;
        if (IsKeyDown(KEY_W)) direction.y -=1.0;
        if (IsKeyDown(KEY_S)) direction.y +=1.0;
        //Các lines này cho phép tính toán khoảng cách di chuyển sau mỗi lần ấn phím AWSD

        //Di chuyển tiled map dựa vào dữ liệu của direction. Vì đây là topdown RPG, chỉ có màn hình di chuyển, còn nhân vật luôn đứng giữa màn hình
        if (Vector2Length(direction)!=0.0)
        {//Cần kiểm tra direction phải khác 0, dựa theo header raymath.h
            // Bây giờ thì mapPos sẽ có dữ liệu mới
            mapPos = Vector2Subtract(mapPos,Vector2Normalize(direction));
            //Vector2Subtract tính Vector2 mới khi có sánh với dữ liệu thay đổi
        }

3.3 Sprite movement

Khai báo texture cho nhân vật, knight

    Texture2D knight=LoadTexture("characters/knight_idle_spritesheet.png");
    Vector2 knightPos=// để nhân vật ngay giữa màn hình
    {
        (float)windowWidth/2.0f - 4.5f*(0.5f* (float)knight.width/6.0f),// scale nhân vật 2.5 lần
        (float)windowHeight/2.0f - 4.5f*(0.5f* (float)knight.height)
    };

Trong vòng lặp while, sử dụng Draw để hiển thị sprite

        Rectangle source{0.f,0.f,(float)knight.width/6.f,(float)knight.height};// sprite là chuỗi hình từ các hình chữ nhật, vì vậy cần khai báo kích thước của hình chữ này này
        Rectangle destination{knightPos.x,knightPos.y,4.5f*(float)knight.width/6.f,4.5f*(float)knight.height};
        DrawTexturePro(knight,source,destination,Vector2{},0.f,WHITE);

Vì nhân vật được hiển thị dựa theo texture của knight, hình này là 1 hình chữ nhật dài với nhiều knight khác nhau (dùng để hiển thị animation). Vì vậy cần phải xác định vị trí của nhân vật trên texture đó. Cần phải khai bác Rectangle source, sẽ là vị trí và kích thước của HCN, còn destination là vị trí của knight trên màn hình.

Lưu ý nếu texture có kích thước nhỏ thì khi khai báo destination cần phải scale nhân vật lên cho phù hợp

4 Class

Sử dụng class để tạo các functions độc lập với main.cpp trong game.

Tạo một class Character như sau:

class Character
{
public:
    Vector2 getWorldPos() { return worldPos; } // không cần sử dụng dấu ; tại cuối hàng trong functions của class
    void setScreenPos(int winWidth, int winHeight);
    void tick(float deltaTime);

private:
    Texture2D texture{LoadTexture("characters/knight_idle_spritesheet.png")};
    Texture2D idle = LoadTexture("characters/knight_idle_spritesheet.png");
    Texture2D run = LoadTexture("characters/knight_run_spritesheet.png");
    Vector2 screenPos;
    Vector2 worldPos;
    // Face direction of character
    float rightLeft{1.f};
    // Animation variables, cần để xác định sprite nào đang được sử dụng trong 6 sprite sheets
    float runningTime{};
    int frame{};
    const int maxFrames{6};             // vì sprite chỉ có 6 hình
    const float updateTime{1.f / 12.f}; // 1/12s đủ để thấy animation, tức là mỗi giây có 12 animation
    const float speed{4.f};
};

// Định nghĩa setScreenPos từ class Character
void Character::setScreenPos(int winWidth, int winHeight)
{
    screenPos = {
        (float)winWidth / 2.0f - 4.5f * (0.5f * (float)texture.width / 6.0f), // scale nhân vật 2.5 lần
        (float)winHeight / 2.0f - 4.5f * (0.5f * (float)texture.height)};
}

void Character::tick(float deltaTime)
{
    Vector2 direction{};

    if (IsKeyDown(KEY_A))
        direction.x -= 1.0;
    if (IsKeyDown(KEY_D))
        direction.x += 1.0;
    if (IsKeyDown(KEY_W))
        direction.y -= 1.0;
    if (IsKeyDown(KEY_S))
        direction.y += 1.0;
    // Các lines này cho phép tính toán khoảng cách di chuyển sau mỗi lần ấn phím AWSD
    // Di chuyển tiled map dựa vào dữ liệu của direction. Vì đây là topdown RPG, chỉ có màn hình di chuyển, còn nhân vật luôn đứng giữa màn hình

    if (Vector2Length(direction) != 0.0)
    { // Cần kiểm tra direction phải khác 0, dựa theo header raymath.h
        // Bây giờ thì mapPos sẽ có dữ liệu mới
        worldPos = Vector2Add(worldPos, Vector2Scale(Vector2Normalize(direction), speed));
        // Vector2Subtract tính Vector2 mới khi có sánh với dữ liệu thay đổi
        // Vector2Normalize để chuyển từ value +-1 sang dạng vector
        // Vector2Scale để nhân giá trị của Normalize theo speed, vì normalize luôn ra 1.0
        // Tuy nhiên nhân vật chỉ có thể move chứ không có animation theo từng step

        direction.x < 0.f ? rightLeft = -1.f : rightLeft = 1.f;
        // Đọc direction dựa vào keyboard, nhỏ hơn 0 (giá trị là -1), tức là nhìn sang trái, rightLeft sẽ -1. Sử dụng dữ liệu này trong Draw chacracter source
        texture = run;
    }
    else
    {
        texture = idle;
    }

    // Update animation frames, sử dụng để xác định spite nào trong spite sheet
    runningTime += deltaTime;
    if (runningTime >= updateTime)
    {
        frame++;
        runningTime = 0.f;
        if (frame > maxFrames)
            frame = 0;
    }

    // Draw Character
    Rectangle source{frame * (float)texture.width / 6.f, 0.f, rightLeft * (float)texture.width / 6.f, (float)texture.height}; // sprite là chuỗi hình từ các hình chữ nhật, vì vậy cần khai báo kích thước của hình chữ này này
    Rectangle destination{screenPos.x, screenPos.y, 4.5f * (float)texture.width / 6.f, 4.5f * (float)texture.height};
    DrawTexturePro(texture, source, destination, Vector2{}, 0.f, WHITE);
}

Trong một file cpp cho phép vừa khai báo một function của class vừa định nghĩa nó ngay phía dưới.

4.1 Header file

Để tách functions của class và cách định nghĩa thì sử dụng header file .h.

Nếu tách ra thì file header chỉ còn

#include "raylib.h"
#include "raymath.h"

class Character
{
public:
    Vector2 getWorldPos() { return worldPos; } // không cần sử dụng dấu ; tại cuối hàng trong functions của class
    void setScreenPos(int winWidth, int winHeight);
    void tick(float deltaTime);

private:
    Texture2D texture{LoadTexture("characters/knight_idle_spritesheet.png")};
    Texture2D idle = LoadTexture("characters/knight_idle_spritesheet.png");
    Texture2D run = LoadTexture("characters/knight_run_spritesheet.png");
    Vector2 screenPos; // toạ độ của Character, nó luôn luôn tại giữa màn hình, và không bao giờ thay đổi
    Vector2 worldPos; // khi Character di chuyển thi worldPos sẽ update
    // Face direction of character
    float rightLeft{1.f};
    // Animation variables, cần để xác định sprite nào đang được sử dụng trong 6 sprite sheets
    float runningTime{};
    int frame{};
    const int maxFrames{6};             // vì sprite chỉ có 6 hình
    const float updateTime{1.f / 12.f}; // 1/12s đủ để thấy animation, tức là mỗi giây có 12 animation
    const float speed{4.f};
};

Tuy nhiên khi tách ra cần phải có constructor

4.2 Constructor

Trong header khai báo constructor

class Character
{
public:
    Character(); // constructor

Trong file cpp định nghĩa constructor các thuộc tính mà sẽ được sử dụng nhiều trong các functions của class

// Definition cho constructor Character()
Character::Character()// khai báo thuộc tính căn bản của charactor mà sẽ được dùng nhiều
{
    width = texture.width / maxFrames;
    height = texture.height;
}

Kết quả như sau

Hiện tại nhân vật có thể đi xuyên qua sông và vượt qua gốc của màn hình.

4.3 Map bound

Tạo function để worldPos không lớn hơn window size hoặc nhỏ hơn 0.
Chỉ cần tạo một function là undoMovement(); Trong movement function Tick() thì xác định worldPosLastFrame = worldPos;. Nếu khi pos ngoài màn hình thì sử dụng undoMovement () này.

        if (knight.getWorldPos().x <0.f ||
            knight.getWorldPos().y <0.f ||
            knight.getWorldPos().x +windowWidth > map.width*mapscale ||
            knight.getWorldPos().y +windowHeight> map.height*mapscale
            )
            {
                knight.undoMovement();
            }
            ```

### static_cast
Trong C++ cho phép thay đổi type nhờ `cast` function
```cpp
 screenPos = {// vì winWidth là dạng int, cần phải static_cast sang float, vì width thuộc dạng float: static_cast<float>(winWidth)
    static_cast<float>(winWidth) / 2.0f - scaleChr * 0.5f * width, 
    static_cast<float>(winHeight) / 2.0f - scaleChr * 0.5f * height};
    ```

Thay  trong `C` cần phải khai báo `(float)winHeight / 2.0f - scaleChr * 0.5f * height` thì dùng `cast` sẽ tốt hơn

Trong game tôi  thêm chức năng ấn Space sẽ speedx10 để test di chuyển.

![](https://i.imgur.com/lly9ylC.gif)

## Prop class - worldMap decoration
Tạo class Prop để thiết kế cho world map

Trong constructor thường  argument như sau
```cpp
Prop::Prop(Vector2 pos)
{
  worldPos = Pos;
}

Cách viết này sẽ tốn thời gian, và worldPos thường đã được khai báo trong private rồi. Vì vậy không cần phải khai báo lại nữa. Khai báo như sau sẽ hiệu quả hơn. Sử dụng dấu hai chấm :

Prop::Prop(Vector2 pos) : // sử dụng dấu :
  worldPos(Pos), // thay vì khai báo worldPos = Pos;
{

}

Cần chú ý là WorldMap sẽ di chuyển theo nhân vật. Còn nhân vật chỉ đứng giữa màn hình. Điều này dẫn đến nếu cho các assets vào WorldMap thì nó sẽ bị di chuyển theo nhân vật. Vì vậy phải fix nó vào WorldMap

4.4 Array of assets

Tạo một array để chứa nhiều assets cho worldmap

Prop props[2]{
        Prop{Vector2{600.f,600.f},LoadTexture("nature_tileset/Rock.png")},
        Prop{Vector2{300.f,300.f},LoadTexture("nature_tileset/Log.png")}
    };
    ```

Trong gameloop sử dụng `for` để Render assets

```cpp
// Draw assets cho world map
        for (auto prop:props)
        {
            prop.Render(knight.getWorldPos());
        };
        ```

### Collision player with assets
Chỉ cần tạo Rectangle  cho player  assets,  sử dụng CheckCollision  sẵn của Raylib.

Tạo một function để return Shape của assets
```cpp
Rectangle Prop::getCollisionRec(Vector2 knightPos)
{
    Vector2 screenPos{Vector2Subtract(worldPos,knightPos)};
    return Rectangle{
        screenPos.x,
        screenPos.y,
        texture.width*scale,
        texture.height*scale
    };
}

for (auto prop:props)
        {      if(CheckCollisionRecs(prop.getCollisionRec(knight.getWorldPos()),knight.GetCollisionRec()))knight.undoMovement(); 
        }

5 Enemy class

Tương tự như Player, thì enemy cũng có khả năng di chuyển, cũng có các toạ độ, cũng có các UndoMovement… Chỉ khác là nó không phụ thuộc vào input WASD

Chúng ta có thể copy class Character và chỉnh sửa 1 chút để có Enemy class.
Header file

#ifndef ENEMY_H
#define ENEMY_H

#pragma once

#include "raylib.h"
class Enemy
{
public:
    Enemy(Vector2 pos, Texture2D idle_texture, Texture2D run_texture); // constructor
    void tick(float deltaTime); // movement của character
    Vector2 getWorldPos(); // trả về vị trí của Map dựa vào toạ độ của nhân vật
    void undoMovement();
    Rectangle GetCollisionRec(); // return shape và pos của player

private:
    Texture2D texture{LoadTexture("characters/knight_idle_spritesheet.png")};
    Texture2D idle = LoadTexture("characters/knight_idle_spritesheet.png");
    Texture2D run = LoadTexture("characters/knight_run_spritesheet.png");
    Vector2 screenPos{}; // toạ độ của Character, nó luôn luôn tại giữa màn hình, và không bao giờ thay đổi
    Vector2 worldPos{}; // khi Character di chuyển thi worldPos sẽ update
    Vector2 worldPosLastFrame{};

    float rightLeft{1.f};// Face direction of character
    // Animation variables, cần để xác định sprite nào đang được sử dụng trong 6 sprite sheets
    float runningTime{};
    int frame{}; // vị trí của frame trong animation
    int maxFrames{6}; // vì sprite chỉ có 6 hình
    float updateTime{1.f / 12.f}; // 1/12s đủ để thấy animation, tức là mỗi giây có 12 animation
    float speed{4.f}; // speed của nhân vật
    float width{}; // kích thước nhân vật
    float height{};// kích thước nhân vật
    float scaleChr{4.5f};
};

#endif

File cpp

#include "Enemy.h"


Enemy::Enemy(Vector2 pos, Texture2D idle_texture, Texture2D run_texture):
    worldPos(pos), //initialize
    texture(idle_texture),
    idle(idle_texture),
    run(run_texture)
{
    width = texture.width / maxFrames;
    height = texture.height;
}

void Enemy::tick(float deltaTime)
{
    worldPosLastFrame = worldPos;
    // Update animation frames, sử dụng để xác định spite nào trong spite sheet
    runningTime += deltaTime;
    if (runningTime >= updateTime)
    {
        frame++;
        runningTime = 0.f;
        if (frame > maxFrames)
            frame = 0;
    }

    // Draw Character
    Rectangle source{frame * width, 0.f, rightLeft * width, height}; // sprite là chuỗi hình từ các hình chữ nhật, vì vậy cần khai báo kích thước của hình chữ này này
    Rectangle destination{screenPos.x, screenPos.y, scaleChr*width, scaleChr*height};
    DrawTexturePro(texture, source, destination, Vector2{}, 0.f, WHITE);
}

Vector2 Enemy::getWorldPos()
{
    return worldPos;
}

void Enemy::undoMovement()
{
    worldPos = worldPosLastFrame;
}

Rectangle Enemy::GetCollisionRec()
{
    return Rectangle{
        screenPos.x,
        screenPos.y,
        width*scaleChr,
        height*scaleChr
    };
}

Kết quả cho ra như sau

Vì khai báo Vector2 toạ độ của Enemy là mặc định

Enemy goblin(
        Vector2{},
        LoadTexture("characters/goblin_idle_spritesheet.png"),
        LoadTexture("characters/goblin_run_spritesheet.png")

Nên Enemy luôn đặt tại vị trí (0,0) của màn hình

5.1 Inheritance class - virtual Class public

Vì Character class và Enemey class có rất nhiều điểm giống nhau, nên tạo một parent class cho cả hai để chúng có thể làm theo và rút gọn code. Ngoài ra constructor của child class không cần phải có kết cấu giống như của parent class.

Mục đích chính của parent class là để các child class sử dụng chung với nhau protected variables được khai báo trong parent và một số functions public

Khi khai báo private thì child của một class không thể access, nhưng protected thì có thể.

Cần chú ý khi sử dụng parent class thì khi khai báo các child class không được khai báo thêm nữa, nếu không sẽ bị lỗi.

Ngoài ra sử dụng child class cũng không thể initialize cho constructor như các parent class được

5.2 Overide function

Mục đích để child class có một function riêng dành cho nó, nhưng vẫn dựa vào parent class, vì vậy cần phải tạo một virtual function overide

void Enemy::tick(float deltaTime)
{
    BaseCharacter::tick(deltaTime);
}

6 Pointer

Sử dụng pointer trong game rất quan trọng, vì các nhân vật luôn tục di chuyển. Pointer có khả năng biết được sự thay đổi trên memory. Trong khi sử dụng gán (copy) thì chỉ lưu được giá trị tại một thời điểm. Khi nhân vật di chuyển thì không thể có giá trị của vị trí mới được.

Vì Enemy chỉ được khai báo tại Vector{} tức là (0,0), vì vậy nó luôn ở góc màn hình. Trong khi đó character class thì có sử dụng Vector2Add để thay đổi worldPos, đây là toạ độ của background thay đổi theo input WASD. Vì vậy trong enemy class cần phải sử tính toán lại

void Enemy::tick(float deltaTime)
{
    screenPos = Vector2Subtract(worldPos,target->getWorldPos());//update toạ độ của nhân vật dựa theo worldPos (background map), tuy nhiên cần trừ WASD của character, nếu không thì enemy cứ di chuyển giống như character
    BaseCharacter::tick(deltaTime);
}

Lưu ý target là một adress của character, cần được khai báo trong class enemy

class Enemy : public BaseCharacter
{
public:
    Enemy(Vector2 pos, Texture2D idle_texture, Texture2D run_texture); // constructor
    virtual void tick(float deltaTime) override; // movement của character, override
    void setTarget(Character* pCharacter){target = pCharacter;} //Follow character based on Character Pointer
private:
    Character* target;

};

Kết quả như sau

6.1 AI for enemy

Cần xác định khoảng cách giữa Enemy và Character tại mỗi Frame, vì vậy Pointer cần được sử dụng. Sau đó chỉ cần cho Enemy chạy lại Character, tuỳ thuộc vào speed của enemy.

void Enemy::tick(float deltaTime)
{
    // Get toTarget, khoảng cách giữa enemy và character
    Vector2 toTarget= Vector2Subtract(target->getScreenPos(),screenPos);

    // normalize toTarget
    toTarget = Vector2Normalize(toTarget);

    // multiply by speed
    toTarget = Vector2Scale(toTarget,speed);

    // move the Enemy
    worldPos = Vector2Add(worldPos, toTarget);

    screenPos = Vector2Subtract(worldPos,target->getWorldPos());//update toạ độ của nhân vật dựa theo worldPos (background map), tuy nhiên cần trừ WASD của character, nếu không thì enemy cứ di chuyển giống như character
    BaseCharacter::tick(deltaTime);
}

6.2 Draw Sword

Trong private class của Character tạo một variable Texture2D là weapon và load nó luôn nếu nó không thay đổi
Texture2D weapon{LoadTexture("characters/weapon_sword_1.png")}

Trong file cpp của Class này thì dùng lệnh vẽ private variable này

Rectangle source{0.f,0.f,static_cast<float>(weapon.width)*rightLeft,-static_cast<float>(weapon.height)}; //position của sprite trong image
    Rectangle dest{getScreenPos().x + offset.x,getScreenPos().y+offset.y,weapon.width*scaleChr,weapon.height*scaleChr};//screen position to draw texture
    DrawTexturePro(weapon,source, dest,origin,0.f,WHITE);

The following pages link to this page:



Created : Feb 12, 2022