Jump to content
  • entries
    51
  • comments
    106
  • views
    28,453

How to attach C++ Actors in Leadwerks 4


reepblue

2,726 views

 Share

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! 

  • Like 6
 Share

0 Comments


Recommended Comments

Thank you very much.

I've been fighting for over two days now with Actors. I couldn't wrap my head around the concept of how to use them and how to implement them in-game.
I've been following you since your blue portal mod and portal notepad videos, and I'm happy to say I'm not disappointed about following you because you are a true genius. I can't tell you how much you have helped me save hours for my projects.

Thank you❤️

  • Like 2
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...