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:
- Truy xuất tọa độ hiện tại của
Entity
. - Thay đổi giá trị tọa độ truy xuất được theo
delta
. - Thiết lập tọa độ mới cho
Entity
theo giá trị tính toán được.
Lưu ý
CMove
và CPosition
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ủaEntity
. - 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::getProperty
và CBase::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 CAnimation
ở CBP-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
- CBP-0: Giới Thiệu về Component Base Development
- CBP-1: Tổng Quan về Project Ví Dụ
- CBP-2: Khai Báo Component Cơ Sở và Entity Cơ Bản
- CBP-3: Khai Báo Component Cơ Bản, Tích Hợp Component vào Entity
- CBP-4: Giao Tiếp với Entity – Hệ Thống Chỉ Lệnh
- CBP-5: Truyền và Lấy Thông Số từ Component
- CBP-6: Hàng Đợi Chỉ Lệnh
- CBP-7: Cài Đặt Phản Ứng cho Entity
- CBP-8: Component Điều Khiển và AI – Component Ra Lệnh
- CBP-9: Bộ Khởi Tạo Entity – Factory và Hệ Thống ID
- CBP-10: Hệ Thống Quản Lý Tập Trung Các Component Đặc Thù