14 KiB
Part 2 (C++): Create the Player
← Previous: Part 1 (C++) - Project Setup | Back to Index | Next: Part 3 (C++) - Create the Bullet →
Overview
In this part, we'll create the player ship as a C++ class called ASTGPawn.
Time comparison:
- Blueprint approach (Part 2): ~60 minutes (12 variables via UI + Blueprint nodes)
- C++ approach (this part): ~15 minutes (copy-paste code)
Step 2.1: Create STGPawn C++ Class
In Unreal Editor:
-
Go to Tools → New C++ Class (top menu bar)
-
In the "Choose Parent Class" window:
- Click "Pawn" (NOT Character - we want simple 2D control)
- Click "Next"
-
In the "Name Your New Actor" window:
- Name:
STGPawn - Path: Should be
Source/BulletHellGame/(default) - Click "Create Class"
- Name:
What Happens Next:
- Unreal generates
STGPawn.handSTGPawn.cpp - Your IDE opens with the new files
- Project compiles automatically (~30-60 seconds)
- Unreal Editor refreshes
💡 TIP: If compilation fails, check the Output Log in Unreal Editor
Step 2.2: Define Player Variables in STGPawn.h
Now comes the magic! Instead of clicking UI 60+ times, we'll copy-paste all variables at once.
Open STGPawn.h in your IDE
Find the class definition (should look like this):
UCLASS()
class BULLETHELLGAME_API ASTGPawn : public APawn
{
GENERATED_BODY()
public:
ASTGPawn();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
Replace the entire class with this:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "STGPawn.generated.h"
// Forward declarations
class UCameraComponent;
class USpringArmComponent;
class UStaticMeshComponent;
class UBoxComponent;
UCLASS()
class BULLETHELLGAME_API ASTGPawn : public APawn
{
GENERATED_BODY()
public:
ASTGPawn();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// ===== COMPONENTS =====
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* ShipMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USpringArmComponent* SpringArm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCameraComponent* Camera;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UBoxComponent* Hitbox;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* HitboxIndicator;
// ===== MOVEMENT & BOUNDARIES =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MoveSpeed = 750.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
FVector2D BoundsMin = FVector2D(-850.0f, -450.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
FVector2D BoundsMax = FVector2D(850.0f, 450.0f);
// ===== FIRING =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float FireInterval = 0.08f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float BulletSpeed = 2200.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
int32 VolleySize = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float VolleySpread = 12.0f;
// ===== LIVES & SCORE =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
int32 MaxLives = 3;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
int32 CurrentLives = 3;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
int32 Score = 0;
// ===== SPECIAL ABILITY =====
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Stats")
bool bSpecialUsed = false;
// ===== INPUT FUNCTIONS =====
void MoveForward(float Value);
void MoveRight(float Value);
void StartFire();
void StopFire();
void UseSpecial();
// ===== GAME LOGIC =====
void FireShot();
void TakeHit(int32 Damage);
void HandleDeath();
void AddScore(int32 Points);
private:
FTimerHandle TimerHandle_Fire;
bool bIsFiring = false;
FVector MovementInput;
float FireTimer = 0.0f;
};
What Just Happened?
You just defined 12 gameplay variables + 5 components with default values in ~30 seconds (copy-paste time)!
In the Blueprint tutorial, this would require:
- 12 variables × 5 clicks each = 60 clicks
- ~15 minutes of manual work
- High chance of typos
Time saved: 14.5 minutes ⚡
Step 2.3: Implement STGPawn Constructor
Open STGPawn.cpp in your IDE
Replace the file content with:
#include "STGPawn.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
ASTGPawn::ASTGPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Root component
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// Ship mesh - cone shape pointing upward
ShipMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ShipMesh"));
ShipMesh->SetupAttachment(RootComponent);
ShipMesh->SetCollisionProfileName("NoCollision");
// Load cone mesh from engine
static ConstructorHelpers::FObjectFinder<UStaticMesh> ConeMesh(TEXT("/Engine/BasicShapes/Cone"));
if (ConeMesh.Succeeded())
{
ShipMesh->SetStaticMesh(ConeMesh.Object);
ShipMesh->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.7f));
ShipMesh->SetRelativeRotation(FRotator(90.f, 0.f, 0.f)); // Point forward
}
// Hitbox - small for bullet-hell precision
Hitbox = CreateDefaultSubobject<UBoxComponent>(TEXT("Hitbox"));
Hitbox->SetupAttachment(RootComponent);
Hitbox->SetBoxExtent(FVector(25.f, 25.f, 10.f));
Hitbox->SetCollisionProfileName("OverlapAllDynamic");
Hitbox->SetGenerateOverlapEvents(true);
// Visual hitbox indicator (small sphere)
HitboxIndicator = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("HitboxIndicator"));
HitboxIndicator->SetupAttachment(RootComponent);
HitboxIndicator->SetCollisionEnabled(ECollisionEnabled::NoCollision);
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMesh(TEXT("/Engine/BasicShapes/Sphere"));
if (SphereMesh.Succeeded())
{
HitboxIndicator->SetStaticMesh(SphereMesh.Object);
HitboxIndicator->SetRelativeScale3D(FVector(0.05f, 0.05f, 0.05f));
}
// Camera setup for top-down view
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetRelativeRotation(FRotator(-90.f, 0.f, 0.f)); // Top-down
SpringArm->TargetArmLength = 1200.f;
SpringArm->bDoCollisionTest = false;
SpringArm->bInheritPitch = false;
SpringArm->bInheritRoll = false;
SpringArm->bInheritYaw = false;
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
}
void ASTGPawn::BeginPlay()
{
Super::BeginPlay();
CurrentLives = MaxLives;
}
void ASTGPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Movement with bounds clamping
if (!MovementInput.IsZero())
{
FVector NewLocation = GetActorLocation();
FVector Normalized = MovementInput.GetSafeNormal();
NewLocation += Normalized * MoveSpeed * DeltaTime;
// Clamp to bounds
NewLocation.X = FMath::Clamp(NewLocation.X, BoundsMin.X, BoundsMax.X);
NewLocation.Y = FMath::Clamp(NewLocation.Y, BoundsMin.Y, BoundsMax.Y);
SetActorLocation(NewLocation);
}
// Auto-fire when holding fire button
if (bIsFiring)
{
FireTimer -= DeltaTime;
if (FireTimer <= 0.0f)
{
FireShot();
FireTimer = FireInterval;
}
}
}
void ASTGPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Bind movement axes
PlayerInputComponent->BindAxis("MoveForward", this, &ASTGPawn::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASTGPawn::MoveRight);
// Bind actions
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASTGPawn::StartFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &ASTGPawn::StopFire);
PlayerInputComponent->BindAction("Special", IE_Pressed, this, &ASTGPawn::UseSpecial);
}
void ASTGPawn::MoveForward(float Value)
{
MovementInput.X = Value;
}
void ASTGPawn::MoveRight(float Value)
{
MovementInput.Y = Value;
}
void ASTGPawn::StartFire()
{
bIsFiring = true;
FireTimer = 0.0f; // Fire immediately
}
void ASTGPawn::StopFire()
{
bIsFiring = false;
}
void ASTGPawn::FireShot()
{
// Will implement in Part 3 when we create bullets
UE_LOG(LogTemp, Warning, TEXT("FIRE!"));
}
void ASTGPawn::UseSpecial()
{
if (!bSpecialUsed)
{
bSpecialUsed = true;
UE_LOG(LogTemp, Warning, TEXT("SPECIAL ABILITY ACTIVATED!"));
// Will implement enemy/bullet destruction in Part 4
}
}
void ASTGPawn::TakeHit(int32 Damage)
{
CurrentLives = FMath::Clamp(CurrentLives - Damage, 0, MaxLives);
if (CurrentLives <= 0)
{
HandleDeath();
}
}
void ASTGPawn::HandleDeath()
{
SetActorHiddenInGame(true);
// Will notify GameMode in Part 6
}
void ASTGPawn::AddScore(int32 Points)
{
Score += Points;
}
Step 2.4: Compile the Code
In Unreal Editor:
- Click "Compile" button (bottom right, or
Ctrl+Alt+F11) - Wait for compilation (~30-60 seconds)
- Check for errors in Output Log
OR
In your IDE:
- Build the project (
Ctrl+Shift+Bin Visual Studio, orCtrl+F9in Rider) - Wait for build to finish
- Unreal Editor will hot-reload automatically
Expected Result:
- ✅ Compilation succeeds
- ✅ "C++ Classes" folder in Content Browser now shows
STGPawn - ✅ You can right-click STGPawn → "Create Blueprint Class based on STGPawn"
Step 2.5: Configure Input Mappings
This step is the same as Blueprint tutorial - we need to configure keyboard inputs in Project Settings.
- Edit → Project Settings
- Engine → Input
- Axis Mappings → Click "+" to add:
MoveForward: W = 1.0, S = -1.0MoveRight: D = 1.0, A = -1.0
- Action Mappings → Click "+" to add:
Fire: Space Bar, Left Mouse ButtonSpecial: X, Right Mouse Button
- Close Project Settings
Step 2.6: Create Blueprint Child (for Visual Assets)
Now we create a minimal Blueprint that inherits from our C++ class. This Blueprint is ONLY for visual assets!
- In Content Browser, navigate to
Content → Blueprints - Right-click → Blueprint Class
- In "Pick Parent Class" window:
- Click "All Classes" dropdown at top
- Search for
STGPawn - Select it
- Click "Select"
- Name it:
BP_Player - Double-click to open
In the Blueprint Editor:
Components (Left Panel):
- You'll see all components we created in C++ (ShipMesh, Hitbox, Camera, etc.)
- NO NEED to add them manually - they're already there from C++!
Variables (My Blueprint Panel):
- You'll see all 12 variables we defined in C++ (MoveSpeed, BoundsMin, etc.)
- NO NEED to create them - they're already there from C++!
- You can see their default values (750.0, etc.) - all from C++!
What to do in Blueprint:
- NOTHING for now! All logic is in C++
- In Part 9, we'll assign visual meshes/materials here
Compile and Save BP_Player
Step 2.7: Test the Player
- Drag
BP_Playerfrom Content Browser into the level viewport - Position it near the center (coordinates around X=0, Y=0, Z=0)
- Press Play (
Alt+P)
Expected Result
In Play Mode Window:
- You see the cone/cube player ship from above (top-down view)
- Ship is positioned at center of screen
- Background is the default gray/blue Unreal grid
- Camera follows the player from above
When pressing WASD keys:
- W - Ship moves upward on screen (toward top)
- S - Ship moves downward on screen (toward bottom)
- A - Ship moves left
- D - Ship moves right
- Movement stops immediately when you release keys (no sliding)
- Ship stops at screen edges (cannot move outside bounds)
When pressing Z or Space:
- You see "FIRE!" message appear in top-left corner of screen (yellow text)
- Message appears repeatedly while holding the button
- No bullets yet (we'll add those after creating bullet class)
When pressing X:
- "SPECIAL ABILITY ACTIVATED!" appears once in top-left corner
- Pressing X again does nothing (ability used up)
Visual Check:
- Ship model is visible (cone or cube shape)
- Ship rotates/faces the direction of movement
- No errors in Output Log
- Frame rate counter shows stable FPS (if enabled)
To exit Play mode: Press Esc or click the "Stop" button in the toolbar
Comparison: What We Just Did
Blueprint Approach (Part 2):
- Create BP_Player Blueprint ✅
- Add 5 components manually (clicking, dragging)
- Add 12 variables one-by-one (click +, type name, select type, compile, set value) × 12 = 15 minutes
- Create Blueprint nodes for movement logic (~30 nodes, connecting wires)
- Create Blueprint nodes for firing logic (~20 nodes)
- Configure input in Project Settings
- Total: ~60 minutes
C++ Approach (This Part):
- Create STGPawn C++ class ✅
- Copy-paste header file with all variables 30 seconds
- Copy-paste implementation file
- Compile
- Create BP_Player (inherits everything from C++)
- Configure input in Project Settings
- Total: ~15 minutes
Time saved: 45 minutes ⚡⚡⚡
What's Next?
In Part 3, we'll create the bullet class in C++. You'll see how defining bullet behavior in code is much cleaner than Blueprint nodes.
← Previous: Part 1 (C++) - Project Setup | Back to Index | Next: Part 3 (C++) - Create the Bullet →