Jump to content
  • Advertisement
Sign in to follow this  
  • entries
    6
  • comments
    3
  • views
    1419

Designing Player World Interaction in Unreal Engine 4

Martin H Hollstein

831 views

Originally posted on Troll Purse development blog.

Unreal Engine 4 is an awesome game engine and the Editor is just as good. There are a lot of built in tools for a game (especially shooters) and some excellent tutorials out there for it. So, here is one more. Today the topic to discuss is different methods to program player world interaction in Unreal Engine 4 in C++. While the context is specific to UE4, it can also easily translate to any game with a similar architecture.

UE-Logo-988x988-1dee3bc7f6714edf3c21ee71

Interaction via Overlaps

By and far, the most common tutorials for player-world interaction is to use Trigger Volumes or Trigger Actors. This makes sense, it is a decoupled way to set up interaction and leverages most of the work using classes already provided by the engine. Here is a simple example where the overlap code is used to interact with the player:

Header

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteractiveActor.generated.h"

UCLASS()
class GAME_API InteractiveActor : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	InteractiveActor();

    virtual void BeginPlay() override;

protected:
	UFUNCTION()
	virtual void OnInteractionTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	virtual void OnInteractionTriggerEndOverlap(UPrimitiveComponent* OverlappedComp, class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

    UFUNCTION()
    virtual void OnPlayerInputActionReceived();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Interaction)
	class UBoxComponent* InteractionTrigger;
}

This is a small header file for a simple base Actor class that can handle overlap events and a single input action. From here, one can start building up the various entities within a game that will respond to player input. For this to work, the player pawn or character will have to overlap with the InteractionTrigger component. This will then put the InteractiveActor into the input stack for that specific player. The player will then trigger the input action (via a keyboard key press for example), and then the code in OnPlayerInputActionReceived will execute. Here is a layout of the executing code.

Source

// Fill out your copyright notice in the Description page of Project Settings.

#include "InteractiveActor.h"
#include "Components/BoxComponent.h"

// Sets default values
AInteractiveActor::AInteractiveActor()
{
	PrimaryActorTick.bCanEverTick = true;

	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	RootComponent->SetMobility(EComponentMobility::Static);

	InteractionTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("Interaction Trigger"));
	InteractionTrigger->InitBoxExtent(FVector(128, 128, 128));
	InteractionTrigger->SetMobility(EComponentMobility::Static);
	InteractionTrigger->OnComponentBeginOverlap.AddUniqueDynamic(this, &ABTPEquipment::OnInteractionProxyBeginOverlap);
	InteractionTrigger->OnComponentEndOverlap.AddUniqueDynamic(this, &ABTPEquipment::OnInteractionProxyEndOverlap);

	InteractionTrigger->SetupAttachment(RootComponent);
}

void AInteractiveActor::BeginPlay()
{
    if(InputComponent == nullptr)
    {
        InputComponent = ConstructObject<UInputComponent>(UInputComponent::StaticClass(), this, "Input Component");
        InputComponent->bBlockInput = bBlockInput;
    }

    InputComponent->BindAction("Interact", EInputEvent::IE_Pressed, this, &AInteractiveActor::OnPlayerInputActionReceived);
}

void AInteractiveActor::OnPlayerInputActionReceived()
{
    //this is where logic for the actor when it receives input will be execute. You could add something as simple as a log message to test it out.
}

void AInteractiveActor::OnInteractionProxyBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AController* Controller = OtherActor->GetController();
        if(Controller)
        {
            APlayerController* PC = Cast<APlayerController>(Controller);
            if(PC)
            {
                EnableInput(PC);
            }
        }
	}
}

void AInteractiveActor::OnInteractionProxyEndOverlap(UPrimitiveComponent* OverlappedComp, class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AController* Controller = OtherActor->GetController();
        if(Controller)
        {
            APlayerController* PC = Cast<APlayerController>(Controller);
            if(PC)
            {
                DisableInput(PC);
            }
        }
	}
}

Pros and Cons

The positives of the collision volume approach is the ease at which the code is implemented and the strong decoupling from the rest of the game logic. The negatives to this approach is that interaction becomes broad when considering the game space as well as the introduction to a new interactive volume for each interactive within the scene.

Interaction via Raytrace

Another popular method is to use the look at viewpoint of the player to ray trace for any interactive world items for the player to interact with. This method usually relies on inheritance for handling player interaction within the interactive object class. This method eliminates the need for another collision volume for item usage and allows for more precise interaction targeting.

Source

AInteractiveActor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteractiveActor.generated.h"

UCLASS()
class GAME_API AInteractiveActor : public AActor
{
	GENERATED_BODY()

public:
    virtual OnReceiveInteraction(class APlayerController* PC);
}

AMyPlayerController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "AMyPlayerController.generated.h"

UCLASS()
class GAME_API AMyPlayerController : public APlayerController
{
	GENERATED_BODY()

    AMyPlayerController();

public:
    virtual void SetupInputComponent() override;

    float MaxRayTraceDistance;

private:
    AInteractiveActor* GetInteractiveByCast();

    void OnCastInput();
}

These header files define the functions minimally needed to setup raycast interaction. Also note that there are two files here as two classes would need modification to support input. This is more work that the first method shown that uses trigger volumes. However, all input binding is now constrained to the single ACharacter class or - if you designed it differently - the APlayerController class. Here, the latter was used.

The logic flow is straight forward. A player can point the center of the screen towards an object (Ideally a HUD crosshair aids in the coordination) and press the desired input button bound to Interact. From here, the function OnCastInput() is executed. It will invoke GetInteractiveByCast() returning either the first camera ray cast collision or nullptr if there are no collisions. Finally, the AInteractiveActor::OnReceiveInteraction(APlayerController*)  function is invoked. That final function is where inherited classes will implement interaction specific code.

The simple execution of the code is as follows in the class definitions.

AInteractiveActor.cpp

void AInteractiveActor::OnReceiveInteraction(APlayerController* PC)
{
    //nothing in the base class (unless there is logic ALL interactive actors will execute, such as cosmetics (i.e. sounds, particle effects, etc.))
}

AMyPlayerController.cpp

AMyPlayerController::AMyPlayerController()
{
    MaxRayTraceDistance = 1000.0f;
}

AMyPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();
    InputComponent->BindAction("Interact", EInputEvent::IE_Pressed, this, &AInteractiveActor::OnCastInput);
}

void AMyPlayerController::OnCastInput()
{
    AInteractiveActor* Interactive = GetInteractiveByCast();
    if(Interactive != nullptr)
    {
        Interactive->OnReceiveInteraction(this);
    }
    else
    {
        return;
    }
}

AInteractiveActor* AMyPlayerController::GetInteractiveByCast()
{
    FVector CameraLocation;
	FRotator CameraRotation;

	GetPlayerViewPoint(CameraLocation, CameraRotation);
	FVector TraceEnd = CameraLocation + (CameraRotation.Vector() * MaxRayTraceDistance);

	FCollisionQueryParams TraceParams(TEXT("RayTrace"), true, GetPawn());
	TraceParams.bTraceAsyncScene = true;

	FHitResult Hit(ForceInit);
	GetWorld()->LineTraceSingleByChannel(Hit, CameraLocation, TraceEnd, ECC_Visibility, TraceParams);

    AActor* HitActor = Hit.GetActor();
    if(HitActor != nullptr)
    {
        return Cast<AInteractiveActor>(HitActor);
    }
	else
    {
        return nullptr;
    }
}

Pros and Cons

One pro for this method is the control of input stays in the player controller and implementation of input actions is still owned by the Actor that receives the input. Some cons are that the interaction can be fired as many times as a player clicks and does not repeatedly detect interactive state without a refactor using a Tick function override.

Conclusion

There are many methods to player-world interaction within a game world. In regards to creating Actors within Unreal Engine 4 that allow for player interaction, two of these potential methods are collision volume overlaps and ray tracing from the player controller. There are several other methods discussed out there that could also be used. Hopefully, the two implementations presented help you decide on how to go about player-world interaction within your game. Cheers!

 

 

Originally posted on Troll Purse development blog.



0 Comments


Recommended Comments

There are no comments to display.

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 hmcdev
      I want to learn to code games in C++ but I have no idea where to start, can someone teach me?
    • By phil67rpg
      I am made a lot of progress in my game but I have hit a small snag. in my code I am using an AABB collision detection. I have also used exit(0) to debug my code.it tells  me where in my code where it is getting access to. I have put exit(0) in the AABB collision detection and it accesses it just fine. but when I put only drawcollision_one function in the AABB collision routine it does not draw my collision sprite  animation. when I put drawcollision_one in the display function it works just fine. when the planes collide nothing happens but the exit(0) is accessed when the planes collide. here is the code I am working on.
      void drawcollision_one() { glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture[3]); glBegin(GL_POLYGON); glTexCoord3f(0.0f + screen, 0.0f, 0.0f); glVertex3f(0.5f, -0.5f, 0.0f); glTexCoord3f(0.167f + screen, 0.0f, 0.0f); glVertex3f(0.5f, 0.5f, 0.0f); glTexCoord3f(0.167f + screen, 1.0f, 0.0f); glVertex3f(-0.5f, 0.5f, 0.0f); glTexCoord3f(0.0f + screen, 1.0f, 0.0f); glVertex3f(-0.5f, -0.5f, 0.0f); glEnd(); glDisable(GL_TEXTURE_2D); } void timer(int val) { screen += 0.1667f; if (screen >= 1.0f) { screen = 1.0f; } glutPostRedisplay(); glutTimerFunc(500, timer, 0); } void coll_plane_one() { //draw bullet float x = -5.0f + horizontal; float y = 0.0f + vertical; float oWidth = 1.0f; float oHeight = 1.0f; //draw plane float xTwo = 5.0f + horizontal_one; float yTwo = 0.0f + vertical_one; float oTwoWidth = 1.0f; float oTwoHeight = 1.0f; if (checkCollide(x, y, oWidth, oHeight, xTwo, yTwo, oTwoWidth, oTwoHeight) == 1) { drawcollision_one(); } }  
    • By sidbhati32
      Hey,
      So I have got this asteroid type game and today I encountered a new issue while testing this game.
      What happened was that two asteroids were close to each other and I shot a bullet at them. The asteroids were so close to each other that a single bullet could collide to both of them.
      It collided and my game crashed there itself. I figured out it happened because two asteroids and one bullet collided in the same frame.
      This is the code -
      ```void Collision::DoCollisions(Game *game) const
      {
          for (ColliderList::const_iterator colliderAIt = colliders_.begin(), end = colliders_.end();
              colliderAIt != end;
              ++colliderAIt)
          {
              ColliderList::const_iterator colliderBIt = colliderAIt;
              for (++colliderBIt; colliderBIt != end; ++colliderBIt)
              {
                  Collider *colliderA = *colliderAIt;
                  Collider *colliderB = *colliderBIt;
                  if (CollisionTest(colliderA, colliderB))
                  {
                      game->DoCollision(colliderA->entity, colliderB->entity);
                  }
              }
          }
      }```
       
      ```
      void Game::DoCollision(GameEntity *a, GameEntity *b)
      {
          Ship *player = static_cast<Ship *>(a == player_ ? a : (b == player_ ? b : 0));
          Bullet *bullet = static_cast<Bullet *>(IsBullet(a) ? a : (IsBullet(b) ? b : 0));
          Asteroid *asteroid = static_cast<Asteroid *>(IsAsteroid(a) ? a : (IsAsteroid(b) ? b : 0));
          Bullet *bulletMode = static_cast<Bullet *>(IsBulletMode(a) ? a : (IsBulletMode(b) ? b : 0));
          if (player && asteroid)
          {
              player->playerCollided = true;
              //AsteroidHit(asteroid);
              //DeletePlayer();
          }
          if (bullet && asteroid)
          {
              collidedBullets.push_back(bullet);
              collidedAsteroid.push_back(asteroid);
              //AsteroidHit(asteroid);
              //DeleteBullet();
          }
          if(bulletMode && asteroid)
          {
              collidedBulletMode.push_back(bulletMode);
              collidedAsteroid.push_back(asteroid);
          }
      }```
       
      ```
      void Game::CollisionResponse()
      {
          if(player_->playerCollided == true)
          {
              DeletePlayer();
          }
          else
          {
          if(!collidedAsteroid.empty())
          {
              for(AsteroidList::const_iterator collidedAsteroidIt = collidedAsteroid.begin(), end = collidedAsteroid.end(); collidedAsteroidIt != end ; ++collidedAsteroidIt )
              {
                  AsteroidHit(*collidedAsteroidIt);
              }
              collidedAsteroid.clear();
          }
          
          if(!collidedBullets.empty())
          {
          for (BulletList::const_iterator bulletIt = collidedBullets.begin(), end = collidedBullets.end() ; bulletIt!=end; ++bulletIt)
          {
              DeleteBullet(*bulletIt);
          }
          
              collidedBullets.clear();
          }
          if(!collidedBulletMode.empty())
          {
              for (BulletList::const_iterator bulletIt = collidedBulletMode.begin(), end = collidedBulletMode.end() ; bulletIt!=end; ++bulletIt)
              {
                  DeleteBulletMode(*bulletIt);
              }
              collidedBulletMode.clear();
          }
      }
          }```
       
       
      in my game->docollision() -
      whenever an asteroid and a bullet used to collide, the collided objects get collected in collidedasteroids and collidedbullets respectively. When two asteroids collided with the same bullet, the two asteroids got collected safely in collidedAsteroid but the single bullet got collected in collidedBullets twice, so when the deletion was happening, the second time iteration of the bullet couldn't find the respective bullet and it got crashed.
       
      How am I supposed to approach this problem now?
       
      Thanks
    • By FredHicks
      I am finishing up my Game Programming and Design BS and am excited to start working in the field.  My biggest concern right now is that I live in a city with almost zero game studios and will be here for a while longer as me and my family have just bought our first house a couple years ago.  We do plan on moving in the future just for the purpose of change and experiencing something new and my girlfriend wants to move somewhere I can at least have opportunities to work in, but for now we are staying here while she finishes up her masters program.  What I am curious about is how to go about trying to get into the industry as a remote developer.  I am interested in any kind of work in the field to start off as I have been working on many different areas of game development since I was a kid in the early 90's but have finally decided to go for it in a serious way.  I guess I'm just looking for some advice and harsh reality checks about breaking into this highly competitive field as a remote developer.  I am new to the forums as well, so I hope I didn't break any rules I missed with this post.
      Thanks
    • By sidbhati32
      How to calculate angle between two points from a third point with the help of D3DXMATH library?
×

Important Information

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

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!