Jump to content
  • Advertisement

Powered actors, laser tripwires, and simulating simple electrical connections

Brain

1387 views

Hi all!

As I progress through adding yet more levels to my game, I have decided that next on my to-do list is the ability to turn on and off devices around the levels based upon the crates passing along the conveyor belts. The system should give visual feedback as to what is connected to what, without me needing to manually connect cable actors to things when creating level mechanics. I took inspiration from the way sensors work on conveyor belts when building settlements and manufacturing systems in Fallout 4;

FO4-Inspiration.jpg.c536fc369f9767add3867e9e6eb2eba7.jpg

So how would I go about making a similar system? Without further ado, let us begin!

Part 1: The Sensor

I first needed to create a laser sensor object that could detect crates passing through it. A quick google found a tutorial on making a 'laser trip mine', as seen in many FPS games, so I decided to use this as a starting point. The difference between this and a mine though is that once triggered a mine will instantly explode, so there is no need to visually display the beam being stopped by an object. In this case, this is essential as well as the ability to detect the type of object which has intersected it.

I started out by creating a simple red beam particle. This is extra bright, with lots of bloom for gameplay reasons. Although a dimmer, shimmering beam much like a red laser pen or barcode reader would be much more realistic, it is also much harder to see from 'gods eye view';

ParticleEmitter.thumb.jpg.f8fd235375cdb11d166f6b1e107497fe.jpg

We can then use this beam in an actor. I created an actor which could serve as a beam emitter, which for now is just a placeholder cube with an unrelated brightly coloured texture on it. The beam particle emits from its right hand edge in a straight horizontal line.

To determine how far this beam should go, we calculate its maximum length in the Begin Play blueprint event, and also every time the Tick event fires to compensate for anything in the path of the beam which stops it. The first of these images is the BeginPlay event, which does three things:

1) Find the actor that the switch powers on/off, identified by a tag
2) Call the function to line trace the beam to its endpoint
3) Create and set up a cable actor connecting this sensor to its powered object for visual polish

BeginPlay.thumb.jpg.86e297d408542c99ab429c397c82b435.jpg

The function Create Beam is simple, as it simply gets the start and end locations, spawns an emitter at the start location and sets the end point of the particle effect:CreateBeam.thumb.jpg.b3b753e04bc3bcec9d13a05f279aba0c.jpg

The Calculate Beam Length is a little more complex, and is called every tick (as we need to adjust the length based on what we find intersecting it using a line trace):

CalcBeamLength.thumb.jpg.48c3620d08f874e59d6100bc80d68a93.jpg

You'll note that this function does not just return an end position vector. As well as the ending position, it will return a reference to a Crate if one intersects the beam, or an invalid (null) reference if the beam does not intersect a crate. We use this later on to determine whether or not to toggle power to connected actors:

EventTick.thumb.jpg.9fef328ce26e2bcfc2a1279a330af6cd.jpg

The three options at the end of this event are implemented by the NativePowered class, which I will discuss in the next section...

Part 2: Powered Actors

To make this work, we need a way to represent which actors are powered by electrical connections. I decided to do this as a simple linked list of powered objects which share one common power source. In my game the actual source of the power is not visible, or relavent to the game mechanics, so i don't show a wall socket or similar, to avoid confusing the player. After all, they aren't building these power networks themselves!

All electricity originates from a switch, so in this case the sensor we implemented above. In the tick event, when a crate intersects the laser, this fires the Set Power State method of the class, which sets the power state of that object, calls the Power On or Power Off event for that object, and then propagates the power change to the next object in the linked list.

All objects which can receive power derive from one type of Actor, written as a C++ class:

NativePowered.h

/* Represents an actor which may receive power from a switch, and be daisy chained to other powered actors.
 * If a powered actor is daisy chained to another actor, power state is shared between all connected objects.
 */
UCLASS()
class FWF_API ANativePowered : public AActor
{
	GENERATED_BODY()

	/* Current power state of this actor, on or off */
	bool PowerState;

	/* Next actor in the chain, or NULL if this is the end of the chain */
	class ANativePowered* Next;

	/* Cable component encapsulated within actor, connecting this actor
	 * to the next actor, if there is a chain.
	 */
	class APowerCable* Cable;

public:	
	// Sets default values for this actor's properties
	ANativePowered(const FObjectInitializer& ObjectInitializer);

	// Set power state to on or off, also changes the power state
	// of any 'Next' actors connected to this one
	UFUNCTION(BlueprintCallable, Category = "Power|Power State")
	void SetPowerState(bool Power);

	// Get current power state of the actor
	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Power|Power State")
	bool GetPowerState();

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	// Called when the actor is destroyed
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
	
	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

	// Can be overridden to get notification events when the object receives power.
	// Use this to turn on animations, enable lights, etc.
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Power|Power Event")
	void PowerOn();
	
	// Can be overriden to get notification events when the object loses power.
	// You can use this for example to stop animations and disable lights.
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Power|Power Event")
	void PowerOff();

	// Called after components are initialised
	void PostInitializeComponents();

	// The main mesh of the powered actor. As objects that receive power are visible
	// in the level and have power cables going into and out of them, they must have a
	// visual presence so a mesh is the default base object.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Power|Mesh")
	UStaticMeshComponent* Mesh;

	// A tag value to find in the next connected actor in a chain. This is then used
	// in BeginPlay() to locate and link to the next connection. We use tags here because
	// there is no other way to represent this loose relation in the editor, which
	// only allows selection of actors that are immediate descendents of the level,
	// not actors embedded in child actor components, etc.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Power|Power Connection")
	FString NextActorInGrid;
};

NativePowered.cpp

/* Powered actors default to powered off */
ANativePowered::ANativePowered(const FObjectInitializer& ObjectInitializer) : PowerState(false), Next(nullptr), Cable(nullptr)
{
	PrimaryActorTick.bCanEverTick = true;

	/* Create static mesh component */
	Mesh = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("Mesh"));
	Mesh->AttachToComponent(GetRootComponent(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, false));
}

void ANativePowered::PostInitializeComponents()
{
	Super::PostInitializeComponents();
}

void ANativePowered::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	/* Don't leave spawned cables hanging around (literally hanging... lol) */
	if (Cable != nullptr) {
		Cable->Destroy();
	}
}

// Called when the game starts or when spawned
void ANativePowered::BeginPlay()
{
	Super::BeginPlay();

	/* Is this powered actor daisy chained to another powered actor? */
	if (NextActorInGrid != TEXT("")) {

		/* Find the actor with this tag. Note that if there are copies of the level actor hanging around,
		 * there may be multiple, so always pick the last.
		 */
		TArray<AActor*> TaggedActors;
		UGameplayStatics::GetAllActorsWithTag(GetWorld(), FName(*this->NextActorInGrid), TaggedActors);
		/* We also can't have the actual NextActorInGrid property be FName directly, because FName values
		 * in properties of a child actor are wiped to 'None' on object initialisation. Bummer.
		 */

		/* Did we find any? */
		if (TaggedActors.Max()) {

			/* Set linked list ptr */
			Next = Cast<ANativePowered>(TaggedActors.Last());

			/* Get the world position of the 'Plug' socket/bone in the source actor.
			 * Enforce a scale and rotation, regardless of the scale and rotation of the actor itself and its components.
			 * If we don't the cable actor flies off in some random direction based on the rotation and scale.
			 */
			FTransform nextlocation = Mesh->GetSocketTransform(FName("Plug"));
			nextlocation.SetScale3D(FVector(1, 1, 1));
			nextlocation.SetRotation(FQuat(FRotator(0, 0, 0)));
			FActorSpawnParameters params;
			params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

			/* NOTE: APowerCable is basically just like ACableActor. It contains nothing but a UCableComponent, and a couple of
			 * lines in the constructor to set its colour and thickness.
			 * I would just be using UCableActor, but due to a BUG, i cant include it because it won't let me spawn it across
			 * module boundaries due to a link error. Clearing intermediate files did not fix this bug, as the answerhub said it should.
			 * UGH!!!
			 */
			Cable = GetWorld()->SpawnActor<APowerCable>(APowerCable::StaticClass(), nextlocation, params);

			/* HACK HACK - To attach to an actor, we must first specify the actor reference in OtherActor, and then we MUST
			 * (NOTE: MUST!!!!) specify a local offset in the EndLocation, from that actor position in actor transform space.
			 * This is the last line here, that sets the relative offset to that of the 'Plug' socket/bone within the static mesh.
			 * Note that this behaviour is quite poorly documented.
			 */
			FComponentReference ref;
			ref.OtherActor = Next;
			Cable->CableComponent->AttachEndTo = ref;
			Cable->CableComponent->EndLocation = Mesh->GetSocketTransform(FName("Plug"), RTS_Actor).GetLocation(); /*FVector(0, 0, 30);*/
		}
	}
}

// Called every frame
void ANativePowered::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

}

void ANativePowered::SetPowerState(bool Power)
{
	this->PowerState = Power;
	/* Trigger overridable events for this actor */
	if (this->PowerState) {
		PowerOn();
	}
	else {
		PowerOff();
	}
	if (Next) {
		/* Cascade the change to other attached actors */
		Next->SetPowerState(Power);
	}
}

/* Simple getter for power state */
bool ANativePowered::GetPowerState()
{
	return PowerState;
}

/* Base class, empty implementation */
void ANativePowered::PowerOn_Implementation()
{
}

/* Base class, empty implementation */
void ANativePowered::PowerOff_Implementation()
{
}

As you can see here, we determine the next 'link' in our chain of powered objects by using the tags of the actor. We must use tags, rather than just an object reference or pointer, because which objects we can pick using the editor in this way is highly restricted, we can only choose pointers to objects that are direct instantiated objects at the root of the level itself, not for example a spawned actor that is spawned at runtime, or a child actor within a child actor component.

To get around this restriction we set up our actors in the editor to work with the tags and the code written above:

Source Object

PowerConnectionSource.thumb.jpg.b0742eb2500ebf0e3368bf488c6dd444.jpg

Destination Object

PowerConnectionDestination.thumb.jpg.2a55d53396f8e92c2f81f5ff4444d51f.jpg

We can repeat this process as many times as we like, within the editor or even at runtime, to create complex chains of powered objects triggered by one switch.

Adding Sockets

All that's needed now is to add a socket to a mesh. In this instance i've picked a random large mesh from my collection of props. We add a socket to it called 'Plug', this is where the cables that link each item visually connect them:

PlugSocket.thumb.jpg.95751c4678a89825e58282f033e38495.jpg

Part 3: The complete system

Now we can test and view the complete system as a whole. As you can see, the system visually connects the related objects, as well as having the logical association behind the scenes. Passing a crate through the laser will toggle the connected objects. This will be used in level 12 to power some quite awesome hardware, namely a large hydraulic press that will destroy crates unless you toggle some switches in the right order to turn it off!

The system below is a simple test system, featuring two powered objects and a sensor:

FunctioningSystem.thumb.jpg.b42e41321a4eac3d9b535a402759139e.jpg##

As always, comments and feedback are more than welcome! Please vent your spleens below! 🙂




0 Comments


Recommended Comments

Glad you were able to come up with a solution for this, even if you had to do a hack job. ;) Nice job!

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • Blog Entries

  • Similar Content

    • By bandages
      So, in real life, incoming dot normal at the silhouette is always 0.  With smooth shaded meshes, it never is, not naturally, not outside of contrived situations.  (Not with flat shaded meshes either, I guess.)
      And incoming dot normal is one of the bedrocks of CG.  Probably the equal of 4x4 matrix multiplication.  Problems with silhouette normals show up in Fresnel, in diffuse lighting, in environment mapping....  everywhere.  But I can't really find anybody talking about it.  (Maybe I'm not Googling the right terms.)
      Obviously, the problem decreases as poly count goes up, eventually reaching a point where it's dwarfed by other silhouette problems (like translucency or micro-occlusion) that CG doesn't handle well either.  But, if I'm reasoning correctly, normal maps don't improve the problem-- they're as likely to exacerbate it as improve it, and the exacerbations are, aesthetically speaking, probably worse than the improvements are better.
      I've tried playing with crude fixes-- basically, rotating normals toward incoming by a percentage, or of course clamping incoming dot normal (like we all have to do) to prevent it from bending behind the mesh.  Nothing I've tried looks good.  I suppose the best option might be to rotate normals to perpendicular to incoming at the silhouette and then interpolate to the nearest inflection point  of something like screen space depth to preserve curvature, but the math for how to do that is beyond me, and I'm not sure it would look any better.  Or maybe, instead, somehow, adjust the drawn silhouette to match the silhouette defined by incoming dot normal?  Not even sure how that would work, not if the normal was pointing away from incoming.
      I don't know-- is this a solvable problem?  Has anyone tried other stuff and given up, pursued anything that was promising but too expensive, anything like that?  Are there any papers I'm missing?  It's really surprising to me that I can't find anyone else talking about this.
      (Apologies if I chose the wrong subforum for this.  I considered art forums, but I felt that people frequenting the programming forums would have more to say on the subject.)
    • By vinibiavatti
      Hi there! I have one issue for now. I'm creating a RayCasting application, and for my floor and ceiling I'm trying to use Mode7 for rendering (I think this is easier to understand). but, I cant align the RayCasting walls with the mode7 floor. I use a rotate matrix to make the rotation of floor. Do you know what a need to think in the implementation to fix that? Or do you know if there is some tutorial explaining about it? Thanks!!! (Check the image below for understand)

      Here is my mode7 code:
      function mode7() { let _x = 0; let _y = 0; let z = 0; let sin = Math.sin(degreeToRadians(data.player.angle)); let cos = Math.cos(degreeToRadians(data.player.angle)); for(let y = data.projection.halfHeight; y < data.projection.height; y++) { for(let x = 0; x < data.projection.width; x++) { _x = ((data.projection.width - x) * cos) - (x * sin); _y = ((data.projection.width - x) * sin) + (x * cos); _x /= z; _y /= z; if(_y < 0) _y *= -1; if(_x < 0) _x *= -1; _y *= 8.0; _x *= 8.0; _y %= data.floorTextures[0].height; _x %= data.floorTextures[0].width; screenContext.fillStyle = data.floorTextures[0].data[Math.floor(_x) + Math.floor(_y) * data.floorTextures[0].width]; screenContext.fillRect(x, y, 1, 1); } z += 1; } }  
    • By DiligentDev
      The latest release of Diligent Engine combines a number of recent updates (Vulkan on iOS, GLTF2.0 support, shadows), significantly improves performance of OpenGL backend, updates API, adds integration with Dear Imgui and implements new samples and tutorials. Some of the new features in this release:
      GLTF2.0 support (loader, PBR renderer and sample viewer) Shadowing Component and Shadows Sample Integration with Dear Imgui library and Dear Imgui demo Tutorial13 - Shadow Map Tutorial14 - Compute Shader Tutorial15 - Multiple Windows Check it out on GitHub.
        
       
       
    • By neiji93
      how is the BSDF function used in the kajiya rendering équations ? We know that path tracing proivde an analytical solution and we saw the BSDF function at first time in the path tracing algorithm. After that, is there a way to use mutliple BSDF function in a full rendering process ? If you have some links to any books or website, please share it !
    • By phil67rpg
      I have a very simple question, I am trying to rotate some vertex's around an arbitrary axis. basically I want to use glRotatef and glTranslatef to rotate a space ship I have drawn on the screen. here is my code of my  ship. what it does do is rotate around the origin when I  use the arrow keys left and right.
      void drawShip() { glPushMatrix(); glColor3f(255.0f, 0.0f, 0.0f); glTranslatef(-50.0f, 0.0f, 0.0f); glRotatef(rotateship, 0.0f, 0.0f, 1.0f); glBegin(GL_LINE_LOOP); glVertex3f(50.0f, 0.0f, 0.0f); glVertex3f(45.0f, -5.0f, 0.0f); glVertex3f(50.0f, 10.0f, 0.0f); glVertex3f(55.0f, -5.0f, 0.0f); glEnd(); glTranslatef(50.0f, 0.0f, 0.0f); glPopMatrix(); }  
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!