Search…

CBP-5: Truyền và Lấy Thông Số từ Component

18/09/20207 min read
Giao tiếp giữa Component và Entity thông qua Command trong mô hình CBP.

Giới thiệu

Đây là 1 nội dung mới trong CBP:

  • Hỗ trợ 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.
  • Hiện thực CPosition - Component quản lý tọa độ của Entity để minh họa cho vấn đề này.

Tải project mẫu

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

Project này có thể mở bằng Visual Studio 2019, project này chứa các file mã C++ của CBP, project có thể không tương thích với nhiều môi trường Visual Studio khác nhau nhưng có thể dành để tham khảo.

CPosition

Ý tưởng

Đây là 1 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 độ.

Đối với Component tọa độ CPosition 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 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 ý

CMoveCPosition hoàn toàn không có liên hệ gì với nhau, 1 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 có thể định nghĩa 1 bộ phương thức get/set cho CPosition. Lúc này, giải pháp như sau:

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

Vector2 là 1 trong những kiểu dữ liệu do 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 nhằm hỗ trợ các class.

Đánh giá giải pháp, nhận thấy 1 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, đố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 lập trình viên 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 dùng 1 struct trung gian tên ComponentProperty lưu trữ địa chỉ dẫn đến dữ liệu và 1 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à 1 hình thức ép kiểu dữ liệu, nhưng với cờ typeFlag đính kèm, 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 có thay đổi 1 chút để thuận tiện cho trường hợp Component có kế thừa từ Component khác - 1 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 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, 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à 1 số thay đổi về định nghĩa phương thức trong Entity.cpp, 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ư 1 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, thêm CPosition cho Entity vừa thử nghiệm với CAnimationCBP-4, kết quả hướng đến là Entity sẽ thay đổi vị trí sau mỗi chu kỳ chớp tắt STDIO logo.

Trong ví dụ có 1 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 và có thể chạy thử.

Tổng kết

Đến thời điểm này đã 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 đ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 về 1 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ài chung series

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024