How to attach C++ Actors in Leadwerks 4
For some reason, I've been seeing a lot of questions on how to add actors created into C++ recently. My first attempt on this was in 2016 with Crawler's Den; A modern remake of the SDK example level. That was on a version of Leadwerks in which there was no official Actor system in place. Today, the engine has an Actor class which can be attached to any entity, but besides some loose examples, it's not well documented. Also there is no official way on linking your entities in the editor with your actors. But today I wish to share with you some insight on how I managed to get not only actors attaching in my maps, but also allowing interaction with the flowgraph system.
Disclaimer: This is more copy and paste code only tested with Leadwerks 4.
Base Actor And Actor Factory
First, you'll want to make a base class off the engine's Actor class that'll allow us to easily grab values from the lua file. Yes, we can have an Actor and a Script attached to the same entity at a time! The idea is that the lua script will be our definition file for our actor to load on map load.
#ifndef BASEACTOR_H #define BASEACTOR_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" #define ACTOR_KEYVALUE "classname" class BaseActor : public Actor { public: void SetBoolValue(const std::string& pValue, const bool pDefault = false); bool GetBoolValue(const std::string& pValue, const bool pDefault = false); void SetFloatValue(const std::string& pValue, const float pDefault = 0); float GetFloatValue(const std::string& pValue, const float pDefault = 0); void SetIntValue(const std::string& pValue, const int pDefault = 0); int GetIntValue(const std::string& pValue, const int pDefault = 0); void SetStringValue(const std::string& pValue, const std::string& pDefault = ""); std::string GetStringValue(const std::string& pValue, const std::string& pDefault = ""); Vec2* GetVec2Value(const std::string& pValue, Vec2* pDefault = 0); Vec3* GetVec3Value(const std::string& pValue, Vec3* pDefault = 0); Vec4* GetVec4Value(const std::string& pValue, Vec4* pDefault = 0); Entity* GetEntityValue(const std::string& pValue, Entity* pDefault = NULL); void FireOutput(const std::string& pEvent); }; #endif
#include "stdafx.h" #include "baseactor.h" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetBoolValue(const std::string& pValue, const bool pDefault) { #ifndef LEADWERKS_5 GetEntity()->SetBool(pValue, pDefault); #else GetEntity()->SetBoolean(pValue, pDefault); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool BaseActor::GetBoolValue(const std::string& pValue, const bool pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetBool(pValue) != pDefault) { return entity->GetBool(pValue); } #else if (GetEntity()->GetBoolean(pValue) != pDefault) { return GetEntity()->GetBoolean(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetFloatValue(const std::string& pValue, const float pDefault) { #ifndef LEADWERKS_5 GetEntity()->SetFloat(pValue, pDefault); #else GetEntity()->SetNumber(pValue, (float)pDefault); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float BaseActor::GetFloatValue(const std::string& pValue, const float pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetFloat(pValue) != NULL) { return entity->GetFloat(pValue); } #else if (GetEntity()->GetNumber(pValue) != NULL) { return (float)GetEntity()->GetNumber(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetIntValue(const std::string& pValue, const int pDefault) { GetEntity()->SetString(pValue, to_string(pDefault)); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int BaseActor::GetIntValue(const std::string& pValue, const int pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetFloat(pValue) != NULL) { return static_cast<int>(entity->GetFloat(pValue)); } #else if (GetEntity()->GetNumber(pValue) != NULL) { double x = GetEntity()->GetNumber(pValue); // stored as 54.999999... x = x + 0.5 - (x < 0); // x is now 55.499999... int y = (int)x; // truncated return y; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::SetStringValue(const std::string& pValue, const std::string& pDefault) { GetEntity()->SetString(pValue, pDefault); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- std::string BaseActor::GetStringValue(const std::string& pValue, const std::string& pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 if (entity->GetString(pValue) != "") { return entity->GetString(pValue); } #else if (GetEntity()->GetString(pValue) != "") { return GetEntity()->GetString(pValue); } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec2* BaseActor::GetVec2Value(const std::string& pValue, Vec2* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec2* test = static_cast<Vec2*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec2* test = (Vec2*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec3* BaseActor::GetVec3Value(const std::string& pValue, Vec3* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec3* test = static_cast<Vec3*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec3* test = (Vec3*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vec4* BaseActor::GetVec4Value(const std::string& pValue, Vec4* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Vec4* test = static_cast<Vec4*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Vec4* test = (Vec4*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Entity* BaseActor::GetEntityValue(const std::string& pValue, Entity* pDefault) { if (GetEntity() == nullptr) return pDefault; #ifndef LEADWERKS_5 Entity* test = static_cast<Entity*>(entity->GetObject(pValue)); if (test != NULL) { return test; } #else Entity* test = (Entity*)GetEntity()->GetObjectPointer(pValue); if (test != NULL) { return test; } #endif return pDefault; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void BaseActor::FireOutput(const std::string& pEvent) { if (GetEntity() == NULL) return; #ifndef LEADWERKS_5 if (entity->component != NULL) { entity->component->CallOutputs(pEvent); } #else GetEntity()->FireOutputs(pEvent); #endif }
Now we need a actor factory of some kind. There is probably an easier and modern way of doing this, but this is how I do it. First we create a header file called actorfactory.h
#ifndef ACTORFACTORY_H #define ACTORFACTORY_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" extern void BaseActorFactory(Entity* pEntity, Object* pObject); #endif // ACTORFACTORY_H
Then in actorfactory.cpp, I have the following.
#include "stdafx.h" #include "baseweapon.h" #include "clientcamera.h" #include "doors.h" #include "noise.h" #include "fpsplayer.h" #include "logicactors.h" #include "pointmessage.h" #include "propspawner.h" #include "point_transition.h" #include "platform.h" #include "triggers.h" #include "vrplayer.h" #define ATTACH_NAME_TO_ACTOR(_name_, _actor_) if (test == _name_) actor = new _actor_() void BaseActorFactory(Entity * pEntity, Object * pObject) { auto classname = GetEntityKeyValue(pEntity, ACTOR_KEYVALUE); if (classname != "") { BaseActor* actor = NULL; std::string entname = pEntity->GetKeyValue("name", "STATIC_MESH"); std::string test = String::Lower(classname); // Global actor is a dummy actor with values for other actors. if (test == "global") { if (g_mGlobalActor == NULL) { actor = new BaseActor(); g_mGlobalActor = actor; } } ATTACH_NAME_TO_ACTOR("ambient_generic", AmbientGeneric); ATTACH_NAME_TO_ACTOR("door_sliding", SlidingDoor); ATTACH_NAME_TO_ACTOR("door_rotating", RotatingDoor); ATTACH_NAME_TO_ACTOR("point_path", PointPath); ATTACH_NAME_TO_ACTOR("point_message", PointMessage); ATTACH_NAME_TO_ACTOR("point_transition", PointTransition); ATTACH_NAME_TO_ACTOR("point_weapon_pickup", PointWeaponPickup); ATTACH_NAME_TO_ACTOR("prop_spawner", PropSpawner); ATTACH_NAME_TO_ACTOR("logic_relay", LogicRelay); ATTACH_NAME_TO_ACTOR("logic_branch", LogicBranch); ATTACH_NAME_TO_ACTOR("logic_counter", LogicCounter); ATTACH_NAME_TO_ACTOR("train", TrainActor); ATTACH_NAME_TO_ACTOR("trigger_once", CollisionTrigger); ATTACH_NAME_TO_ACTOR("trigger_multiple", CollisionTriggerMultiple); ATTACH_NAME_TO_ACTOR("volume", BaseVolume); ATTACH_NAME_TO_ACTOR("volume_push", PushVolume); ATTACH_NAME_TO_ACTOR("volume_hurt", HurtVolume); ATTACH_NAME_TO_ACTOR("weapon_pickup", WeaponPickup); if (BaseActor::AttachActor(pEntity, actor, true)) { pEntity->SetKeyValue(ACTOR_KEYVALUE, classname); System::Print("Attached actor: \"" + classname + "\" to \"" + entname + "\"."); } else { System::Print("Error: failed to attach actor: \"" + classname + "\" to \"" + entname + "\"."); } } }
Each actor gets a "classname" assigned to it. This can be anything you want and it's defined here. I went with a quake naming scheme for personal preference.
We then call this function in our map load hook as so. This will pass each entity loaded though our factory when the map file is being loaded in.
if (Map::Load(pszPath, &BaseActorFactory) == false) { Msg("Failed to load \"" + pszPath + "\" as path is invaild/missing from disk!"); return false; }
Custom Actors
Now we have the platform to build our actor, lets do so! Here's sample code of a relay actor. I'm picking this class as it shows an input and output functions being used that'll work with the flowgraph as well as loading values.
#ifndef LOGICACTORS_H #define LOGICACTORS_H #if defined( _WIN32 ) #pragma once #endif #include "stdafx.h" #include "baseactor.h" class LogicRelay : public BaseActor { long m_intDelayTime; long m_intTriggerTime; public: virtual void Start(); virtual void ReceiveSignal(const std::string& inputname, Entity* sender); virtual void UpdateWorld(); }; #endif
#include "stdafx.h" #include "logicactors.h" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::Start() { BaseToggle::Start(); m_intDelayTime = GetIntValue("delay", 0); //FireOutput("OnStart"); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::ReceiveSignal(const std::string& inputname, Entity* sender) { auto _event = String::Lower(inputname); if (_event == "trigger") { m_intTriggerTime = Timing::GetCurrent(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void LogicRelay::UpdateWorld() { if (m_intTriggerTime > 0) { if (Timing::GetCurrent() > m_intTriggerTime + m_intDelayTime) { FireOutput("OnTrigger"); } } }
Last we write the lua script:
--[[ Purpose: A Logic entity that is used for relaying outputs. ]]-- Script.classname="logic_relay" Script.delay=-0--int "Delay" function Script:Trigger()--in end function Script:Outputs() self.component:CallOutputs("OnStart") self.component:CallOutputs("OnTrigger") end
Compile your C++ code and attach your script to your entity. If all is well, you should have a working actor in your game!
Remarks
This is the best and simple way of getting actors to work in your game much like the lua alternative. However, as a C++ user I can ensure you that your adventure isn't over. First you're going to have to figure out how actor members are deleted and when they should be deleted. The clear function in the world class will release all engine actors, but not assets so you need to take that into consideration. Also, I personally ran into issues with sound Source classes (Now called Speaker in Leadwerks 5) not being recreated when the map was reloaded. I ended up having to make my own source management system. To be honest, I forgot how it works since I haven't really touched my project in months.
Even after all my workarounds, I still have things not releasing properly sometimes, but if you're smarter than me, I'm sure you can figure it out.
But I wrote this blog so if anyone had questions on how to use actors, You or I can point them here. Happy coding!
- 6
0 Comments
Recommended Comments