Máy tính cho phép bạn thực hiện nhiều lỗi sai nhanh hơn so với bất kì sự phát minh trong lịch sử loài người. Bjarne Stroustrup
STDIO Hiện thực phương thức truyền và nhận thông số cho các component, hiện thực CPosition – component xử lý tọa độ của Entity. Đây là một phần trong hệ thống do tôi sáng tạo dựa trên tư tưởng của Component Base Programming (CBP). Toàn bộ hệ thống được trình bày trong chuỗi gồm 10 bài viết với tiền tố CBP.
Nội dung bài viết

Giới thiệu

Đây là một nội dung mới trong CBP, hỗ trợ chúng ta thao tác với các Component bằng cách truyền và lấy giá trị từ các Component của Entity.

Trong bài viết, tôi sẽ hiện thực CPosition - Component quản lý tọa độ của Entity để minh họa cho vấn đề này.

Đây là project ví dụ dùng trong bài viết: CBP-5_Example.

Project được khởi tạo trên nền Visual Studio 2013, sử dụng Fred Framework do tôi định nghĩa, độc giả không cần cài thêm DirectX hoặc OpenGL để sử dụng, đây là framework làm việc với win32.

Tiền đề bài viết

Đây là bài viết thứ 5 trong chuỗi bài viết của tôi về CBP thuộc STDIO :: www.stdio.vn.

Đối tượng hướng đến

Các lập trình viên:

  • Mong muốn tham khảo một phương pháp hiện thực của CBP.
  • Có kiến thức nền tảng tương đối tốt về C++.
  • Nắm được tư duy của CBP.
  • Đã nắm được cấu trúc project ở bài CBP-4.

Các lập trình viên khác có thể sử dụng làm tài liệu tham khảo.

Danh mục các bài viết CBP

Các bài viết liên quan

CPosition

Ý tưởng

Đây là một dạng Component khá đặc biệt với chức năng chính là lưu trữ tọa độ của đối tượng.

Giai đoạn phân tích có thể dẫn độc giả đến việc hiện thực “Component di chuyển có kèm thuộc tính tọa độ” thay vì “Component tọa độ”, đây là điều tôi cần nói rõ.

Đối với Component tọa độ (CPosition), ta chỉ có thuộc tính tọa độ và phương thức truy vấn hay thay đổi trực tiếp giá trị mà CPosition nắm giữ (get/ set).

Đối với Component di chuyển (CMove), ta chỉ có phương thức thay đổi tọa độ của Entity theo giá trị delta chênh lệch, gồm các bước cơ bản:

  1. Truy xuất tọa độ hiện tại của Entity.
  2. Thay đổi giá trị tọa độ truy xuất được theo delta.
  3. Thiết lập tọa độ mới cho Entity theo giá trị tính toán được.

Lưu ý:

CMove CPosition hoàn toàn không có liên hệ gì với nhau, một Entity hoàn toàn có thể sở hữu 1 trong 2 hoặc cả 2 Component trên. Trường hợp Entity tồn tại CMove nhưng không có CPosition thì xử lý của CMove xem như không ảnh hưởng đến Entity.

Giải pháp ban đầu

Để giải quyết vấn đề này, chúng ta có thể đơn giản là định nghĩa một bộ phương thức get/ set cho CPosition. Lúc này, giải pháp của chúng ta như sau:

// CPosition.h
// class CPosition
// public
Vector2 getProperty();
int     setProperty(Vector2 newPos);

Vector2 là một trong những kiểu dữ liệu do tôi tự định nghĩa (tất cả được gói trong FDataTypes.h, file chứa định nghĩa của những kiểu dữ liệu dạng struct, enum, union do tôi tự định nghĩa nhằm hỗ trợ các class).

Đánh giá giải pháp, chúng ta nhận thấy một số vấn đề nhãn tiền như sau:

  • Chỉ khả dụng cho CPosition, cần ép kiểu Component về CPosition nếu thao tác trên danh sách Component của Entity.
  • Chỉ dùng được cho 1 kiểu dữ liệu.

Giải quyết vấn đề và hoàn thiện

Bước 1: khai báo getProperty/ setProperty ảo ở CBase

// CBase.h
// class CBase
// public
virtual Vector2 getProperty();
virtual int     setProperty(Vector2 newPos);

Tận dụng tính đa hình của lập trình hướng đối tượng, bước này giải quyết được vấn đề “phương thức chỉ khả dụng cho CPosition và cần ép kiểu Component để sử dụng”. CBase::getPropertyCBase::setProperty có thể không có chức năng gì cả, đối với mỗi Component, bộ phương thức này chỉ thao tác với những thuộc tính nhất định, với những kiểu dữ liệu nhất định, cách giải quyết này mặc định “không làm gì cả” khi Developer gọi phương thức bằng đối tượng không phù hợp.

Bước 2: chuẩn hóa đối số đầu vào

// CBase.h
// class CBase
// public

// To get component Property.
// The data location will be auto released by destroy returning variable.
// @param propertyFlag: the flag define the property you wanna get, listed in CBasePropertyFlag.
virtual ComponentProperty getProperty(int propertyFlag);

// To set component Property.
// @param propertyData: a variable contain pointer to data location.
// @param propertyFlag: the flag define the property you wanna set, listed in CBasePropertyFlag.
virtual int setProperty(ComponentProperty propertyData, int propertyFlag) 
throw(ComponentProperty::InvalidPointerType);

Ở đây tôi dùng một struct trung gian tên ComponentProperty lưu trữ địa chỉ dẫn đến dữ liệu và một cờ đánh dấu thuộc tính cần lấy hoặc thay đổi.

ComponentProperty có khai báo như sau:

struct ComponentProperty
{
enum Type
  {
      PTYPE_VOID = 0,
      PTYPE_VECTOR2
  };

union
  {
      void*     ptrVoid;
      Vector2*  ptrVector2;
  };

bool autoCleanup;
Type typeFlag;

...
};

Về cơ bản, đây cũng là một hình thức ép kiểu dữ liệu, nhưng với cờ typeFlag đính kém, ta luôn biết được kiểu thật của biến.

Cờ thuộc tính của các Component được định nghĩa đơn giản theo kiểu enum, tương tự như Command. Ở đây tôi có thay đổi một chút để thuận tiện cho trường hợp Component có kế thừa từ Component khác - một kiểu enum cho phép mở rộng thêm thông qua "kế thừa".

class CPositionPropertyFlag : public CBase::CBasePropertyFlag
{
public:
   enum
   {
       PFLAG_POSITION = CBase::CBasePropertyFlag::NUM_OF_CBASE_PROPERTY_FLAG,
       NUM_OF_CPOSITION_PROPERTY_FLAG
   };
};

Vậy là khai báo của bộ phương thức get/ set đã tương đối sẵn sàng, tiếp theo ta sẽ override lại bộ phương thức này cho CPosition như sau:

// CPosition.h
virtual ComponentProperty getProperty(int propertyFlag);
virtual int setProperty(ComponentProperty propertyData, int propertyFlag) 
throw(ComponentProperty::InvalidPointerType);
// CPosition.cpp
ComponentProperty CPosition::getProperty(int propertyFlag)
{
   switch (propertyFlag)
   {
   case PropertyFlag::PFLAG_POSITION:
      return ComponentProperty(new Vector2(m_position), ComponentProperty::PTYPE_VECTOR2, true);

   default:
      return ComponentProperty(nullptr, ComponentProperty::PTYPE_VOID, false);
   }
}

int CPosition::setProperty(ComponentProperty propertyData, int propertyFlag) 
throw(ComponentProperty::InvalidPointerType)
{
    switch (propertyFlag)
    {
    case PropertyFlag::PFLAG_POSITION:
          if (propertyData.typeFlag == ComponentProperty::PTYPE_VECTOR2)
              m_position = *propertyData.ptrVector2;
          else
              throw ComponentProperty::InvalidPointerType();
          break;

    default:
          break;
    }
}

Bước 3: thêm bộ get/ set Property cho Entity.

// Entity.h
// To get property from the component owned by this entity.
// @param componentFlag: the flag define the component you gonna access.
// @param propertyFlag: the flag define the property you wanna get.
ComponentProperty getProperty(ID::COMPONENT componentFlag, int propertyFlag)
        throw(ID::Unexpected);

// To set the component property.
// @param componentFlag: the flag define the component you gonna access.
// @param propertyData: the variable contain pointer to data location.
// @param propertyFlag: the flag define the property you wanna set.
int setProperty(ID::COMPONENT componentFlag, ComponentProperty propertyData, int propertyFlag)
        throw(ID::Unexpected, ComponentProperty::InvalidPointerType);

Để thuận tiện cho việc truy xuất Component theo ID, tôi chuyển thuộc tính m_components của Entity thành kiểu dữ liệu map<ID::Component, CBase*>. Mặt khác, thay đổi này sẽ đảm bảo mỗi Entity chỉ chứa 1 component mỗi loại.

// Entity.h
private:
        map<ID::COMPONENT, CBase*> m_components;

Kèm với thay đổi này là một số thay đổi về định nghĩa phương thức trong Entity.cpp, độc giả có thể xem chi tiết hơn trong project ví dụ. Tiếp theo là định nghĩa của Entity get/ set:

// Entity.cpp
ComponentProperty Entity::getProperty(ID::COMPONENT componentFlag, int propertyFlag)
throw(ID::Unexpected)
{
   try
   {
       return m_components.at(componentFlag)->getProperty(propertyFlag);
   }
   catch (out_of_range e)
   {
       throw ID::Unexpected();
   }
}

int Entity::setProperty(ID::COMPONENT componentFlag, 
                        ComponentProperty propertyData, int propertyFlag)
throw(ID::Unexpected, ComponentProperty::InvalidPointerType)
{
    try
    {
        m_components.at(componentFlag)->setProperty(propertyData, propertyFlag);
        return FSUCCESS;
    }
    catch (out_of_range e)
    {
         throw ID::Unexpected();
    }
    catch (ComponentProperty::InvalidPointerType e)
    {
         throw e;
    }
return FFAILD;
}

Bỏ qua các xử lý về exception, bộ phương thức này chỉ hoạt động đơn thuần như một bộ tìm kiếm, gọi phương thức tương ứng của Component thích hợp.

Thêm CPosition và thử nghiệm

Bước thử nghiệm này, chúng ta sẽ thêm CPosition cho Entity mà chúng ta vừa thử nghiệm với CAnimation CBP-4, kết quả hướng tới là Entity sẽ thay đổi vị trí sau mỗi chu kỳ chớp tắt Stdio Logo.

Trong ví dụ, chúng ta có một số thay đổi như sau:

// main.cpp
void init(HINSTANCE instance, HWND handler)
{
    ...
    CPosition::addComponentTo(g_entity, 50, 50);
}
// Entity.cpp
int Entity::update(float delta)
{
...
if (s_timer >= 2.0f)
{
    ComponentProperty newPos(new Vector2(rand() % 400 + 50, rand() % 400 + 50), 
                             ComponentProperty::Type::PTYPE_VECTOR2, true);
    this->setProperty(ID::COMPONENT::POSITION, newPos, CPosition::PropertyFlag::PFLAG_POSITION);

    m_components.at(ID::COMPONENT::ANIMATION)->commandProcess(CAnimation::COMMAND::SHOW);
    s_timer = 0.0f;
}
...
}

void Entity::draw(HDC hdc)
{
    try
    {
         Vector2 drawPos = *this->getProperty(ID::COMPONENT::POSITION, 
                                            CPosition::PropertyFlag::PFLAG_POSITION).ptrVector2;
         ((CAnimation*)m_components.at(ID::COMPONENT::ANIMATION))->draw(hdc, drawPos);
    }
    catch (exception e)
    {
         OUTPUT("Entity::draw\nCannot draw\n" << std::string(e.what()) << endl);
    }
}

Vậy là quá trình chuẩn bị thử nghiệm đã hoàn thành, chúng ta đã có thể chạy thử.

Tổng kết

Đến thời điểm này, chúng ta đã hoàn chỉnh cách lấy và sửa thông số của Component. Bộ phương thức này rất quan trọng trong khi làm việc với hệ thống mà chúng ta đang định nghĩa. Nói nôm na, bộ phương thức này là 1 trong 2 công cụ liên lạc của Component với Entity và với thế giới game (công cụ còn lại là Command). Lại nhắc đến Command, bài viết sau sẽ hướng dẫn độc giả về một phần rất quan trọng, là bước cuối nhằm hoàn thiện hệ thống Command – Command Queue (hàng đợi chỉ lệnh).

Bạn cần hỗ trợ các dự án kết nối không dây?

Quí doanh nghiệp, cá nhân cần hỗ trợ, hợp tác các dự án IoT, kết nối không dây. Vui lòng liên hệ, hoặc gọi trực tiếp 0942.111912.

  • TỪ KHÓA
  • Arduino
  • ESP32
  • ESP8266
  • Wifi
  • Bluetooth
  • Zigbee
  • Raspberry Pi
THẢO LUẬN
ĐÓNG