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.
Trong khi full map thì
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
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
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 vì 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 có thêm chức năng ấn Space sẽ speedx10 để test di chuyển.

## Prop class - worldMap decoration
Tạo class Prop để thiết kế cho world map
Trong constructor thường có 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 :
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 và assets, và sử dụng CheckCollision có 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
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);
Backlinks¶
The following pages link to this page:
Created : Feb 12, 2022
Recent Posts
- 2024-11-02: BUỔI 10 - Phân tích thị trường
- 2024-11-02: BUỔI 11 - Phân tích thị trường
- 2024-11-02: BUỔI 12 - Phân tích sóng tăng
- 2024-11-02: BUỔI 13 - Phân tích hỏi đáp
- 2024-11-02: BUỔI 14 - Yếu tố kiểm soát
- 2024-11-02: BUỔI 15 - Hỏi đáp
- 2024-11-01: BUỔI 6 - Ôn lại và bổ sung
- 2024-11-01: BUỔI 7 - Chiến thuật Trend
- 2024-11-01: BUỔI 8 - Công thức điểm vào lệnh
- 2024-11-01: K2023 - BUỔI 9 - Quy trình vào lệnh