praca_magisterska/games/unreal/tutorial/part-2-cpp-create-player.md
copilot-swe-agent[bot] 1053bdec73 Address PR feedback: improve part-1, add Expected Results, split parts 4-9 into separate files
Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
2026-01-08 15:19:06 +00:00

510 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Part 2 (C++): Create the Player
[← Previous: Part 1 (C++) - Project Setup](part-1-cpp-project-setup.md) | [Back to Index](README.md) | [Next: Part 3 (C++) - Create the Bullet →](part-3-cpp-create-bullet.md)
---
## 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:
1. Go to **Tools → New C++ Class** (top menu bar)
2. In the "Choose Parent Class" window:
- Click **"Pawn"** (NOT Character - we want simple 2D control)
- Click **"Next"**
3. In the "Name Your New Actor" window:
- **Name:** `STGPawn`
- **Path:** Should be `Source/BulletHellGame/` (default)
- Click **"Create Class"**
### What Happens Next:
1. Unreal generates `STGPawn.h` and `STGPawn.cpp`
2. Your IDE opens with the new files
3. Project compiles automatically (~30-60 seconds)
4. 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):
```cpp
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:
```cpp
#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:
```cpp
#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:
1. Click **"Compile"** button (bottom right, or `Ctrl+Alt+F11`)
2. Wait for compilation (~30-60 seconds)
3. Check for errors in Output Log
OR
### In your IDE:
1. Build the project (`Ctrl+Shift+B` in Visual Studio, or `Ctrl+F9` in Rider)
2. Wait for build to finish
3. 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.
1. **Edit → Project Settings**
2. **Engine → Input**
3. **Axis Mappings** → Click "+" to add:
- `MoveForward`: W = 1.0, S = -1.0
- `MoveRight`: D = 1.0, A = -1.0
4. **Action Mappings** → Click "+" to add:
- `Fire`: Space Bar, Left Mouse Button
- `Special`: X, Right Mouse Button
5. 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!
1. In Content Browser, navigate to `Content → Blueprints`
2. Right-click → **Blueprint Class**
3. In "Pick Parent Class" window:
- Click "All Classes" dropdown at top
- Search for `STGPawn`
- Select it
- Click "Select"
4. Name it: `BP_Player`
5. 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
1. Drag `BP_Player` from Content Browser into the level viewport
2. Position it near the center (coordinates around X=0, Y=0, Z=0)
3. 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):
1. Create BP_Player Blueprint ✅
2. Add 5 components manually (clicking, dragging)
3. Add 12 variables one-by-one (click +, type name, select type, compile, set value) × 12 = **15 minutes**
4. Create Blueprint nodes for movement logic (~30 nodes, connecting wires)
5. Create Blueprint nodes for firing logic (~20 nodes)
6. Configure input in Project Settings
7. **Total: ~60 minutes**
### C++ Approach (This Part):
1. Create STGPawn C++ class ✅
2. Copy-paste header file with all variables **30 seconds**
3. Copy-paste implementation file
4. Compile
5. Create BP_Player (inherits everything from C++)
6. Configure input in Project Settings
7. **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](part-1-cpp-project-setup.md) | [Back to Index](README.md) | [Next: Part 3 (C++) - Create the Bullet →](part-3-cpp-create-bullet.md)