praca_magisterska/games/unreal/tutorial/part-3-create-bullet.md

28 KiB

Part 3: Create the Bullet

← Previous: Part 2 - Create the Player | Back to Index | Next: Part 4 - Create the Enemy →


Step 3.1: Create Bullet Blueprint

  1. In Content Browser → Blueprints folder

  2. Right-click → Blueprint ClassActor

  3. Name it: BP_Bullet

  4. Double-click to open

  5. Add Components:

    • Paper Sprite → name BulletSprite
    • Sphere Collision → name BulletCollision
      • Radius: 8
      • Generate Overlap Events: CHECKED
      • Collision Preset: Custom → Query Only
    • Cube (Static Mesh) → name TempVisual
      • Scale: (0.1, 0.1, 0.05) - small bullet-sized cube
      • This provides visibility until proper visuals in Part 9
  6. Create Variables:

Variable Name Type Default Value
TravelDirection Vector (0, 1, 0)
TravelSpeed Float 1200.0
RemainingLifetime Float 4.0
IsEnemyProjectile Boolean false
Damage Integer 1

Expected Result after Compile:

  • Compile button shows GREEN checkmark
  • Components panel shows: DefaultSceneRoot → BulletSprite, BulletCollision, TempVisual
  • Variables panel shows all 5 variables with correct types

Expected Result in Viewport (Blueprint Editor):

  • Small cube visible (the TempVisual placeholder)
  • Sphere collision visible (radius 8)

Step 3.2: Bullet Movement Logic

  1. In Event Graph, from Event Tick:

a) Calculate movement:

  1. Right-click → Get TravelDirection

  2. Right-click → Get TravelSpeed

  3. Right-click → search Multiply (Vector * Float) → add it

    • Connect TravelDirection to the Vector input
    • Connect TravelSpeed to the Float input
    • Result: a vector representing full speed in travel direction
  4. Right-click → Get World Delta Seconds

  5. Right-click → search Multiply (Vector * Float) → add another one

    • Connect the RESULT from step 3 to the Vector input
    • Connect World Delta Seconds to the Float input
    • Result: movement delta for this frame (frame-rate independent)
  6. Right-click → Get Actor Location

  7. Right-click → search Add (Vector + Vector) → add it

    • Connect Actor Location to first input
    • Connect the movement delta (from step 5) to second input
  8. Right-click → Set Actor Location

    • Connect the Add result to "New Location"
    • Connect execution wire from Event Tick to Set Actor Location

b) Check lifetime and destroy when expired:

  1. Right-click → Get RemainingLifetime

  2. Right-click → Get World Delta Seconds

  3. Right-click → search Subtract (Float) → add it

    • Connect RemainingLifetime to the TOP input (A)
    • Connect World Delta Seconds to the BOTTOM input (B)
    • Result: RemainingLifetime minus DeltaSeconds
  4. Right-click → Set RemainingLifetime

    • Connect the Subtract result to it
    • This ALWAYS stores the new decreased lifetime value
  5. Connect execution wire:

    • From Set Actor Location (from step 8) → Set RemainingLifetime
  6. Right-click → search <= (Float) (less than or equal) → add it

    • Connect RemainingLifetime (from Set node output, or Get again) to TOP input
    • Type 0 in BOTTOM input
    • Result: true if lifetime has expired
  7. From Set RemainingLifetime, drag execution → Branch

    • Connect the <= result to Branch's "Condition" input
  8. On TRUE (lifetime expired):

    • From Branch's "True" pin, drag → Destroy Actor
    • Target: leave as "Self" (destroys this bullet)
  9. On FALSE (still alive):

    • Leave unconnected (bullet continues to exist, will check again next frame)

Visual:

Event Tick ──► Set Actor Location ──► Set RemainingLifetime ──► Branch
               (movement)              = Remaining - Delta      (Remaining <= 0?)
                                                                     │
                                                          TRUE ──────┴────── FALSE
                                                            │                  │
                                                            ▼                  ▼
                                                      Destroy Actor        (nothing)

2. CREATE "Initialize" FUNCTION:

a) Create the function:

  1. In "My Blueprint" panel (left side), find "Functions" section
  2. Click the "+" button next to Functions
  3. Name the new function Initialize
  4. Double-click to open the function graph

b) Add input parameters:

  1. In the function graph, you should see a purple "Initialize" entry node
  2. With the entry node selected, look at the Details panel (right side)
  3. Find "Inputs" section and click "+" to add parameters:
Parameter Type Default
Direction Vector -
Speed Float -
bIsEnemy Boolean -
Lifetime Float -
DamageValue Integer 1

The entry node should now show 5 input pins.

c) Build the function logic:

  1. Normalize and set direction:

    • Drag from Direction parameter (yellow pin) → search Normalize → add it
    • Right-click → Set TravelDirection
    • Connect Normalize's "Return Value" → Set TravelDirection input
  2. Set travel speed:

    • Right-click → Set TravelSpeed
    • Drag from Speed parameter → Set TravelSpeed input
  3. Set enemy projectile flag:

    • Right-click → Set IsEnemyProjectile
    • Drag from bIsEnemy parameter → Set IsEnemyProjectile input
  4. Set remaining lifetime:

    • Right-click → Set RemainingLifetime
    • Drag from Lifetime parameter → Set RemainingLifetime input
  5. Set damage:

    • Right-click → Set Damage
    • Drag from DamageValue parameter → Set Damage input

d) CRITICAL - Connect execution wires:

IMPORTANT: Without execution wires, the SET nodes will NEVER run! You must chain them together.

  1. From Initialize entry node (white triangle on right), drag execution → Set TravelDirection
  2. From Set TravelDirection (white triangle), drag execution → Set TravelSpeed
  3. From Set TravelSpeed, drag execution → Set IsEnemyProjectile
  4. From Set IsEnemyProjectile, drag execution → Set RemainingLifetime
  5. From Set RemainingLifetime, drag execution → Set Damage

Visual:

┌─────────────────────────┐
│ Initialize              │
│ Direction ○─────────────┼──► Normalize ──► Set TravelDirection
│ Speed ○─────────────────┼──────────────────► Set TravelSpeed
│ bIsEnemy ○──────────────┼──────────────────► Set IsEnemyProjectile
│ Lifetime ○──────────────┼──────────────────► Set RemainingLifetime
│ DamageValue ○───────────┼──────────────────► Set Damage
└──────────┬──────────────┘
           │ (white execution wire)
           ▼
    Set TravelDirection ──► Set TravelSpeed ──► Set IsEnemyProjectile
                                                         │
                                                         ▼
                              Set Damage ◄── Set RemainingLifetime

3. Compile and Save

Expected Result after Compile:

  • Compile button shows GREEN checkmark
  • "Initialize" function appears under Functions with 5 input parameters

NOTE: Bullet testing is possible after completing Step 3.4.


Step 3.3: Bullet Collision Logic

1. ADD THE OVERLAP EVENT:

  1. In the Components panel (top-left), click on "BulletCollision" to select it

  2. In the Details panel (right side), scroll down to find the "Events" section

    • Look for "On Component Begin Overlap"
    • Click the green "+" button next to it
    • This creates an event node in the Event Graph
  3. The Event Graph now shows a red node: "On Component Begin Overlap (BulletCollision)"

    • This fires whenever another actor overlaps with the bullet's collision sphere

2. CHECK IF THIS IS AN ENEMY PROJECTILE:

  1. Right-click → Get IsEnemyProjectile

    • This gets your boolean variable
  2. From "On Component Begin Overlap", drag execution → Branch

    • Connect IsEnemyProjectile to Branch's "Condition" input
    • TRUE = this is an enemy bullet (should damage player)
    • FALSE = this is a player bullet (should damage enemies)

3. TRUE BRANCH - Enemy Bullet Hits Player:

  1. From the "On Component Begin Overlap" node, look for the "Other Actor" output pin

    • This is the actor that overlapped with the bullet
    • Drag from "Other Actor" → search Cast to BP_Player
  2. Connect execution wire:

    • From Branch TRUE pin → Cast to BP_Player
  3. The cast has two execution outputs:

    • "Cast Succeeded" (top) - the other actor IS a player
    • "Cast Failed" (bottom) - the other actor is NOT a player
  4. From "Cast Succeeded", we need to damage the player:

    • From the cast's "As BP Player" output pin, drag → search TakeHit
    • This calls the TakeHit function you created in Part 2
    • IMPORTANT: The "Target" pin on TakeHit should now be connected to "As BP Player" (this happened automatically when you dragged from that pin)
    • If Target shows "self", you must manually connect "As BP Player" to the "Target" pin - TakeHit must be called ON the player, not on the bullet!
  5. Connect the Damage parameter:

    • Right-click → Get Damage (your bullet's damage variable)
    • Connect to TakeHit's "DamageAmount" input
  6. After damaging the player, destroy the bullet:

    • From TakeHit, drag execution → Destroy Actor
    • Leave "Target" as "Self" (destroys this bullet)
  7. Connect execution wire:

    • Cast to BP_Player → TakeHit → Destroy Actor

4. FALSE BRANCH - Player Bullet Hits Enemy (PLACEHOLDER):

NOTE: BP_Enemy doesn't exist yet, so we'll add a placeholder. You'll complete this logic in Part 4, Step 4.7.

  1. From Branch FALSE pin, drag execution → Print String
    • In the "In String" field, type: TODO: Damage enemy
    • This is temporary - we'll replace it with real enemy damage logic in Part 4

Visual:

┌──────────────────────────────────────┐
│ On Component Begin Overlap           │
│ (BulletCollision)                    │
│                          Other Actor ○─────────────────────┐
└──────────────┬───────────────────────┘                     │
               │                                             │
               ▼                                             ▼
        ┌─────────────────┐                       ┌──────────────────┐
        │ Branch          │                       │ Cast to BP_Player│
        │ IsEnemyProjectile                       └────────┬─────────┘
        └───────┬───┬─────┘                                │
                │   │                             Cast Succeeded
          TRUE ─┘   └─ FALSE                               │
                │         │                                ▼
                ▼         │                       ┌──────────────────┐
     Cast to BP_Player    │                       │ TakeHit          │
     (see above)          │                       │ Damage: Damage   │
                          │                       └────────┬─────────┘
                          ▼                                │
               ┌──────────────────┐                        ▼
               │ Print String     │               ┌──────────────────┐
               │ "TODO: Damage    │               │ Destroy Actor    │
               │  enemy"          │               │ (Self)           │
               └──────────────────┘               └──────────────────┘

4. Compile and Save

Expected Result after Compile:

  • Compile button shows GREEN checkmark
  • Event Graph shows "On Component Begin Overlap" event connected to Branch

NOTE: Collision testing requires player firing (Step 3.4) and enemies (Part 4). Full collision behavior is testable after completing Part 4.


Step 3.4: Complete Player Firing Logic (BP_Player)

Now that BP_Bullet exists, we can complete the player's firing functions.

1. Open BP_Player Blueprint:

  • In Content Drawer, navigate to Content → Blueprints
  • Double-click "BP_Player" to open the Blueprint Editor

2. SET THE BULLET CLASS VARIABLE:

  1. In My Blueprint panel, find the BulletClass variable
  2. Click on it to select it
  3. In Details panel, find "Default Value"
  4. Click the dropdown and select BP_Bullet
  5. Compile to save the change

3. CREATE SPAWN BULLET FUNCTION:

a) In "My Blueprint" panel, under "Functions", click "+"

b) Name the function SpawnBullet

c) Double-click to open function graph

d) Add input parameters to the function:

  • In the function graph, look at the purple "SpawnBullet" entry node
  • In Details panel (right side), find "Inputs" section
  • Click "+" to add a new input parameter
Parameter Type
SpawnLocation Vector
Direction Vector

The entry node should now show two input pins: SpawnLocation and Direction

e) Build the spawning logic:

  1. Right-click → search Spawn Actor from Class → add it

  2. For the "Class" input:

    • Right-click → Get BulletClass (your variable)
    • Connect BulletClass output (blue pin) to SpawnActor's "Class" input (purple pin)
  3. For the "Spawn Transform Location" input:

    • On the SpawnActor node, look for "Spawn Transform Location" (orange pin)
    • If you only see "Spawn Transform", right-click on it → "Split Struct Pin" to expand it
    • Drag a wire from the SpawnLocation parameter (on the SpawnBullet entry node) to "Spawn Transform Location"
    • Leave Rotation and Scale at defaults (0,0,0 and 1,1,1)
  4. Connect execution wire:

    • Drag from SpawnBullet entry node (white triangle) → SpawnActor (white triangle)

f) Initialize the spawned bullet:

  1. Add the Cast node:

    • From SpawnActor's "Return Value" output (blue pin on right side), drag → search Cast to BP_Bullet
    • Connect "Return Value" to the Cast's "Object" input
  2. Connect execution wire:

    • From SpawnActor (white triangle output) → Cast to BP_Bullet (white triangle input)
  3. Add the Initialize call:

    • From the Cast's "As BP Bullet" output (blue pin), drag → search Initialize
    • This calls the Initialize function you created in Step 3.2
    • IMPORTANT: The "Target" pin should automatically connect to "As BP Bullet"
  4. Connect Initialize parameters:

    • Direction: Drag from the Direction parameter (on SpawnBullet entry node, yellow pin) → Initialize's "Direction" input
    • Speed: Right-click → Get BulletSpeed → connect to Initialize's "Speed" input
    • bIsEnemy: Leave unchecked (false) - player bullets aren't enemy projectiles
    • Lifetime: Type 4.0 in the input field (or leave default)
    • DamageValue: Leave at default 1
  5. Connect execution wire:

    • From Cast to BP_Bullet → Initialize

Visual:

┌───────────────────────┐      ┌─────────────────────────────────┐
│ SpawnBullet           │─────►│ Spawn Actor from Class          │
│ SpawnLocation ○───────┼─────►│   Class: BulletClass            │
│ Direction ○           │      │   Spawn Transform Location ○◄───┘
└───────────────────────┘      │   Return Value ○────────────────┐
                               └─────────────────────────────────┘
                                                                 │
                                           ┌─────────────────────┘
                                           ▼
                               ┌─────────────────────────┐
                               │ Cast to BP_Bullet       │
                               │   Object ○◄─────────────┘
                               │   As BP Bullet ○────────┐
                               └─────────────────────────┘
                                                         │
                                           ┌─────────────┘
                                           ▼
                               ┌─────────────────────────┐
                               │ Initialize              │
                               │   Target ○◄─────────────┘
                               │   Direction ○◄──────────── (from entry node)
                               │   Speed ○◄────────────── BulletSpeed
                               │   bIsEnemy: false       │
                               │   Lifetime: 4.0         │
                               └─────────────────────────┘

g) Compile (should have no errors now)

4. CREATE FIRE VOLLEY FUNCTION:

a) In "My Blueprint" panel, under "Functions", click "+"

b) Name the function FireVolley

c) Double-click to open function graph

d) Inside FireVolley function graph, build this logic step by step:


STEP 1 - Get the volley size and check for single bullet case:

  1. You should see a purple "FireVolley" entry node on the left

    • This is where execution starts when the function is called
  2. Right-click → search Get VolleySize → add it

    • This gets your variable value (default 3)
  3. Right-click → search Equal (Integer) or == → add it

    • Connect VolleySize output to the TOP input (A)
    • Type 1 in the BOTTOM input (B)
    • This checks: is VolleySize exactly 1?
  4. From FireVolley entry node, drag execution → Branch

    • Connect the == result to Branch's "Condition" input
    • TRUE = single bullet, FALSE = multiple bullets in spread

STEP 2 - TRUE branch: Fire single bullet straight up:

  1. From Branch TRUE pin, drag execution → right-click → search SpawnBullet (the function you created in step 3 above)

  2. For SpawnBullet's "Spawn Location" input:

    • Right-click → Get Actor Location
    • Connect the output to Spawn Location
  3. For SpawnBullet's "Direction" input (fire straight UP):

    • Right-click → Make Vector
    • Set X = 1 (up in top-down view)
    • Set Y = 0
    • Set Z = 0
    • Connect to Direction input
  4. After SpawnBullet, drag execution → Return Node

    • This exits the function early (don't continue to the loop)

Visual so far:

┌─────────────┐      ┌────────────────────┐
│ FireVolley  │─────►│ Branch             │
│ (entry)     │      │ VolleySize == 1    │
└─────────────┘      └─────────┬──────────┘
                               │ TRUE
                               ▼
                     ┌─────────────────────┐
                     │ SpawnBullet         │
                     │ Location: ActorLoc  │───► Return
                     │ Direction: (1,0,0)  │
                     └─────────────────────┘

STEP 3 - FALSE branch: Fire multiple bullets in a spread pattern:

  1. From Branch FALSE pin, we need to loop through each bullet. First, calculate the starting angle:

    The spread is CENTERED on "straight up". For 3 bullets with 12° spread:

    • Bullet 0: -12° (left of center)
    • Bullet 1: 0° (center, straight up)
    • Bullet 2: +12° (right of center)

    Formula: StartAngle = -(VolleySpread * (VolleySize - 1)) / 2

  2. Calculate StartAngle:

    • Right-click → Get VolleySize

    • Right-click → Subtract (Integer) → connect VolleySize, type 1

      • Result: (VolleySize - 1) = 2 for default
    • Right-click → Get VolleySpread

    • Right-click → Multiply (float) → connect VolleySpread and (VolleySize-1)

      • NOTE: The integer will auto-convert to float
      • Result: VolleySpread * (VolleySize - 1) = 12 * 2 = 24
    • Right-click → Divide (float) → connect the multiply result, type 2

      • Result: 24 / 2 = 12
    • Right-click → Negate (float) or "Multiply by -1"

      • Result: -12 (this is StartAngle)
    • Right-click → Set → create a LOCAL variable "StartAngle" (Float)

      • OR just keep the wire connected (we'll use it in the loop)
  3. Set up the FOR loop:

    • From Branch FALSE pin, drag → right-click → search For Loop
    • "First Index": type 0
    • "Last Index": connect (VolleySize - 1)
      • You already calculated this in step 10, reuse it or calculate again:
      • Get VolleySize → Subtract 1 → connect to Last Index

    The loop will run with Index = 0, 1, 2 for VolleySize=3

  4. Inside the loop (from "Loop Body" execution pin):

    Calculate angle for THIS bullet:

    • Right-click → Multiply (float)

      • Connect loop "Index" to first input (auto-converts int to float)
      • Connect VolleySpread to second input
      • Result: Index * VolleySpread (0, 12, 24 for indices 0, 1, 2)
    • Right-click → Add (float)

      • Connect StartAngle (the -12 from step 10) to first input
      • Connect (Index * VolleySpread) to second input
      • Result: StartAngle + (Index * VolleySpread) = -12, 0, +12 degrees

    This is the angle in DEGREES. Store or continue with this value.

  5. Convert angle from degrees to a direction vector:

    In Unreal's top-down view (looking down Z axis):

    • X axis = up/down on screen (vertical)
    • Y axis = left/right on screen (horizontal)
    • Angle 0° = straight up = direction (1, 0, 0)
    • Angle 90° = right = direction (0, 1, 0)
    Direction.X = Cos(angle)   // Cos for vertical (forward/up)
    Direction.Y = Sin(angle)   // Sin for horizontal offset
    Direction.Z = 0
    

    BUT Unreal's Sin/Cos use RADIANS, not degrees!

    a) Convert degrees to radians:

    • Right-click → search Degrees To Radians → add it
    • Connect your angle (from step 12) to the input
    • Output is angle in radians

    b) Calculate X component (Cos - forward/up direction):

    • Right-click → Cos (Radians) → add it
    • Connect the Degrees To Radians output to Cos input

    c) Calculate Y component (Sin - horizontal offset):

    • Right-click → Sin (Radians) → add it
    • To connect the SAME radians value to Sin (without losing the Cos connection):
      • Option 1: Ctrl+drag from "Degrees To Radians" output to Sin input (creates second wire)
      • Option 2: Drag directly from "Degrees To Radians" output again - UE5 allows multiple wires from one output pin
    • Both Cos and Sin should now be connected to the same radians value

    d) Make the direction vector:

    • Right-click → Make Vector
    • Connect Cos result to X (forward/up direction)
    • Connect Sin result to Y (horizontal spread)
    • Set Z = 0
  6. Spawn the bullet:

    • From the loop body execution, drag → SpawnBullet (from step 3)
    • Spawn Location: Get Actor Location
    • Direction: Connect the Make Vector from step 13
  7. Connect loop completion:

    • The For Loop has a "Completed" execution pin
    • This fires AFTER all iterations are done
    • Drag from "Completed" → Return Node (or leave unconnected)

Complete Visual:

┌─────────────┐      ┌────────────────────┐
│ FireVolley  │─────►│ Branch             │
│ (entry)     │      │ VolleySize == 1    │
└─────────────┘      └──────┬───────┬─────┘
                            │TRUE   │FALSE
                            ▼       ▼
                     ┌──────────┐  ┌─────────────────────────────┐
                     │SpawnBullet│  │ Calculate StartAngle        │
                     │Dir:(1,0,0)│  │ = -(Spread*(Size-1))/2      │
                     └─────┬────┘  └──────────────┬──────────────┘
                           │                      ▼
                           ▼              ┌─────────────────┐
                        Return            │ For Loop        │
                                          │ 0 to Size-1     │
                                          └───────┬─────────┘
                                                  │ Loop Body
                                                  ▼
                                   ┌──────────────────────────────┐
                                   │ Angle = Start + (i * Spread) │
                                   │ Radians = DegreesToRadians   │
                                   │ Dir.X = Cos(Radians)         │
                                   │ Dir.Y = Sin(Radians)         │
                                   │ SpawnBullet(Location, Dir)   │
                                   └──────────────────────────────┘

Test Values (VolleySize=3, VolleySpread=12):

  • StartAngle = -(12 * 2) / 2 = -12°
  • Bullet 0: -12 + (0 * 12) = -12° → slightly left
  • Bullet 1: -12 + (1 * 12) = 0° → straight up
  • Bullet 2: -12 + (2 * 12) = +12° → slightly right

5. CONNECT FIREVOLLEY TO THE FIRE RATE LIMITER:

  1. Go back to the Event Graph tab
  2. Find your IA_Fire logic from Step 2.5
  3. Delete the Print String "FIRE!" placeholder
  4. From the "Set FireTimer = FireInterval" node, drag execution → FireVolley

Updated Visual:

┌─────────────────┐    TRUE    ┌─────────────────────┐
│ Branch          │───────────►│ Set FireTimer       │──► FireVolley
│ FireTimer <= 0  │            │   = FireInterval    │
└─────────────────┘            └─────────────────────┘

6. Compile and Save

Expected Result after Compile:

  • Compile button shows GREEN checkmark
  • "FireVolley" and "SpawnBullet" functions appear under Functions in My Blueprint panel

Expected Result in Play mode:

  • Pressing Z or Left Mouse Button spawns 3 bullets in a spread pattern
  • Bullets travel upward from player position
  • Rapid fire when holding the button (every 0.08 seconds)
  • Bullets disappear after 4 seconds (RemainingLifetime)

← Previous: Part 2 - Create the Player | Back to Index | Next: Part 4 - Create the Enemy →