9.0 KiB
Part 3 (C++): Create the Bullet
← Previous: Part 2 (C++) - Create the Player | Back to Index | Next: Part 4 (C++) - Create the Enemy →
Overview
Create the bullet/projectile class in C++. Again, we'll copy-paste all variables instead of clicking UI.
Time comparison:
- Blueprint: ~30 minutes (5 variables + Blueprint nodes)
- C++ (this part): ~10 minutes (copy-paste code)
Step 3.1: Create STGProjectile C++ Class
- Tools → New C++ Class
- Choose "Actor" as parent class → Next
- Name:
STGProjectile→ Create Class - Wait for compilation
Step 3.2: Copy-Paste STGProjectile.h
Replace the entire header file with:
⚠️ IMPORTANT: Replace
YOURPROJECTNAME_APIwith your actual project's API macro (e.g.,BULLETHELLCPP_API).
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "STGProjectile.generated.h"
class USphereComponent;
class UProjectileMovementComponent;
class UStaticMeshComponent;
UCLASS()
class YOURPROJECTNAME_API ASTGProjectile : public AActor
{
GENERATED_BODY()
public:
ASTGProjectile();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
// ===== COMPONENTS =====
UPROPERTY(VisibleDefaultsOnly, Category=Projectile)
USphereComponent* CollisionComp;
UPROPERTY(VisibleDefaultsOnly, Category=Projectile)
UStaticMeshComponent* MeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Movement)
UProjectileMovementComponent* ProjectileMovement;
// ===== VARIABLES (all with defaults!) =====
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
bool bIsPlayerBullet = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
float Damage = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
FLinearColor BulletColor = FLinearColor::Green;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gameplay")
float Lifetime = 4.0f;
// ===== FUNCTIONS =====
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
void SetBulletColor(FLinearColor InColor);
void SetSpeed(float InSpeed);
private:
UMaterialInstanceDynamic* DynamicMaterial;
};
What just happened: 5 bullet variables defined with defaults in 10 seconds!
Step 3.3: Copy-Paste STGProjectile.cpp
Replace the implementation file with:
#include "STGProjectile.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "STGPawn.h"
ASTGProjectile::ASTGProjectile()
{
PrimaryActorTick.bCanEverTick = true;
// Collision sphere
CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
CollisionComp->InitSphereRadius(5.0f);
CollisionComp->SetCollisionProfileName("OverlapAllDynamic");
CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ASTGProjectile::OnOverlapBegin);
RootComponent = CollisionComp;
// Visual mesh
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
MeshComp->SetupAttachment(CollisionComp);
MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereMesh(TEXT("/Engine/BasicShapes/Sphere"));
if (SphereMesh.Succeeded())
{
MeshComp->SetStaticMesh(SphereMesh.Object);
MeshComp->SetRelativeScale3D(FVector(0.1f, 0.1f, 0.1f));
}
// Projectile movement
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp"));
ProjectileMovement->UpdatedComponent = CollisionComp;
ProjectileMovement->InitialSpeed = 1200.f;
ProjectileMovement->MaxSpeed = 1200.f;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->bShouldBounce = false;
ProjectileMovement->ProjectileGravityScale = 0.0f; // No gravity for top-down shooter!
}
void ASTGProjectile::BeginPlay()
{
Super::BeginPlay();
// Set lifetime here so Blueprint overrides of Lifetime variable work
SetLifeSpan(Lifetime);
// Create dynamic material for bullet color
if (MeshComp)
{
DynamicMaterial = MeshComp->CreateAndSetMaterialInstanceDynamic(0);
if (DynamicMaterial)
{
DynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), BulletColor);
DynamicMaterial->SetVectorParameterValue(TEXT("EmissiveColor"), BulletColor * 3.0f);
}
}
}
void ASTGProjectile::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASTGProjectile::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && OtherActor != this)
{
if (bIsPlayerBullet)
{
// Player bullet hits enemy (will implement in Part 4)
// ASTGEnemy* Enemy = Cast<ASTGEnemy>(OtherActor);
// if (Enemy) { Enemy->HandleDamage(Damage); Destroy(); }
}
else
{
// Enemy bullet hits player
ASTGPawn* Player = Cast<ASTGPawn>(OtherActor);
if (Player)
{
Player->TakeHit(1);
Destroy();
}
}
}
}
void ASTGProjectile::SetBulletColor(FLinearColor InColor)
{
BulletColor = InColor;
if (DynamicMaterial)
{
DynamicMaterial->SetVectorParameterValue(TEXT("BaseColor"), BulletColor);
DynamicMaterial->SetVectorParameterValue(TEXT("EmissiveColor"), BulletColor * 3.0f);
}
}
void ASTGProjectile::SetSpeed(float InSpeed)
{
if (ProjectileMovement)
{
ProjectileMovement->InitialSpeed = InSpeed;
ProjectileMovement->MaxSpeed = InSpeed;
}
}
Step 3.4: Compile
- Click Compile in Unreal Editor
- Wait for compilation
- Check for errors
Step 3.5: Update Player Firing Logic
Now that we have bullets, let's make the player actually spawn them!
Open STGPawn.cpp
Find the FireShot() function and replace it with:
void ASTGPawn::FireShot()
{
// Spawn volley of bullets shooting FORWARD (+X direction = toward top of screen)
for (int32 i = 0; i < VolleySize; i++)
{
// Spawn slightly in front of player (+X)
FVector SpawnLocation = GetActorLocation() + FVector(50.f, 0.f, 0.f);
// Base rotation: FRotator(0,0,0) points in +X direction (forward in top-down view)
FRotator SpawnRotation = FRotator::ZeroRotator;
// Spread calculation: fan out on Yaw (left/right)
float Angle = VolleySpread * (i - (VolleySize - 1) / 2.0f);
SpawnRotation.Yaw += Angle;
// Spawn bullet
UWorld* World = GetWorld();
if (World)
{
ASTGProjectile* Bullet = World->SpawnActor<ASTGProjectile>(
ASTGProjectile::StaticClass(),
SpawnLocation,
SpawnRotation
);
if (Bullet)
{
Bullet->bIsPlayerBullet = true;
Bullet->SetSpeed(BulletSpeed);
Bullet->SetBulletColor(FLinearColor::Green);
}
}
}
}
Add include at top of STGPawn.cpp:
#include "STGProjectile.h" // Add this line
Compile again!
Step 3.6: Test Bullet Firing
- Press Play (
Alt+P) - Move with WASD
- Press Space to fire
- You should see spheres shooting toward the top of the screen!
Expected Result
- ✅ Player fires 3 bullets in a spread pattern
- ✅ Bullets travel toward the top of the screen (forward direction)
- ✅ Bullets auto-destroy after 4 seconds
- ✅ Can hold Space to auto-fire
Note: Bullets use the default engine material (gray/white). We'll add colored materials in Part 9 (Polish phase).
Comparison Summary
Blueprint Approach:
- Create BP_Bullet Blueprint
- Add 5 variables manually (×5 clicks each = 25 clicks)
- Create Blueprint nodes for movement (~15 nodes)
- Create Blueprint nodes for collision (~10 nodes)
- Update BP_Player firing logic (~15 more nodes)
- Total: ~30 minutes
C++ Approach:
- Create STGProjectile class
- Copy-paste header (5 variables with defaults)
- Copy-paste implementation
- Update one function in STGPawn
- Compile twice
- Total: ~10 minutes
Time saved: 20 minutes ⚡
What's Next?
In Part 4, we'll create enemies with C++. You'll see how easy it is to copy enemy patterns!
← Previous: Part 2 (C++) - Create the Player | Back to Index | Next: Part 4 (C++) - Create the Enemy →