diff --git a/games/unreal/BulletHellGame/BulletHellGame/Content/Blueprints/BP_Player.uasset b/games/unreal/BulletHellGame/BulletHellGame/Content/Blueprints/BP_Player.uasset index db6fb7e..d9082b5 100644 Binary files a/games/unreal/BulletHellGame/BulletHellGame/Content/Blueprints/BP_Player.uasset and b/games/unreal/BulletHellGame/BulletHellGame/Content/Blueprints/BP_Player.uasset differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto5.uasset b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto5.uasset index 4997cdd..3f55fc4 100644 Binary files a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto5.uasset and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto5.uasset differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto6.uasset b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto6.uasset new file mode 100644 index 0000000..61757ba Binary files /dev/null and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto6.uasset differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto7.uasset b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto7.uasset new file mode 100644 index 0000000..ef2ea01 Binary files /dev/null and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto7.uasset differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto8.uasset b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto8.uasset new file mode 100644 index 0000000..eba29cb Binary files /dev/null and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Game/Blueprints/BP_Player_Auto8.uasset differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/PackageRestoreData.json b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/PackageRestoreData.json index 84b654a..52549a4 100644 Binary files a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/PackageRestoreData.json and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/PackageRestoreData.json differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Temp/Untitled_1_Auto8.umap b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Temp/Untitled_1_Auto8.umap new file mode 100644 index 0000000..aeba8f1 Binary files /dev/null and b/games/unreal/BulletHellGame/BulletHellGame/Saved/Autosaves/Temp/Untitled_1_Auto8.umap differ diff --git a/games/unreal/BulletHellGame/BulletHellGame/Saved/Config/LinuxEditor/EditorPerProjectUserSettings.ini b/games/unreal/BulletHellGame/BulletHellGame/Saved/Config/LinuxEditor/EditorPerProjectUserSettings.ini index 0ba2f7b..f4c4bb9 100644 --- a/games/unreal/BulletHellGame/BulletHellGame/Saved/Config/LinuxEditor/EditorPerProjectUserSettings.ini +++ b/games/unreal/BulletHellGame/BulletHellGame/Saved/Config/LinuxEditor/EditorPerProjectUserSettings.ini @@ -393,6 +393,8 @@ K2Node_VariableGet=CameraComponent.CameraSettings.Overscan,CameraComponent.PostP K2Node_EnhancedInputAction=CameraComponent.CameraSettings.Overscan,CameraComponent.PostProcess.Lens K2Node_VariableSet=CameraComponent.CameraSettings.Overscan,CameraComponent.PostProcess.Lens K2Node_GetInputActionValue=CameraComponent.CameraSettings.Overscan,CameraComponent.PostProcess.Lens +EdGraph=CameraComponent.CameraSettings.Overscan,CameraComponent.PostProcess.Lens +K2Node_FunctionEntry=CameraComponent.CameraSettings.Overscan,CameraComponent.PostProcess.Lens [/Script/GraphEditor.GraphEditorSettings] DataPinStyle=BPST_VariantA @@ -684,6 +686,8 @@ InputTriggerTimedBase="\"Object\" " InputTriggerHoldAndRelease="\"Object\" " InputTriggerTap="\"Object\" " InputTriggerPulse="\"Object\" " +EdGraphNode_Comment="\"Object\" " +EdGraphNode="\"Object\" " [AssetEditorSubsystem] CleanShutdown=False @@ -1588,6 +1592,14 @@ EdGraph.Graph=True EdGraph.Inputs=True EdGraph.Outputs=True K2Node_CallFunction.Graph=True +K2Node_FunctionEntry.GraphNodeDetail=True +K2Node_FunctionEntry.Graph=True +K2Node_FunctionEntry.Inputs=True +K2Node_FunctionEntry.Outputs=True +K2Node_CallFunction.Inputs=True +K2Node_CallFunction.Outputs=True +EdGraphNode_Comment.GraphNodeDetail=True +EdGraphNode_Comment.Comment=True [DetailMultiObjectNodeExpansion] GeneralProjectSettings=True @@ -1661,6 +1673,7 @@ PreserveScaleRatio=True [DetailCategoriesAdvanced] InputAction.Action=False +EdGraph.Graph=True [AssetEditorToolkitTabLocation] /Game/Input/IA_Move.IA_Move=1 diff --git a/games/unreal/bullet_hell_diy_unreal.txt b/games/unreal/bullet_hell_diy_unreal.txt deleted file mode 100644 index a43aa47..0000000 --- a/games/unreal/bullet_hell_diy_unreal.txt +++ /dev/null @@ -1,2031 +0,0 @@ -================================================================================ -UNREAL ENGINE BULLET HELL GAME - COMPLETE STEP-BY-STEP TUTORIAL -================================================================================ - -This tutorial recreates the Unity "magisterka_1" bullet-hell shooter in Unreal Engine 5. -The game features: -- Player ship with movement and shooting -- Enemies that move downward with sinusoidal horizontal motion -- Enemy spawning that increases over time -- Radial bullet patterns from enemies -- Score, lives, and timer UI -- Special "bomb" ability to clear screen -- 5-minute game duration with victory/defeat conditions - -================================================================================ -PART 1: PROJECT SETUP -================================================================================ - -STEP 1.1: Create New Project --------------------------------------------------------------------------------- -1. Open Epic Games Launcher -2. Click "Unreal Engine" tab on the left sidebar -3. Click yellow "Launch" button next to your UE5 version -4. Wait for Unreal Engine to open (this may take 1-2 minutes) - -5. In the "Unreal Project Browser" window that appears: - - At the top, select "Games" category (should be selected by default) - - Click "Blank" template (empty square icon) - -6. On the right side panel, configure: - - Project Defaults: Blueprint (not C++) - - Target Platform: Desktop - - Quality Preset: Maximum - - Starter Content: UNCHECKED (we don't need it) - - Raytracing: UNCHECKED - -7. At the bottom: - - Choose folder location where you want to save - - Name the project: "BulletHellGame" - -8. Click "Create" button (bottom right, yellow) - -EXPECTED RESULT: Unreal Editor opens with an empty level. You should see: -- Main 3D viewport in the center -- Outliner panel on the right (showing "Untitled" level) -- Details panel on the right side - -NOTE: The Content Drawer is NOT open by default. To open it: -- Click "Content Drawer" button at the bottom of the screen, OR -- Press Ctrl+Space, OR -- Go to Window → Content Drawer - - -STEP 1.2: Set Up 2D Game View --------------------------------------------------------------------------------- -Since bullet-hell games are typically 2D, we'll set up a top-down orthographic view. - -1. In the main viewport, look at the top-left corner -2. Click the dropdown that says "Perspective" -3. Select "Top" from the dropdown menu (alt + J) - -4. To set up proper camera for the game: - - Go to menu bar (the top bar): Edit (Second from left, next fo "File" and "Window") → Project Settings - - In the left sidebar, search for "Maps & Modes" (Project -> 4th from the top, next to GameplayTags and Movies) - - Click on "Maps & Modes" - -5. Under "Default Modes": - - Find "Default GameMode" dropdown - - We'll create our own later, leave it for now - -6. Close Project Settings window (X in top right) - - -STEP 1.3: Create Folder Structure --------------------------------------------------------------------------------- -1. Look at the Content Browser at the bottom of the screen -2. You should see "Content" folder on the left panel - -3. Right-click on "Content" folder → New Folder - Name it: "Blueprints" - -4. Right-click on "Content" folder → New Folder - Name it: "Materials" - -5. Right-click on "Content" folder → New Folder - Name it: "Sprites" (for 2D textures) - -6. Right-click on "Content" folder → New Folder - Name it: "UI" - -EXPECTED RESULT: Content Browser shows 4 folders: - Content/ - ├── Blueprints/ - ├── Materials/ - ├── Sprites/ - └── UI/ - - -================================================================================ -PART 2: CREATE THE PLAYER -================================================================================ - -STEP 2.1: Create Player Blueprint --------------------------------------------------------------------------------- -1. In Content Browser, double-click "Blueprints" folder to open it - -2. Right-click in empty space → Blueprint Class - -3. In the popup window "Pick Parent Class": - - Click "Pawn" (NOT Character - we want simple 2D control) - - Click "Select" - -4. Name the new Blueprint: "BP_Player" - -5. Double-click "BP_Player" to open the Blueprint Editor - -EXPECTED RESULT: A new tab opens showing the Blueprint Editor with: -- Components panel on the left -- Viewport in the center -- Details panel on the right -- Tab bar above the Viewport with: Viewport | Construction Script | Event Graph (third from left) - - -STEP 2.2: Add Player Visual Components --------------------------------------------------------------------------------- -1. In the Components panel (left side), you'll see "DefaultSceneRoot" - - Click on "DefaultSceneRoot" to select it (this ensures new components are added as children) - -2. With DefaultSceneRoot selected, click "Add" button (green, top of Components panel) - - Search for "Paper Sprite" - - Click "Paper Sprite" to add it (it will be added as a child of DefaultSceneRoot) - - Rename it to "PlayerSprite" (click on it, then press F2) - - NOTE: If Paper Sprite is not available: - - Go to Edit → Plugins (from main menu bar) - - Search for "Paper2D" - - Make sure "Paper2D" plugin is ENABLED - - Restart the editor if prompted - -3. With "PlayerSprite" selected, look at Details panel (right side): - - Find "Source Sprite" property - - For now, leave it empty (we'll create sprites later) - - Under "Transform", set Scale to (1.0, 1.0, 1.0) - -4. With DefaultSceneRoot still selected, click "Add" button again: - - Search for "Box Collision" - - Click "Box Collision" to add it (it will be added as a child of DefaultSceneRoot) - - Rename it to "PlayerCollision" - -5. With "PlayerCollision" selected, in Details panel: - - Under "Shape", set Box Extent to: X=25, Y=25, Z=10 - - Under "Collision", click "Collision Presets" dropdown - - Select "Custom..." - - Set "Collision Enabled" to "Query Only (No Physics Collision)" - - Enable "Generate Overlap Events" box - -6. With DefaultSceneRoot still selected, click "Add" button: - - Search for "Arrow" - - Click "Arrow" to add it (it will be added as a child of DefaultSceneRoot) - - This shows which direction is "forward" (useful for debugging) - -7. With DefaultSceneRoot still selected, click "Add" button: - - Search for "Cube" → add "Cube" (Static Mesh) - - Rename it to "TempVisual" - - In Details panel, set Scale to (0.5, 0.5, 0.1) so it's small and flat - - This provides a visible placeholder until we add proper sprites in Part 9 - -8. With DefaultSceneRoot still selected, click "Add" button: - - Search for "Camera" → add "Camera" - - Rename it to "PlayerCamera" - - In Details panel, set Location to: X=0, Y=0, Z=1000 (1000 units above player) - - Set Rotation to: X=0, Y=-90, Z=0 (looking straight down) - - In Details panel, find "Activation" category → "Auto Activate" checkbox - - This should already be ENABLED by default - - If not, check/enable it - this tells Unreal to use THIS camera when playing - - This camera will follow the player, making testing easy - - NOTE: This temporary camera ensures you can see the player during testing. - In Part 8, we'll set up a proper fixed camera for the final game. - -EXPECTED RESULT: Components panel shows: - DefaultSceneRoot - ├── PlayerSprite (Paper Sprite) - ├── PlayerCollision (Box Collision) - ├── Arrow - ├── TempVisual (Cube) - └── PlayerCamera (Camera) - -9. IMPORTANT - Enable Auto Possession for testing: - - Click on "BP_Player (Self)" at the top of the Components panel (the root) - - In Details panel, find "Pawn" category - - Find "Auto Possess Player" dropdown (currently set to "Disabled") - - Change it to "Player 0" - - This makes the engine automatically possess this pawn when playing, - which activates the PlayerCamera - - NOTE: This setting will be overridden later when we set up the GameMode - in Part 8, which handles possession automatically. - - -STEP 2.3: Create Player Variables --------------------------------------------------------------------------------- -(Continue in the same BP_Player Blueprint Editor tab that was opened in Step 2.1) - -1. In the Blueprint Editor, look at the left panel (below the Components panel) -2. Find "My Blueprint" section -3. Under "Variables", click the "+" button - -4. Create these variables one by one (click +, then set properties in Details): - - NOTE: Variables are NOT added to the Components hierarchy. They appear in a - separate "Variables" list within the "My Blueprint" panel. Variables store - data values, while Components are physical/visual parts of the actor. - - Variable 1: - - Name: "MoveSpeed" - - Type: Float (click dropdown next to variable, select Float) - - Click "eye" icon to make it public/editable - - Compile the blueprint (Click on "Compile" third option from left or cimply Ctrl + Alt) - - In Details panel, set Default Value: 750.0 - - Variable 2: - - Name: "BoundsMin" - - Type: Vector 2D (search for "Vector 2D" in type dropdown) - - Default Value: X=-850, Y=-450 - - Variable 3: - - Name: "BoundsMax" - - Type: Vector 2D - - Default Value: X=850, Y=450 - - Variable 4: - - Name: "FireInterval" - - Type: Float - - Default Value: 0.08 - - Variable 5: - - Name: "FireTimer" - - Type: Float - - Default Value: 0.0 - - Variable 6: - - Name: "BulletSpeed" - - Type: Float - - Default Value: 2200.0 - - Variable 7: - - Name: "MaxLives" - - Type: Integer - - Default Value: 3 - - Variable 8: - - Name: "CurrentLives" - - Type: Integer - - Default Value: 3 - - Variable 9: - - Name: "VolleySize" - - Type: Integer - - Default Value: 3 - - Variable 10: - - Name: "VolleySpread" - - Type: Float - - Default Value: 12.0 - - Variable 11: - - Name: "SpecialUsed" - - Type: Boolean - - Default Value: false (unchecked) - - Variable 12: - - Name: "BulletClass" - - Type: In the type dropdown, go to "Object Types" category → "Actor" → - select "Class Reference" (the subcategory under Actor) - - This will hold reference to bullet blueprint class for spawning - -5. Click "Compile" button (top left, blue checkmark icon) -6. Click "Save" button (floppy disk icon next to Compile) - -EXPECTED RESULT: My Blueprint panel shows: -- All 12 variables you created listed under "Variables" -- A "Components" category with 4 component references (DefaultSceneRoot, - PlayerSprite, PlayerCollision, Arrow) - these are automatically created - from the components you added in Step 2.2 - - -STEP 2.4: Set Up Enhanced Input System --------------------------------------------------------------------------------- -UE5 uses the Enhanced Input System. Before creating movement logic in our -Blueprint, we first need to create Input Actions and an Input Mapping Context. - -0. ENABLE ENHANCED INPUT IN PROJECT SETTINGS (CRITICAL): - - Before creating any input assets, you MUST configure the project to use - Enhanced Input as the default input system: - - 1. Go to Edit → Project Settings - 2. In the left sidebar, scroll down to "Engine" section - 3. Click on "Input" - 4. Find "Default Classes" section (near the top) - 5. Set "Default Player Input Class" to: EnhancedPlayerInput - 6. Set "Default Input Component Class" to: EnhancedInputComponent - 7. Close Project Settings - - WARNING: If you skip this step, WASD controls will NOT work even if - everything else is set up correctly! - -1. SET UP INPUT ASSETS (in Content Drawer): - - A) CREATE INPUT ACTIONS: - - 1. Open Content Drawer (Ctrl+Space or click "Content Drawer" at bottom) - 2. In Content folder, right-click → New Folder → name it "Input" - - 3. Inside Input folder, right-click → Input → Input Action - - Name it "IA_Move" - - Double-click to open - - Set "Value Type" to "Axis2D (Vector2D)" - - Save and close - - 4. Create another Input Action: "IA_Fire" - - Set "Value Type" to "Digital (bool)" (default) - - 5. Create another Input Action: "IA_Special" - - Set "Value Type" to "Digital (bool)" - - B) CREATE INPUT MAPPING CONTEXT: - - 1. In Input folder, right-click → Input → Input Mapping Context - - Name it "IMC_Default" - - Double-click to open - - 2. Click "+" next to "Mappings" to add IA_Move: - - Select "IA_Move" from dropdown - - Click "+" under IA_Move to add key bindings: - - NOTE: In Unreal's top-down view, X axis = up/down, Y axis = left/right - - For W key (move UP on screen): - - Click "+", select "W" - - No modifiers needed (outputs X positive = up) - - TOTAL MODIFIERS FOR W: 0 - - For S key (move DOWN on screen): - - Click "+", select "S" - - Click "+" next to "Modifiers" → Add "Negate" - - TOTAL MODIFIERS FOR S: 1 (Negate only) - - For A key (move LEFT on screen): - - Click "+", select "A" - - Click "+" next to "Modifiers" → Add "Swizzle Input Axis Values" → Order: "YXZ" - - Click "+" next to "Modifiers" AGAIN → Add "Negate" - - TOTAL MODIFIERS FOR A: 2 (Swizzle AND Negate) - - For D key (move RIGHT on screen): - - Click "+", select "D" - - Click "+" next to "Modifiers" → Add "Swizzle Input Axis Values" → Order: "YXZ" - - TOTAL MODIFIERS FOR D: 1 (Swizzle only) - - SUMMARY TABLE - verify your IMC_Default matches this: - ┌─────┬────────────────────────────────────────────┬─────────────────┐ - │ Key │ Modifiers │ Output Vector │ - ├─────┼────────────────────────────────────────────┼─────────────────┤ - │ W │ (none) │ (1, 0) = UP │ - │ S │ Negate │ (-1, 0) = DOWN │ - │ A │ Swizzle Input Axis Values (YXZ) + Negate │ (0, -1) = LEFT │ - │ D │ Swizzle Input Axis Values (YXZ) │ (0, 1) = RIGHT │ - └─────┴────────────────────────────────────────────┴─────────────────┘ - - After adding all keys, press Ctrl+S to SAVE IMC_Default! - - 3. Click "+" to add IA_Fire: - - Select "IA_Fire" - - Add key: "Z" - - Add key: "Left Mouse Button" - - 4. Click "+" to add IA_Special: - - Select "IA_Special" - - Add key: "X" - - Add key: "Right Mouse Button" - - 5. Save the Input Mapping Context - -2. NOW OPEN BP_PLAYER BLUEPRINT: - - 1. In Content Drawer, navigate to Content → Blueprints - 2. Double-click "BP_Player" to open the Blueprint Editor - 3. Click on "Event Graph" tab (above the main view, third tab from left, - next to Construction Script) - - You should see three default events (red nodes): - - Event BeginPlay - - Event ActorBeginOverlap - - Event Tick - -3. ADD MAPPING CONTEXT IN BEGINPLAY: - - From "Event BeginPlay" node: - - 1. Right-click in empty space → search "Get Player Controller" → add it - - Use the one under "Game" → "Player" category - - It has a "Player Index" input (default 0) and "Return Value" output - 2. From the blue "Return Value" output, drag and search "Get Enhanced Input Local Player Subsystem" - 3. From that blue output, drag and search "Add Mapping Context" - 4. For "Mapping Context" input: - - Click dropdown and select "IMC_Default" - - Or drag IMC_Default from Content Drawer - 5. Set "Priority" to 0 - - 6. CRITICAL - Connect the EXECUTION wire (white wire): - - The WHITE TRIANGLE pins are different from the BLUE CIRCLE pins! - - Blue circles = DATA (what values to use) - - White triangles = EXECUTION (when to run) - - - On "Event BeginPlay", find the WHITE TRIANGLE on the RIGHT side - - On "Add Mapping Context", find the WHITE TRIANGLE on the LEFT side - - Click and DRAG from BeginPlay's white triangle to Add Mapping Context's white triangle - - WITHOUT THIS WHITE WIRE, THE CODE NEVER RUNS! - - YOUR GRAPH SHOULD LOOK LIKE THIS: - - ┌──────────────────┐ ┌─────────────────────────────────────┐ - │ Event BeginPlay │ │ Add Mapping Context │ - │ ├────────►│► │ - │ │ WHITE │ Target ○────────────────┐ │ - └──────────────────┘ WIRE │ Mapping Context: IMC_Default │ - │ Priority: 0 │ - └─────────────────────────────────────┘ - ▲ - ┌──────────────────┐ │ - │Get Player │ ┌─────────────────────────┐ - │Controller ├───►│ Enhanced Input Local │ - │ Player Index [0] │ │ Player Subsystem ├───┘ - └──────────────────┘ └─────────────────────────┘ - (blue data wires) - - VERIFY: You must have BOTH: - - Blue data wires connecting the nodes (passes the subsystem reference) - - White execution wire from Event BeginPlay to Add Mapping Context (makes it run) - -4. INPUT DEBUG TEST (verify input works - separate from movement logic): - - Before creating movement, let's verify input is working with a separate - debug setup. This debug logic is COMPLETELY INDEPENDENT from movement - - you can delete it later without affecting anything. - - a) Create debug variables (these are ONLY for debugging): - - In My Blueprint panel → Variables → click "+" - - Name: "DEBUG_LastMoveInput" - - Type: Vector 2D - - Default Value: (0, 0) - - Compile to save - - b) Create a SEPARATE debug event using a Custom Event: - - Right-click in empty space → search "Custom Event" → add it - - Name it "DEBUG_PrintInput" - - This keeps debug logic completely isolated - - c) Build the debug logic from DEBUG_PrintInput: - - 1. Get the current input value: - - Right-click → "Get Player Controller" - - Drag → "Get Enhanced Input Local Player Subsystem" - - Drag → search "IA_Move" (under Input → Enhanced Action Values) - - 2. Compare to last value (only print on change): - - Right-click → "Not Equal (Vector2D)" - - Connect IA_Move to TOP input - - "Get DEBUG_LastMoveInput" → connect to BOTTOM input - - 3. Branch on change: - - From DEBUG_PrintInput WHITE pin → "Branch" - - Connect != result to Condition - - 4. On TRUE (changed): - - "Set DEBUG_LastMoveInput" = IA_Move value - - Then → "Print String" with IA_Move value - - d) Call the debug event from Event Tick: - - From "Event Tick" WHITE pin → search "DEBUG_PrintInput" - - This adds a node that calls your custom event - - VISUAL (two separate areas in your graph): - - AREA 1 - Event Tick (will later have movement): - ┌─────────────┐ ┌─────────────────────┐ - │ Event Tick │─────►│ DEBUG_PrintInput │ - └─────────────┘ │ (call custom event) │ - └─────────────────────┘ - - AREA 2 - Debug logic (completely separate): - ┌─────────────────────┐ - │ DEBUG_PrintInput │ - │ (Custom Event) │ - └──────────┬──────────┘ - │ - ▼ - ┌─────────────────────────────┐ - │ Branch │ - │ Condition: IA_Move != Last │ - └──────┬──────────────────────┘ - │ TRUE - ▼ - ┌──────────────────┐ ┌──────────────┐ - │Set DEBUG_LastMove│────►│ Print String │ - └──────────────────┘ └──────────────┘ - - e) Compile and Save - f) Drag BP_Player into level, Press Play (Alt+P) - g) IMPORTANT: Click inside the game viewport to give it keyboard focus! - h) Press WASD keys and look at top-left of screen - - EXPECTED: Message appears ONLY when you press or release a key: - - Press W: prints "X=1.0 Y=0.0" - - Release W: prints "X=0.0 Y=0.0" - - Log is NOT spammed every frame - - TO DISABLE DEBUG LATER: Simply delete the "DEBUG_PrintInput" call node - from Event Tick. The Custom Event and its logic can stay (unused) or be - deleted entirely - movement will be unaffected either way. - - IF YOU SEE "X=0.0 Y=0.0" ALWAYS (no change when pressing WASD): - - DEBUGGING STEP 1 - Verify the pawn is possessed: - - When playing, look at the Outliner (right panel) - - Find BP_Player in the list - - If it shows a small controller icon next to it, it's possessed - - If NOT possessed: Check that "Auto Possess Player" = "Player 0" in BP_Player - - DEBUGGING STEP 2 - Verify the game window has keyboard focus: - - After pressing Play, CLICK INSIDE THE GAME VIEWPORT - - The viewport must have focus to receive keyboard input - - Try pressing Shift+F1 to release mouse, then click viewport again - - Then press WASD again - - DEBUGGING STEP 3 - Test with a simpler Print String: - - Delete the current Print String setup - - From Event Tick, drag → Print String - - Type "Tick is running" in the In String field (just plain text) - - Play the game - you should see "Tick is running" spam in top-left - - If you DON'T see this: The blueprint isn't running at all - - DEBUGGING STEP 4 - Test BeginPlay is running: - - From Event BeginPlay, AFTER the Add Mapping Context node - - Add another Print String with text "BeginPlay executed" - - Connect the white wire: BeginPlay → Add Mapping Context → Print String - - Play the game - you should see "BeginPlay executed" once at start - - If you DON'T see this: BeginPlay isn't running (pawn not spawned?) - - DEBUGGING STEP 5 - Check if there are MULTIPLE BP_Player in level: - - Look in Outliner for multiple BP_Player instances - - Delete ALL of them - - Drag in exactly ONE fresh BP_Player from Content Drawer - - Try again - - DEBUGGING STEP 6 - Nuclear option (create fresh test): - - Create a NEW blueprint: "BP_InputTest" (Pawn class) - - Add ONLY: Auto Possess Player = Player 0 - - In Event Graph: Event BeginPlay → Add Mapping Context (IMC_Default) - - In Event Graph: Event Tick → Print String (connected to Get IA_Move) - - Drag this minimal test blueprint into level - - If THIS works, something is wrong with your BP_Player - - IF THIS TEST WORKS: Continue to step 5. You can optionally delete the - DEBUG_PrintInput call from Event Tick to stop the debug prints. - IF THIS TEST FAILS: Work through debugging steps above. - -5. MOVEMENT LOGIC - Create this node network: - - NOTE: If you added debug from step 4, Event Tick already has a wire to - DEBUG_PrintInput. To have BOTH debug AND movement run from Event Tick, - you need a Sequence node (one output pin = one wire only). - - a) INSERT A SEQUENCE NODE between Event Tick and debug: - - Delete the wire from Event Tick to DEBUG_PrintInput - - Right-click → search "Sequence" → add it - - Connect Event Tick → Sequence (input) - - Connect Sequence "Then 0" → DEBUG_PrintInput - - Sequence "Then 1" will be used for movement - - RESULT: - Event Tick → Sequence ─┬─ Then 0 → DEBUG_PrintInput - │ - └─ Then 1 → (movement, next steps) - - If you skipped debug, just connect Event Tick directly to movement. - - b) Starting point for movement: - - If using Sequence: drag from "Then 1" output - - If no debug: drag from Event Tick directly - - b) Right-click → search "Get Player Controller" and add it - (This is a NEW Get Player Controller - separate from the one in debug) - - c) From Player Controller output, drag → "Get Enhanced Input Local Player Subsystem" - - c) From subsystem output, drag and right-click → search "IA_Move" - - Look under "Input" → "Enhanced Action Values" → select "IA_Move" - - This node returns the current value of the IA_Move input action - - d) The output is an Input Action Value (Vector2D since we set IA_Move to Axis2D). - Right-click the output pin → "Split Struct Pin" to get X and Y components, - OR drag from output and search "To Vector 2D" to convert it - - e) Right-click → "Make Vector" - - Connect the X from movement input to X - - Connect the Y from movement input to Y - - Set Z to 0 - - f) Right-click → "Normalize" - - Connect the vector output to Normalize input - - g) Right-click → "Get World Delta Seconds" - - h) Right-click → "Get MoveSpeed" (your variable) - - i) Right-click → Multiply (float * float) - - Connect Delta Seconds output to first input - - Connect MoveSpeed output to second input - - j) Right-click → Multiply (vector * float) - - Connect Normalized vector (from step f) to vector input - - Connect (DeltaSeconds * MoveSpeed) result (from step i) to float input - - This output is the "movement delta" - how far to move this frame - - k) Right-click → "Get Actor Location" - - l) Right-click → Add (vector + vector) - - Connect current location (from step k) to first input - - Connect movement delta (from step j) to second input - - m) CLAMP X COORDINATE: - First, break apart the new position vector from step l: - - Right-click on the output pin of the Add node (from step l) → "Split Struct Pin" - - This splits the vector into three separate pins: X, Y, Z - - Now clamp the X value: - - Right-click in empty space → search "Clamp (float)" → add it - - Connect the "X" output (from the split Add node) to "Value" input of Clamp - - Get the min bound: - - Right-click → search "Get BoundsMin" (your variable) → add it - - Right-click on BoundsMin output pin → "Split Struct Pin" (splits into X, Y) - - Connect BoundsMin's "X" to Clamp's "Min" input - - Get the max bound: - - Right-click → search "Get BoundsMax" (your variable) → add it - - Right-click on BoundsMax output pin → "Split Struct Pin" - - Connect BoundsMax's "X" to Clamp's "Max" input - - The Clamp node now outputs the X position clamped within bounds. - - n) CLAMP Y COORDINATE: - - Right-click → add another "Clamp (float)" node - - Connect the "Y" output (from the split Add node in step m) to "Value" - - Connect BoundsMin's "Y" (already split) to "Min" - - Connect BoundsMax's "Y" (already split) to "Max" - - o) Right-click → "Make Vector" - - Connect clamped X - - Connect clamped Y - - Set Z to 0 - - p) Right-click → "Set Actor Location" - - Connect the clamped vector to "New Location" - - q) CRITICAL - Connect the execution wire (white wire): - - If using Sequence (from step a): drag from "Then 1" to Set Actor Location - - If no debug: drag from Event Tick to Set Actor Location - - Without this execution wire, the movement code NEVER runs! - - FINAL STRUCTURE (with debug - using Sequence node): - Event Tick → Sequence ─┬─ Then 0 → DEBUG_PrintInput - │ - └─ Then 1 → [movement] → Set Actor Location - - FINAL STRUCTURE (without debug): - Event Tick → [movement logic] → Set Actor Location - - TO REMOVE DEBUG: Delete the wire from "Then 0" to DEBUG_PrintInput. - Movement on "Then 1" is completely unaffected. - -6. Click Compile and Save - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark (no errors) -- No warnings about unconnected pins - -HOW TO TEST AT THIS STAGE: -1. Open any level (or the default "Untitled" level) -2. From Content Drawer, drag BP_Player into the viewport -3. Press Play (Alt+P) -4. Click inside the game viewport to give it keyboard focus -5. You should see the cube (TempVisual added in Step 2.2) and control it with WASD - -EXPECTED RESULT when tested: -- Player pawn moves smoothly when pressing WASD keys -- Movement is frame-rate independent (consistent speed) -- Player cannot move outside the screen bounds (stops at edges) -- Releasing keys immediately stops movement (no drift) - -NOTE: Full game testing will be possible after completing Part 8 (Level Setup). - - -VISUAL DIAGRAM OF MOVEMENT NODES: -┌─────────────┐ ┌──────────┐ -│ Event Tick │────►│ Sequence │─┬─ Then 0 ──► DEBUG_PrintInput (optional) -└─────────────┘ └──────────┘ │ - └─ Then 1 ──► Movement logic ──► Set Actor Location - -(If no debug, skip Sequence and connect Event Tick directly to movement) - -┌──────────────────────────────────┐ ┌────────────────────┐ -│ Get Enhanced Input Subsystem │ │ Set Actor Location │ -│ → Get Action Value (IA_Move) │ └────────────────────┘ -│ → Get as Vector2D │ ▲ -└────────────────┬─────────────────┘ │ - │ ┌───────────────┐ - ▼ │ Clamped Vector│ - ┌────────────────────────────┐ └───────────────┘ - │ Make Vector │───►Normalize───► ▲ - │ X=Input.X, Y=Input.Y, Z=0 │ (direction) │ - └────────────────────────────┘ │ ┌─────────────┐ - ▼ │ + Location │ - ┌──────────────┐ └─────────────┘ - │ * MoveSpeed │─────────┘ - │ * DeltaTime │ - └──────────────┘ - - -STEP 2.5: Create Player Firing Logic --------------------------------------------------------------------------------- -1. Continue in Event Graph, we'll add firing after movement - -2. After the movement logic, add firing check using Enhanced Input: - - a) Right-click → search "IA_Fire" - - Look under "Input" → "Enhanced Action Events" category (diamond ◇ icons) - - Select "IA_Fire" (the one with diamond icon, NOT the square icon) - - This creates an EVENT node (red, like Event Tick) that fires when the button is pressed - - NOTE: "Enhanced Action Events" (diamond ◇) = triggers when button pressed - "Enhanced Action Values" (square □) = reads current value continuously - For firing, we want the EVENT. - - b) From the "Triggered" execution pin, build the fire rate limiter: - - 1. Get current timer value: - - Right-click → "Get FireTimer" (your variable) - - 2. Subtract frame time from it: - - Right-click → "Get World Delta Seconds" (time since last frame, ~0.016 at 60fps) - - Right-click → search "float - float" or "Subtract" → add Subtract node - - Connect FireTimer to the TOP input (A) - - Connect Delta Seconds to the BOTTOM input (B) - - Result = FireTimer minus DeltaSeconds - - 3. Store the new timer value: - - Right-click → "Set FireTimer" - - Connect the Subtract result to it - - Connect execution wire: Triggered → Set FireTimer - - 4. Check if we can fire: - - From Set FireTimer, drag execution → "Branch" - - Right-click → "<= (float)" (less than or equal) - - Connect FireTimer (use the output from Set, or Get it again) to top input - - Type 0 in bottom input - - Connect result to Branch's Condition - - 5. On TRUE branch (timer expired, can fire): - - Right-click → "Set FireTimer" - - Right-click → "Get FireInterval" (your variable) - - Connect FireInterval to Set FireTimer (resets the cooldown) - - For now, add a Print String with text "FIRE!" to verify it works - - Leave the execution wire after this Set FireTimer OPEN - (we'll connect FireVolley function here in step 3) - - VISUAL: - ┌──────────────────┐ - │ IA_Fire Triggered│ - └────────┬─────────┘ - │ (white wire) - ▼ - ┌─────────────────────────────────────┐ - │ Set FireTimer │ - │ = FireTimer - GetWorldDeltaSeconds │ - └────────┬────────────────────────────┘ - │ - ▼ - ┌─────────────────┐ TRUE ┌─────────────────────┐ - │ Branch │───────────►│ Set FireTimer │──► Print "FIRE!" - │ FireTimer <= 0 │ │ = FireInterval │ (placeholder) - └────────┬────────┘ └─────────────────────┘ - │ FALSE - ▼ - (do nothing) - - TEST NOW: Press Play, hold Z key. You should see "FIRE!" spam in top-left - at rapid intervals (every 0.08 seconds = FireInterval). - - NOTE: Since IA_Fire was set up with Z key and Left Mouse Button in the - Input Mapping Context, both inputs will trigger this action automatically. - -3. 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: - - - Get VolleySize variable - - Branch: if VolleySize == 1, fire single bullet straight up - - Else: loop from 0 to VolleySize-1 - - Calculate angle: StartAngle + (i * VolleySpread) - - StartAngle = -(VolleySpread * (VolleySize-1)) / 2 - - Create direction vector from angle - - Spawn bullet at player location with that direction - -4. CREATE SPAWN BULLET FUNCTION: - - a) Add new function "SpawnBullet" - - b) Add input parameters (click + on function entry): - - SpawnLocation (Vector) - - Direction (Vector) - - c) Inside function: - - Right-click → "Spawn Actor from Class" - - Connect BulletClass variable to "Class" input - - Connect SpawnLocation to "Spawn Transform Location" - - d) After spawning: - - Cast the return value to BP_Bullet (we'll create this later) - - Call "Initialize" function on the bullet - - Pass Direction and BulletSpeed, IsEnemy=false - -5. 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: -- NOTE: Bullets won't spawn yet because BP_Bullet doesn't exist -- After BP_Bullet is created (Part 3), pressing Z or Left Mouse Button will: - - Spawn 3 bullets in a spread pattern (VolleySize=3) - - Bullets travel upward from player position - - Rapid fire when holding the button (every 0.08 seconds) - - -STEP 2.6: Create Player Damage and Special Ability --------------------------------------------------------------------------------- -1. CREATE "TakeHit" FUNCTION: - - a) Add new function "TakeHit" - b) Add input parameter: Damage (Integer) - - c) Inside: - - Get CurrentLives - - Subtract Damage - - Clamp to minimum 0 - - Set CurrentLives - - If CurrentLives <= 0: - - Call HandleDeath function - -2. CREATE "HandleDeath" FUNCTION: - - a) Add new function "HandleDeath" - - b) Inside: - - Get reference to GameDirector (we'll create later) - - Call "HandlePlayerDeath" on GameDirector - - Set Actor Hidden in Game = true - - Disable Input - -3. SPECIAL ABILITY (Screen Clear): - - Using Enhanced Input (IA_Special was already set up with X and Right Mouse): - - a) Right-click → search "EnhancedInputAction IA_Special" - - This creates an event node that fires when X or Right Mouse is pressed - - b) From the "Triggered" execution pin: - - Get SpecialUsed variable - - Branch: if SpecialUsed is false: - - Set SpecialUsed = true - - Call "ClearAllEnemies" (global function) - - Call "ClearAllEnemyBullets" (global function) - -4. Compile and Save - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "TakeHit" and "HandleDeath" functions appear under Functions -- Event Graph shows special ability logic connected to Event Tick - -EXPECTED RESULT in Play mode (after all parts are complete): -- When enemy bullet hits player: Lives decrease by 1 -- When Lives reach 0: Player disappears, game shows "Game Over" -- Pressing X or Right Mouse Button (once per game): - - All enemies on screen are destroyed - - All enemy bullets are destroyed - - Second press does nothing (SpecialUsed = true) - - -================================================================================ -PART 3: CREATE THE BULLET -================================================================================ - -STEP 3.1: Create Bullet Blueprint --------------------------------------------------------------------------------- -1. In Content Browser → Blueprints folder -2. Right-click → Blueprint Class → Actor -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: - - TravelDirection (Vector) - Default: (0, 1, 0) - - TravelSpeed (Float) - Default: 1200.0 - - RemainingLifetime (Float) - Default: 4.0 - - IsEnemyProjectile (Boolean) - Default: false - - Damage (Integer) - Default: 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: - - Get TravelDirection - - Multiply by TravelSpeed - - Multiply by Delta Seconds - - Add to current location - - Set Actor Location - - b) Check lifetime: - - Subtract Delta Seconds from RemainingLifetime - - If <= 0: Destroy Actor - -2. CREATE "Initialize" FUNCTION: - - a) Add input parameters: - - Direction (Vector) - - Speed (Float) - - bIsEnemy (Boolean) - - Lifetime (Float) - - DamageValue (Integer) - default 1 - - b) Inside: - - Normalize Direction → Set TravelDirection - - Set TravelSpeed = Speed - - Set IsEnemyProjectile = bIsEnemy - - Set RemainingLifetime = Lifetime - - Set Damage = DamageValue - -3. Compile and Save - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "Initialize" function appears under Functions with 5 input parameters - -EXPECTED RESULT in Play mode (when spawned by player): -- Bullets move in their assigned direction at TravelSpeed -- Bullets automatically destroy after RemainingLifetime seconds (default 4s) -- Movement is smooth and frame-rate independent - - -STEP 3.3: Bullet Collision Logic --------------------------------------------------------------------------------- -1. Click on "BulletCollision" component in Components panel - -2. In Details panel, scroll to "Events" section - - Click "+" next to "On Component Begin Overlap" - - This creates event node in Event Graph - -3. In the Event Graph, from "On Component Begin Overlap": - - a) First, check if this is enemy projectile: - - Get IsEnemyProjectile - - Branch - - b) IF IS ENEMY PROJECTILE (True branch): - - Get Other Actor from the overlap event - - Cast to BP_Player - - If cast succeeds: - - Call TakeHit on player, passing Damage - - Destroy this bullet (Destroy Actor) - - c) IF IS PLAYER BULLET (False branch): - - Get Other Actor - - Cast to BP_Enemy - - If cast succeeds: - - Call ApplyDamage on enemy, passing Damage - - Destroy this bullet - -4. Compile and Save - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Event Graph shows "On Component Begin Overlap" event connected to Branch - -EXPECTED RESULT in Play mode: -- Player bullets (IsEnemyProjectile=false) hitting enemies: - - Enemy takes damage, bullet disappears -- Enemy bullets (IsEnemyProjectile=true) hitting player: - - Player takes hit, bullet disappears -- Bullets passing through other objects: No effect (bullets ignore non-targets) - - -================================================================================ -PART 4: CREATE THE ENEMY -================================================================================ - -STEP 4.1: Create Enemy Blueprint --------------------------------------------------------------------------------- -1. Content Browser → Blueprints -2. Right-click → Blueprint Class → Actor -3. Name: "BP_Enemy" -4. Double-click to open - -5. Add Components: - - Paper Sprite → "EnemySprite" - - Box Collision → "EnemyCollision" - - Box Extent: X=30, Y=30, Z=10 - - Generate Overlap Events: CHECKED - - Cube (Static Mesh) → name "TempVisual" - - Scale: (0.6, 0.6, 0.1) - larger than player cube - - This provides visibility until proper visuals in Part 9 - -6. Create Variables: - - MaxHealth (Integer) - Default: 12 - - CurrentHealth (Integer) - Default: 12 - - ScoreValue (Integer) - Default: 50 - - VerticalSpeed (Float) - Default: 220.0 - - HorizontalAmplitude (Float) - Default: 250.0 - - HorizontalFrequency (Float) - Default: 1.8 - - DespawnY (Float) - Default: -750.0 - - FireInterval (Float) - Default: 0.35 - - BulletsPerBurst (Integer) - Default: 20 - - BurstSpread (Float) - Default: 360.0 - - EnemyBulletSpeed (Float) - Default: 1000.0 - - EnemyBulletLifetime (Float) - Default: 6.0 - - BaseX (Float) - Default: 0.0 - - WaveSeed (Float) - Default: 0.0 - - FireTimer (Float) - Default: 0.0 - - BulletClass (Class Reference to Actor) - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Components panel shows: DefaultSceneRoot → EnemySprite, EnemyCollision, TempVisual -- Variables panel shows all 16 variables with correct types and defaults - -EXPECTED RESULT in Viewport (Blueprint Editor): -- Cube visible (the TempVisual placeholder, larger than player) -- Box collision visible (30x30x10) - - -STEP 4.2: Enemy Initialization --------------------------------------------------------------------------------- -1. In Event Graph, from "Event BeginPlay": - - a) Set CurrentHealth = MaxHealth - - b) Get Actor Location → Break Vector → Set BaseX = X value - - c) Random Float in Range (0, 6.28) → Set WaveSeed - (6.28 ≈ 2π for wave randomization) - - d) Random Float in Range (0, FireInterval) → Set FireTimer - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- BeginPlay event connected to variable setters - -EXPECTED RESULT in Play mode: -- Each enemy spawns with randomized WaveSeed (0 to 6.28) -- Each enemy starts with different FireTimer offset -- This creates varied, non-synchronized enemy behavior - - -STEP 4.3: Enemy Movement Logic --------------------------------------------------------------------------------- -1. In Event Graph, from "Event Tick": - - a) VERTICAL MOVEMENT: - - Get Actor Location → Break into X, Y, Z - - Subtract (VerticalSpeed * DeltaSeconds) from Y - - b) HORIZONTAL SINE WAVE: - - Get Game Time in Seconds - - Add WaveSeed - - Multiply by HorizontalFrequency - - Calculate Sine of result - - Multiply by HorizontalAmplitude - - Add to BaseX - - This becomes new X position - - c) Set Actor Location with new X, Y (Z stays 0) - - d) DESPAWN CHECK: - - If Y < DespawnY: Destroy Actor - - If Abs(X) > 1400: Destroy Actor - -VISUAL DIAGRAM: -┌─────────────┐ -│ Event Tick │ -└──────┬──────┘ - │ - ▼ -┌─────────────────────────────────────────────┐ -│ Get Location │ -│ NewY = Y - (VerticalSpeed * DeltaSeconds) │ -│ NewX = BaseX + Sin(Time * Freq) * Amplitude │ -│ Set Location (NewX, NewY, 0) │ -└──────────────────────────────────────────────┘ - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Event Tick connected to movement and despawn logic - -EXPECTED RESULT in Play mode: -- Enemies drift downward at VerticalSpeed (220 units/sec) -- Enemies oscillate horizontally in sine wave pattern -- Each enemy has different horizontal phase (due to WaveSeed) -- Enemies disappear when Y < -750 or |X| > 1400 - - -STEP 4.4: Enemy Firing Logic --------------------------------------------------------------------------------- -1. Continue in Event Tick (after movement): - - a) Decrease FireTimer by DeltaSeconds - - b) Branch: if FireTimer <= 0: - - Reset FireTimer to FireInterval - - Call "FireBurst" function - -2. CREATE "FireBurst" FUNCTION: - - a) Add function "FireBurst" - - b) Inside: - - Get BulletsPerBurst - - Calculate angle step: 360 / BulletsPerBurst (for full circle) - OR: BurstSpread / (BulletsPerBurst - 1) (for partial arc) - - c) For Loop from 0 to BulletsPerBurst - 1: - - Calculate angle: i * AngleStep - - Convert to direction vector: - - X = Sin(angle in radians) - - Y = -Cos(angle in radians) ← negative because enemies fire DOWN - - Spawn bullet: - - Spawn Actor from Class (BP_Bullet) - - Get spawned bullet, call Initialize: - - Direction = calculated direction - - Speed = EnemyBulletSpeed - - bIsEnemy = true - - Lifetime = EnemyBulletLifetime - -VISUAL: Radial bullet pattern (20 bullets in 360°) - ↑ - ↗ | ↖ - ↗ | ↖ - →───────●───────← - ↘ | ↙ - ↘ | ↙ - ↓ - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "FireBurst" function appears under Functions - -EXPECTED RESULT in Play mode: -- Every 0.35 seconds (FireInterval), enemy fires bullet burst -- 20 bullets spawn in a full 360° circle pattern -- Bullets travel outward from enemy position -- Different enemies fire at different times (randomized FireTimer) - - -STEP 4.5: Enemy Damage and Death --------------------------------------------------------------------------------- -1. CREATE "ApplyDamage" FUNCTION: - - a) Add input: DamageAmount (Integer) - - b) Inside: - - Subtract DamageAmount from CurrentHealth - - If CurrentHealth <= 0: - - Call HandleDeath - -2. CREATE "HandleDeath" FUNCTION: - - a) Inside: - - Get reference to ScoreManager (we'll create later) - - Call AddScore, passing ScoreValue - - Spawn death effect (optional) - - Destroy Actor - -3. COLLISION WITH PLAYER: - - a) On the EnemyCollision component overlap event: - - Cast Other Actor to BP_Player - - If successful: - - Call TakeHit(1) on player - - Call HandleDeath() on self - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "ApplyDamage" and "HandleDeath" functions appear under Functions -- EnemyCollision has overlap event in Event Graph - -EXPECTED RESULT in Play mode: -- Player bullets hitting enemy: Enemy health decreases -- After 12 hits (MaxHealth): Enemy disappears, score increases by 50 -- Player colliding with enemy: Player takes 1 damage, enemy dies - - -================================================================================ -PART 5: CREATE ENEMY SPAWNER -================================================================================ - -STEP 5.1: Create Spawner Blueprint --------------------------------------------------------------------------------- -1. Content Browser → Blueprints -2. Right-click → Blueprint Class → Actor -3. Name: "BP_EnemySpawner" -4. Double-click to open - -5. Create Variables: - - EnemyClass (Class Reference) - will reference BP_Enemy - - SpawnAreaHalfWidth (Float) - Default: 900.0 - - GameDuration (Float) - Default: 300.0 (5 minutes) - - MaxSimultaneousEnemies (Integer) - Default: 120 - - ElapsedTime (Float) - Default: 0.0 - - SpawnTimer (Float) - Default: 0.0 - - SpawningActive (Boolean) - Default: true - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Variables panel shows all 7 variables with correct types -- No components needed (spawner is invisible logic actor) - - -STEP 5.2: Spawn Rate Curve --------------------------------------------------------------------------------- -1. Create Variable: - - SpawnCurve (Curve Float) - -2. To create the curve asset: - a) In Content Browser, right-click → Miscellaneous → Curve - b) Select "CurveFloat" - c) Name it "SpawnRateCurve" - d) Double-click to open Curve Editor - -3. In Curve Editor: - - Right-click on the curve → Add Key - - Create these keyframes: - - Time: 0.0, Value: 0.4 (slow spawn at start) - - Time: 0.5, Value: 2.0 (medium spawn halfway) - - Time: 1.0, Value: 4.5 (fast spawn at end) - - The X axis is normalized time (0-1) - - The Y axis is spawns per second - -4. In BP_EnemySpawner, set SpawnCurve default to this curve asset - -EXPECTED RESULT in Curve Editor: -- Curve line visible starting at (0, 0.4), rising through (0.5, 2.0), ending at (1.0, 4.5) -- Smooth interpolation between keyframes - -EXPECTED RESULT in Play mode: -- Game start: ~0.4 enemies spawn per second (slow) -- At 2.5 minutes: ~2 enemies spawn per second (medium) -- At 5 minutes: ~4.5 enemies spawn per second (intense) - - -STEP 5.3: Spawning Logic --------------------------------------------------------------------------------- -1. In Event Graph, from Event Tick: - - a) Check if SpawningActive is true - - If false, do nothing - - b) Update ElapsedTime: - - Add DeltaSeconds to ElapsedTime - - c) Calculate normalized time: - - Divide ElapsedTime by GameDuration - - Clamp between 0 and 1 - - d) Get spawn rate from curve: - - Get SpawnCurve - - Call "Get Float Value" with normalized time - - This returns spawns per second - - e) Update SpawnTimer: - - Subtract DeltaSeconds from SpawnTimer - - If SpawnTimer <= 0: - - Reset SpawnTimer to (1.0 / SpawnsPerSecond) - - Call SpawnWave function - -2. CREATE "SpawnWave" FUNCTION: - - a) Inside: - - Calculate burst size based on time: - - BaseCount = 1 + (NormalizedTime * 6) - - BurstSize = BaseCount + Random(0, 2) - - Clamp between 1 and 12 - - b) For Loop from 0 to BurstSize - 1: - - Call SpawnEnemy function - -3. CREATE "SpawnEnemy" FUNCTION: - - a) Inside: - - Check: Get All Actors of Class (BP_Enemy) - - Get array length - - If >= MaxSimultaneousEnemies: Return (don't spawn) - - b) Calculate spawn position: - - X = Random Float in Range (-SpawnAreaHalfWidth, SpawnAreaHalfWidth) - - Y = Get this actor's Y position (top of screen) - - Z = 0 - - c) Spawn Actor from Class: - - Class: EnemyClass - - Location: calculated position - - Rotation: (0, 0, 0) - -4. CREATE "StopSpawning" FUNCTION: - - Set SpawningActive = false - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "SpawnWave", "SpawnEnemy", "StopSpawning" functions appear under Functions - -EXPECTED RESULT in Play mode: -- Enemies spawn at top of screen (Y=500) at random X positions -- Spawn rate increases over time following the curve -- Maximum 120 enemies on screen at once -- When StopSpawning called: No new enemies appear - - -================================================================================ -PART 6: CREATE GAME DIRECTOR -================================================================================ - -STEP 6.1: Create Game Director Blueprint --------------------------------------------------------------------------------- -1. Content Browser → Blueprints -2. Right-click → Blueprint Class → Actor -3. Name: "BP_GameDirector" -4. Double-click to open - -5. Create Variables: - - PlayerReference (Object Reference to BP_Player) - - SpawnerReference (Object Reference to BP_EnemySpawner) - - GameDuration (Float) - Default: 300.0 - - ElapsedTime (Float) - Default: 0.0 - - GameActive (Boolean) - Default: true - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Variables panel shows all 5 variables with correct types -- No components needed (director is invisible logic actor) - - -STEP 6.2: Game Director Initialization --------------------------------------------------------------------------------- -1. From Event BeginPlay: - - a) Find player in scene: - - Get All Actors of Class → BP_Player - - Get first element (index 0) - - Set PlayerReference - - b) Find spawner in scene: - - Get All Actors of Class → BP_EnemySpawner - - Get first element - - Set SpawnerReference - - c) Initialize ScoreManager: - - Get ScoreManager reference - - Call RegisterGameStart with initial lives and duration - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- BeginPlay event connected to "Get All Actors of Class" nodes - -EXPECTED RESULT in Play mode: -- Game Director automatically finds Player, Spawner, and ScoreManager -- UI initializes with correct starting values (Lives: 3, Time: 05:00) - - -STEP 6.3: Game Director Update Logic --------------------------------------------------------------------------------- -1. From Event Tick: - - a) Check if GameActive - - If false, skip everything - - b) Update elapsed time: - - Add DeltaSeconds to ElapsedTime - - c) Calculate remaining time: - - Subtract ElapsedTime from GameDuration - - Max with 0 (don't go negative) - - d) Update UI timer: - - Get ScoreManager - - Call UpdateTimer with remaining time - - e) Check for victory: - - If ElapsedTime >= GameDuration: - - Set GameActive = false - - Get SpawnerReference → Call StopSpawning - - Get ScoreManager → Call HandleGameClear - -2. CREATE "HandlePlayerDeath" FUNCTION: - - a) Inside: - - Set GameActive = false - - Get SpawnerReference → Call StopSpawning - - Get ScoreManager → Call HandleGameOver - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- "HandlePlayerDeath" function appears under Functions -- Event Tick connected to timer update and victory check - -EXPECTED RESULT in Play mode: -- Timer counts down from 05:00 to 00:00 -- At 00:00: "Mission Complete" appears, enemies stop spawning -- When player dies: "Game Over" appears, enemies stop spawning - - -================================================================================ -PART 7: CREATE SCORE MANAGER / UI -================================================================================ - -STEP 7.1: Create UI Widget Blueprint --------------------------------------------------------------------------------- -1. Content Browser → UI folder -2. Right-click → User Interface → Widget Blueprint -3. Name: "WBP_HUD" -4. Double-click to open Widget Designer - -EXPECTED RESULT: Widget Designer opens with: -- Hierarchy panel on left -- Canvas preview in center -- Details panel on right - - -STEP 7.2: Design HUD Layout --------------------------------------------------------------------------------- -1. In the Palette panel (left side), search for "Canvas Panel" - - Drag Canvas Panel to the Hierarchy (if not already there) - -2. Add Score Text: - a) In Palette, search for "Text" - b) Drag "Text" widget onto Canvas Panel in hierarchy - c) Rename it "ScoreText" (right-click → Rename) - d) In Details panel: - - Under "Slot (Canvas Panel Slot)": - - Anchors: Top-Left (click dropdown, select corner) - - Position X: 20 - - Position Y: 20 - - Size X: 300 - - Size Y: 40 - - Under "Appearance": - - Text: "Score: 0" - - Font Size: 24 - - Color: White - -3. Add Lives Text: - a) Drag another "Text" widget - b) Rename to "LivesText" - c) Position X: 20, Y: 60 - d) Text: "Lives: 3" - e) Same font settings as Score - -4. Add Timer Text: - a) Drag another "Text" widget - b) Rename to "TimerText" - c) Position X: 20, Y: 100 - d) Text: "Time: 05:00" - e) Same font settings - -5. Click "Compile" and "Save" (top buttons) - -EXPECTED RESULT: Preview shows: -┌────────────────────────────────────┐ -│ Score: 0 │ -│ Lives: 3 │ -│ Time: 05:00 │ -│ │ -│ │ -└────────────────────────────────────┘ - - -STEP 7.3: Create Score Manager Blueprint --------------------------------------------------------------------------------- -1. Content Browser → Blueprints -2. Right-click → Blueprint Class → Actor -3. Name: "BP_ScoreManager" -4. Double-click to open - -5. Create Variables: - - Score (Integer) - Default: 0 - - CurrentLives (Integer) - Default: 3 - - HUDWidget (Object Reference to WBP_HUD) - - HUDWidgetClass (Class Reference) - set to WBP_HUD - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Variables panel shows all 4 variables with correct types - - -STEP 7.4: Score Manager Initialization --------------------------------------------------------------------------------- -1. From Event BeginPlay: - - a) Create HUD Widget: - - Right-click → "Create Widget" - - Class: Select WBP_HUD (or use HUDWidgetClass variable) - - Owning Player: Get Player Controller (index 0) - - b) Store widget reference: - - Set HUDWidget to the created widget - - c) Add to viewport: - - Right-click → "Add to Viewport" - - Connect widget reference as target - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- BeginPlay event connected to Create Widget → Add to Viewport - -EXPECTED RESULT in Play mode: -- HUD appears immediately when game starts -- HUD displays in top-left corner of screen -- Text is visible and readable (white on game background) - - -STEP 7.5: Score Manager Functions --------------------------------------------------------------------------------- -1. CREATE "RegisterGameStart" FUNCTION: - - Inputs: InitialLives (Integer), Duration (Float) - - Set Score = 0 - - Set CurrentLives = InitialLives - - Update all UI labels - -2. CREATE "AddScore" FUNCTION: - - Input: Amount (Integer) - - Add Amount to Score - - Update Score label in HUD: - - Get HUDWidget - - Cast to WBP_HUD - - Get "ScoreText" widget - - Set Text to "Score: " + Score - -3. CREATE "SetLives" FUNCTION: - - Input: Lives (Integer) - - Set CurrentLives = Lives - - Update Lives label in HUD - -4. CREATE "UpdateTimer" FUNCTION: - - Input: TimeRemaining (Float) - - Convert to minutes:seconds format: - - Minutes = Floor(TimeRemaining / 60) - - Seconds = Floor(TimeRemaining mod 60) - - Format string: "Time: MM:SS" - - Update Timer label - -5. CREATE "HandleGameOver" FUNCTION: - - Set Timer text to "Game Over" - -6. CREATE "HandleGameClear" FUNCTION: - - Set Timer text to "Mission Complete" - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- All 6 functions appear under Functions panel - -EXPECTED RESULT in Play mode: -- Score updates instantly when enemies are killed (+50 each) -- Lives display updates when player is hit -- Timer counts down in MM:SS format -- "Game Over" or "Mission Complete" replaces timer at game end - - -================================================================================ -PART 8: CREATE GAME MODE AND LEVEL -================================================================================ - -STEP 8.1: Create Custom Game Mode --------------------------------------------------------------------------------- -1. Content Browser → Blueprints -2. Right-click → Blueprint Class -3. In popup, expand "All Classes" -4. Search for "Game Mode Base" -5. Select "Game Mode Base" and click Select -6. Name: "BP_BulletHellGameMode" -7. Double-click to open - -8. In the Details panel (with BP_BulletHellGameMode open): - - Find "Classes" section - - Default Pawn Class: Select "BP_Player" - - Player Controller Class: Keep default - -EXPECTED RESULT after Compile: -- Compile button shows GREEN checkmark -- Details panel shows "Default Pawn Class" set to BP_Player - - -STEP 8.2: Configure Project to Use Game Mode --------------------------------------------------------------------------------- -1. Go to Edit → Project Settings -2. In left sidebar, click "Maps & Modes" -3. Under "Default Modes": - - Default GameMode: Select BP_BulletHellGameMode -4. Close Project Settings - -EXPECTED RESULT: -- Project Settings shows BP_BulletHellGameMode as Default GameMode -- This means the game will automatically spawn BP_Player when Play is pressed - - -STEP 8.3: Create Game Level --------------------------------------------------------------------------------- -1. File → New Level -2. Select "Empty Level" -3. File → Save Current Level As -4. Navigate to Content folder -5. Name: "BulletHellLevel" -6. Click Save - -EXPECTED RESULT: -- New level file "BulletHellLevel" appears in Content folder -- Level is completely empty (black viewport) -- Outliner shows only default actors (if any) - - -STEP 8.4: Set Up Level Components --------------------------------------------------------------------------------- -1. In the level (main viewport), we need to add game actors: - -2. ADD CAMERA: - a) In Place Actors panel (left side, or Window → Place Actors) - b) Search for "Camera Actor" - c) Drag into viewport - d) Position camera: - - In Details panel, under Transform: - - Location: X=0, Y=0, Z=1000 - - Rotation: X=-90, Y=0, Z=0 (looking down) - e) Set as default camera: - - In Details panel, find "Auto Activate for Player" - - Check the box, set Player Index to 0 - - FOR ORTHOGRAPHIC VIEW: - - Click on Camera component - - In Details, set "Projection Mode" to "Orthographic" - - Set "Ortho Width" to 1920 (or your screen width) - -3. ADD PLAYER: - a) From Content Browser, drag "BP_Player" into level - b) Position: X=0, Y=-300, Z=0 (bottom center) - -4. ADD ENEMY SPAWNER: - a) Drag "BP_EnemySpawner" into level - b) Position: X=0, Y=500, Z=0 (top of screen) - c) In Details panel, set EnemyClass to "BP_Enemy" - -5. ADD GAME DIRECTOR: - a) Drag "BP_GameDirector" into level - b) Position doesn't matter (it's invisible) - -6. ADD SCORE MANAGER: - a) Drag "BP_ScoreManager" into level - b) Position doesn't matter - -7. Save the level (Ctrl+S) - -EXPECTED RESULT in Level Viewport: -- Camera actor visible at top of scene (Z=1000) -- BP_Player visible at bottom center (Y=-300) -- BP_EnemySpawner visible at top (Y=500) -- BP_GameDirector and BP_ScoreManager in Outliner (invisible actors) - -EXPECTED RESULT in Outliner: -- CameraActor -- BP_Player -- BP_EnemySpawner -- BP_GameDirector -- BP_ScoreManager - - -STEP 8.5: Set Default Level --------------------------------------------------------------------------------- -1. Edit → Project Settings -2. Maps & Modes -3. Under "Default Maps": - - Editor Startup Map: Select BulletHellLevel - - Game Default Map: Select BulletHellLevel - -EXPECTED RESULT: -- Project Settings shows BulletHellLevel as both startup and default map -- Launching the game (standalone or in editor) loads this level automatically - - -================================================================================ -PART 9: FINAL SETUP AND TESTING -================================================================================ - -STEP 9.1: Assign Blueprint References --------------------------------------------------------------------------------- -1. Open BP_Player: - - In Details panel (with blueprint open) - - Set BulletClass: BP_Bullet - -2. Open BP_EnemySpawner: - - Set EnemyClass: BP_Enemy - -3. Open BP_Enemy: - - Set BulletClass: BP_Bullet - -4. Compile and Save all blueprints - -EXPECTED RESULT after Compile (all blueprints): -- All blueprints compile with GREEN checkmark -- No "None" or missing references in variable defaults -- BP_Player.BulletClass → BP_Bullet -- BP_Enemy.BulletClass → BP_Bullet -- BP_EnemySpawner.EnemyClass → BP_Enemy - -EXPECTED RESULT in Play mode: -- Player can shoot bullets (BP_Bullet spawns) -- Enemies spawn and shoot bullets -- All collision/damage systems functional - - -STEP 9.2: Create Final Visuals (Replace Placeholder) --------------------------------------------------------------------------------- -Now replace the temporary cube visuals with proper colored materials: - -1. REMOVE TEMPORARY COMPONENTS: - a) Open BP_Player blueprint - b) In Components panel, select "TempVisual" (the cube added in Step 2.2) - c) Press Delete to remove it - d) Select "PlayerCamera" and delete it (the level camera from Step 8.4 will be used) - e) Repeat for BP_Bullet and BP_Enemy (delete their TempVisual cubes) - -2. Content Browser → Materials folder - -3. PLAYER MATERIAL: - a) Right-click → Material - b) Name: "M_Player" - c) Double-click to open Material Editor - d) Create Vector3 node (hold 3, click) - e) Set to blue color (0, 0.5, 1) - f) Connect to Base Color - g) Save and Close - -4. BULLET MATERIAL: - a) Create "M_PlayerBullet" - Yellow (1, 1, 0) - b) Create "M_EnemyBullet" - Red (1, 0, 0) - -5. ENEMY MATERIAL: - a) Create "M_Enemy" - Magenta (1, 0, 1) - -6. Apply materials to sprite components in each Blueprint - (Or use Sprite assets if you have 2D images) - -EXPECTED RESULT in Play mode: -- Player visible as blue shape -- Player bullets visible as yellow shapes -- Enemy bullets visible as red shapes -- Enemies visible as magenta shapes -- All game elements distinguishable by color - - -STEP 9.3: Add Background (Optional) --------------------------------------------------------------------------------- -1. In level, add a Plane mesh: - - Place Actors → Basic → Plane - - Scale: X=20, Y=30, Z=1 - - Position: X=0, Y=0, Z=-100 (behind everything) - -2. Create dark space material: - - M_Background - Dark blue/black - -EXPECTED RESULT in Play mode: -- Dark background visible behind all game elements -- Game elements (player, enemies, bullets) clearly visible against background -- Background doesn't interfere with gameplay (Z=-100, behind everything) - - -STEP 9.4: Test the Game --------------------------------------------------------------------------------- -1. Click "Play" button (green arrow in main toolbar) - OR press Alt+P - -2. TEST CHECKLIST: - □ Player moves with WASD or Arrow keys - □ Player stays within screen bounds - □ Player shoots with Z key or Left Mouse - □ Bullets travel upward - □ Enemies spawn at top of screen - □ Enemies move down with wavy motion - □ Enemies shoot radial bullet patterns - □ Player bullets damage enemies - □ Enemy bullets damage player - □ Score increases when enemies die - □ Lives decrease when player is hit - □ Timer counts down from 5:00 - □ Game shows "Game Over" when lives = 0 - □ Game shows "Mission Complete" after 5 minutes - □ Special ability (X key) clears screen - -3. To stop playing: Press ESC or click "Stop" button - -EXPECTED RESULT - Complete Game Test: -- All checklist items above should pass -- Frame rate stable (60+ FPS recommended) -- No crashes or Blueprint errors in Output Log -- Game is playable from start to victory/defeat - - -STEP 9.5: Build Standalone Game --------------------------------------------------------------------------------- -1. File → Package Project → Windows (or your platform) -2. Select output folder -3. Wait for build to complete -4. Navigate to output folder → WindowsNoEditor → [ProjectName].exe -5. Run the executable to play standalone - -EXPECTED RESULT: -- Build completes without errors (check Output Log) -- Executable file created in output folder -- Running .exe launches the game in fullscreen -- Game plays identically to editor Play mode -- Can close with Alt+F4 or in-game quit (if implemented) - - -================================================================================ -APPENDIX A: COMPLETE VARIABLE REFERENCE -================================================================================ - -BP_PLAYER Variables: -┌─────────────────────┬───────────────┬───────────────────────────────┐ -│ Variable Name │ Type │ Default Value │ -├─────────────────────┼───────────────┼───────────────────────────────┤ -│ MoveSpeed │ Float │ 750.0 │ -│ BoundsMin │ Vector 2D │ (-850, -450) │ -│ BoundsMax │ Vector 2D │ (850, 450) │ -│ FireInterval │ Float │ 0.08 │ -│ FireTimer │ Float │ 0.0 │ -│ BulletSpeed │ Float │ 2200.0 │ -│ MaxLives │ Integer │ 3 │ -│ CurrentLives │ Integer │ 3 │ -│ VolleySize │ Integer │ 3 │ -│ VolleySpread │ Float │ 12.0 │ -│ SpecialUsed │ Boolean │ false │ -│ BulletClass │ Class Ref │ BP_Bullet │ -└─────────────────────┴───────────────┴───────────────────────────────┘ - -BP_BULLET Variables: -┌─────────────────────┬───────────────┬───────────────────────────────┐ -│ TravelDirection │ Vector │ (0, 1, 0) │ -│ TravelSpeed │ Float │ 1200.0 │ -│ RemainingLifetime │ Float │ 4.0 │ -│ IsEnemyProjectile │ Boolean │ false │ -│ Damage │ Integer │ 1 │ -└─────────────────────┴───────────────┴───────────────────────────────┘ - -BP_ENEMY Variables: -┌─────────────────────┬───────────────┬───────────────────────────────┐ -│ MaxHealth │ Integer │ 12 │ -│ CurrentHealth │ Integer │ 12 │ -│ ScoreValue │ Integer │ 50 │ -│ VerticalSpeed │ Float │ 220.0 │ -│ HorizontalAmplitude │ Float │ 250.0 │ -│ HorizontalFrequency │ Float │ 1.8 │ -│ DespawnY │ Float │ -750.0 │ -│ FireInterval │ Float │ 0.35 │ -│ BulletsPerBurst │ Integer │ 20 │ -│ BurstSpread │ Float │ 360.0 │ -│ EnemyBulletSpeed │ Float │ 1000.0 │ -│ EnemyBulletLifetime │ Float │ 6.0 │ -│ BaseX │ Float │ 0.0 │ -│ WaveSeed │ Float │ 0.0 │ -│ FireTimer │ Float │ 0.0 │ -│ BulletClass │ Class Ref │ BP_Bullet │ -└─────────────────────┴───────────────┴───────────────────────────────┘ - -BP_ENEMYSPAWNER Variables: -┌─────────────────────┬───────────────┬───────────────────────────────┐ -│ EnemyClass │ Class Ref │ BP_Enemy │ -│ SpawnAreaHalfWidth │ Float │ 900.0 │ -│ GameDuration │ Float │ 300.0 │ -│ MaxSimultaneousEnemies│ Integer │ 120 │ -│ ElapsedTime │ Float │ 0.0 │ -│ SpawnTimer │ Float │ 0.0 │ -│ SpawningActive │ Boolean │ true │ -│ SpawnCurve │ Curve Float │ SpawnRateCurve asset │ -└─────────────────────┴───────────────┴───────────────────────────────┘ - - -================================================================================ -APPENDIX B: TROUBLESHOOTING -================================================================================ - -PROBLEM: Player doesn't move -SOLUTIONS: -1. Verify IMC_Default Input Mapping Context is added in BeginPlay -2. Check that IA_Move Input Action exists and has correct key bindings -3. Ensure "Possess" is called on player pawn (automatic with GameMode) -4. Verify MoveSpeed > 0 -5. Check if blueprint has errors (Compile button should be green checkmark) -6. Make sure Enhanced Input plugin is enabled (Edit → Plugins → Enhanced Input) - -PROBLEM: Bullets don't spawn -SOLUTIONS: -1. Check BulletClass variable is set to BP_Bullet -2. Verify SpawnActor node has valid class -3. Check that fire logic is connected to execution flow - -PROBLEM: No collisions detected -SOLUTIONS: -1. Verify collision components have "Generate Overlap Events" checked -2. Check collision channels are set correctly -3. Ensure "Collision Enabled" is set to "Query Only" or "Query and Physics" - -PROBLEM: UI doesn't appear -SOLUTIONS: -1. Check HUD widget is created and added to viewport -2. Verify ScoreManager is in the level -3. Check widget blueprint compiles without errors - -PROBLEM: Enemies don't spawn -SOLUTIONS: -1. Check EnemyClass is set in Spawner -2. Verify SpawningActive is true -3. Check spawn position is within camera view - -PROBLEM: Game runs too fast/slow -SOLUTIONS: -1. All movement should multiply by DeltaSeconds -2. Check speed values (may need adjustment for Unreal scale) -3. Unreal uses centimeters; multiply Unity values by ~100 - - -================================================================================ -APPENDIX C: UNITY TO UNREAL CONVERSION NOTES -================================================================================ - -SCALE CONVERSION: -- Unity units (1 = 1 meter) → Unreal units (1 = 1 centimeter) -- Multiply all Unity position/speed values by 100 -- Example: Unity speed 7.5 → Unreal speed 750 - -COORDINATE SYSTEM: -- Unity: Y = up, Z = forward -- Unreal: Z = up, X = forward -- For 2D top-down: Both use X for horizontal, but Y/Z swap for vertical - -INPUT: -- Unity: Input.GetAxisRaw("Horizontal") -- Unreal (Enhanced Input): - 1. Create Input Action (IA_Move) with Axis2D type - 2. Create Input Mapping Context with key bindings - 3. Add Mapping Context in BeginPlay - 4. Use "Get Action Value" node to read input - -INSTANTIATE: -- Unity: Instantiate(prefab, position, rotation) -- Unreal: Spawn Actor from Class (class, transform) - -DESTROY: -- Unity: Destroy(gameObject) -- Unreal: Destroy Actor - -DELTATIME: -- Unity: Time.deltaTime -- Unreal: Get World Delta Seconds - -FIND OBJECTS: -- Unity: FindObjectOfType() -- Unreal: Get All Actors of Class → Get first element - -SINGLETON PATTERN: -- Unity: static Instance property -- Unreal: Use Game Instance or subsystem, or Get All Actors - - -================================================================================ -END OF TUTORIAL -================================================================================ - -This tutorial creates a complete bullet-hell game matching the Unity magisterka_1 -implementation with: -- Player with 3 lives, WASD movement, Z/mouse shooting -- Volley shooting (3 bullets in spread pattern) -- Screen-clear special ability (X key, one use) -- Enemies with sine-wave movement -- Radial bullet patterns (20 bullets per burst) -- Progressive difficulty (spawn rate increases over 5 minutes) -- Score, lives, and timer UI -- Victory after 5 minutes survival -- Game over when lives reach 0 diff --git a/games/unreal/tutorial/README.md b/games/unreal/tutorial/README.md new file mode 100644 index 0000000..31ce91e --- /dev/null +++ b/games/unreal/tutorial/README.md @@ -0,0 +1,102 @@ +# Unreal Engine Bullet Hell Game - Complete Tutorial + +This tutorial recreates the Unity "magisterka_1" bullet-hell shooter in Unreal Engine 5. + +## Game Features + +- Player ship with movement and shooting +- Enemies that move downward with sinusoidal horizontal motion +- Enemy spawning that increases over time +- Radial bullet patterns from enemies +- Score, lives, and timer UI +- Special "bomb" ability to clear screen +- 5-minute game duration with victory/defeat conditions + +--- + +## Table of Contents + +### Part 1: Project Setup +- [Step 1.1: Create New Project](part-1-project-setup.md#step-11-create-new-project) +- [Step 1.2: Set Up 2D Game View](part-1-project-setup.md#step-12-set-up-2d-game-view) +- [Step 1.3: Create Folder Structure](part-1-project-setup.md#step-13-create-folder-structure) + +### Part 2: Create the Player +- [Step 2.1: Create Player Blueprint](part-2-create-player.md#step-21-create-player-blueprint) +- [Step 2.2: Add Player Visual Components](part-2-create-player.md#step-22-add-player-visual-components) +- [Step 2.3: Create Player Variables](part-2-create-player.md#step-23-create-player-variables) +- [Step 2.4: Set Up Enhanced Input System](part-2-create-player.md#step-24-set-up-enhanced-input-system) +- [Step 2.5: Create Player Firing Logic](part-2-create-player.md#step-25-create-player-firing-logic) +- [Step 2.6: Create Player Damage and Special Ability](part-2-create-player.md#step-26-create-player-damage-and-special-ability) + +### Part 3: Create the Bullet +- [Step 3.1: Create Bullet Blueprint](part-3-create-bullet.md#step-31-create-bullet-blueprint) +- [Step 3.2: Bullet Movement Logic](part-3-create-bullet.md#step-32-bullet-movement-logic) +- [Step 3.3: Bullet Collision Logic](part-3-create-bullet.md#step-33-bullet-collision-logic) +- [Step 3.4: Complete Player Firing Logic](part-3-create-bullet.md#step-34-complete-player-firing-logic-bp_player) + +### Part 4: Create the Enemy +- [Step 4.1: Create Enemy Blueprint](part-4-create-enemy.md#step-41-create-enemy-blueprint) +- [Step 4.2: Enemy Initialization](part-4-create-enemy.md#step-42-enemy-initialization) +- [Step 4.3: Enemy Movement Logic](part-4-create-enemy.md#step-43-enemy-movement-logic) +- [Step 4.4: Enemy Firing Logic](part-4-create-enemy.md#step-44-enemy-firing-logic) +- [Step 4.5: Enemy Damage and Death](part-4-create-enemy.md#step-45-enemy-damage-and-death) +- [Step 4.6: Complete Special Ability in BP_Player](part-4-create-enemy.md#step-46-complete-special-ability-in-bp_player) + +### Part 5: Create Enemy Spawner +- [Step 5.1: Create Spawner Blueprint](part-5-create-spawner.md#step-51-create-spawner-blueprint) +- [Step 5.2: Spawn Rate Curve](part-5-create-spawner.md#step-52-spawn-rate-curve) +- [Step 5.3: Spawning Logic](part-5-create-spawner.md#step-53-spawning-logic) + +### Part 6: Create Game Director +- [Step 6.1: Create Game Director Blueprint](part-6-game-director.md#step-61-create-game-director-blueprint) +- [Step 6.2: Game Director Initialization](part-6-game-director.md#step-62-game-director-initialization) +- [Step 6.3: Game Director Update Logic](part-6-game-director.md#step-63-game-director-update-logic) + +### Part 7: Create Score Manager / UI +- [Step 7.1: Create UI Widget Blueprint](part-7-score-manager-ui.md#step-71-create-ui-widget-blueprint) +- [Step 7.2: Design HUD Layout](part-7-score-manager-ui.md#step-72-design-hud-layout) +- [Step 7.3: Create Score Manager Blueprint](part-7-score-manager-ui.md#step-73-create-score-manager-blueprint) +- [Step 7.4: Score Manager Initialization](part-7-score-manager-ui.md#step-74-score-manager-initialization) +- [Step 7.5: Score Manager Functions](part-7-score-manager-ui.md#step-75-score-manager-functions) + +### Part 8: Create Game Mode and Level +- [Step 8.1: Create Custom Game Mode](part-8-game-mode-level.md#step-81-create-custom-game-mode) +- [Step 8.2: Configure Project to Use Game Mode](part-8-game-mode-level.md#step-82-configure-project-to-use-game-mode) +- [Step 8.3: Create Game Level](part-8-game-mode-level.md#step-83-create-game-level) +- [Step 8.4: Set Up Level Components](part-8-game-mode-level.md#step-84-set-up-level-components) +- [Step 8.5: Set Default Level](part-8-game-mode-level.md#step-85-set-default-level) + +### Part 9: Final Setup and Testing +- [Step 9.1: Assign Blueprint References](part-9-final-setup.md#step-91-assign-blueprint-references) +- [Step 9.2: Create Final Visuals](part-9-final-setup.md#step-92-create-final-visuals-replace-placeholder) +- [Step 9.3: Add Background](part-9-final-setup.md#step-93-add-background-optional) +- [Step 9.4: Test the Game](part-9-final-setup.md#step-94-test-the-game) +- [Step 9.5: Build Standalone Game](part-9-final-setup.md#step-95-build-standalone-game) + +### Appendices +- [Appendix A: Complete Variable Reference](appendix-a-variables.md) +- [Appendix B: Troubleshooting](appendix-b-troubleshooting.md) +- [Appendix C: Unity to Unreal Conversion Notes](appendix-c-unity-conversion.md) + +--- + +## Quick Start + +1. Start with [Part 1: Project Setup](part-1-project-setup.md) +2. Follow each part in order +3. Test frequently using the "EXPECTED RESULT" sections +4. Use the [Troubleshooting](appendix-b-troubleshooting.md) appendix if you encounter issues + +--- + +## Navigation + +Each page includes: +- **← Previous** and **Next →** links at the top and bottom +- Links back to this index +- Cross-references to related sections + +--- + +*This tutorial is part of a master's thesis comparing Unity and Unreal Engine.* diff --git a/games/unreal/tutorial/appendix-a-variables.md b/games/unreal/tutorial/appendix-a-variables.md new file mode 100644 index 0000000..4e0e563 --- /dev/null +++ b/games/unreal/tutorial/appendix-a-variables.md @@ -0,0 +1,99 @@ +# Appendix A: Complete Variable Reference + +[← Part 9: Final Setup](part-9-final-setup.md) | [Back to Index](README.md) | [Appendix B: Troubleshooting →](appendix-b-troubleshooting.md) + +--- + +## BP_Player Variables + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `MoveSpeed` | Float | 750.0 | +| `BoundsMin` | Vector 2D | (-850, -450) | +| `BoundsMax` | Vector 2D | (850, 450) | +| `FireInterval` | Float | 0.08 | +| `FireTimer` | Float | 0.0 | +| `BulletSpeed` | Float | 2200.0 | +| `MaxLives` | Integer | 3 | +| `CurrentLives` | Integer | 3 | +| `VolleySize` | Integer | 3 | +| `VolleySpread` | Float | 12.0 | +| `SpecialUsed` | Boolean | false | +| `BulletClass` | Class Ref | BP_Bullet | + +--- + +## BP_Bullet 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 | + +--- + +## BP_Enemy Variables + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `MaxHealth` | Integer | 12 | +| `CurrentHealth` | Integer | 12 | +| `ScoreValue` | Integer | 50 | +| `VerticalSpeed` | Float | 220.0 | +| `HorizontalAmplitude` | Float | 250.0 | +| `HorizontalFrequency` | Float | 1.8 | +| `DespawnY` | Float | -750.0 | +| `FireInterval` | Float | 0.35 | +| `BulletsPerBurst` | Integer | 20 | +| `BurstSpread` | Float | 360.0 | +| `EnemyBulletSpeed` | Float | 1000.0 | +| `EnemyBulletLifetime` | Float | 6.0 | +| `BaseX` | Float | 0.0 | +| `WaveSeed` | Float | 0.0 | +| `FireTimer` | Float | 0.0 | +| `BulletClass` | Class Ref | BP_Bullet | + +--- + +## BP_EnemySpawner Variables + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `EnemyClass` | Class Ref | BP_Enemy | +| `SpawnAreaHalfWidth` | Float | 900.0 | +| `GameDuration` | Float | 300.0 | +| `MaxSimultaneousEnemies` | Integer | 120 | +| `ElapsedTime` | Float | 0.0 | +| `SpawnTimer` | Float | 0.0 | +| `SpawningActive` | Boolean | true | +| `SpawnCurve` | Curve Float | SpawnRateCurve asset | + +--- + +## BP_GameDirector Variables + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `PlayerReference` | Object Ref | (set at runtime) | +| `SpawnerReference` | Object Ref | (set at runtime) | +| `GameDuration` | Float | 300.0 | +| `ElapsedTime` | Float | 0.0 | +| `GameActive` | Boolean | true | + +--- + +## BP_ScoreManager Variables + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `Score` | Integer | 0 | +| `CurrentLives` | Integer | 3 | +| `HUDWidget` | Object Ref | (set at runtime) | +| `HUDWidgetClass` | Class Ref | WBP_HUD | + +--- + +[← Part 9: Final Setup](part-9-final-setup.md) | [Back to Index](README.md) | [Appendix B: Troubleshooting →](appendix-b-troubleshooting.md) diff --git a/games/unreal/tutorial/appendix-b-troubleshooting.md b/games/unreal/tutorial/appendix-b-troubleshooting.md new file mode 100644 index 0000000..857c80a --- /dev/null +++ b/games/unreal/tutorial/appendix-b-troubleshooting.md @@ -0,0 +1,171 @@ +# Appendix B: Troubleshooting + +[← Appendix A: Variables](appendix-a-variables.md) | [Back to Index](README.md) | [Appendix C: Unity Conversion →](appendix-c-unity-conversion.md) + +--- + +## Player doesn't move + +
+Click to expand solutions + +1. Verify IMC_Default Input Mapping Context is added in BeginPlay +2. Check that IA_Move Input Action exists and has correct key bindings +3. Ensure "Possess" is called on player pawn (automatic with GameMode) +4. Verify MoveSpeed > 0 +5. Check if blueprint has errors (Compile button should be green checkmark) +6. Make sure Enhanced Input plugin is enabled (Edit → Plugins → Enhanced Input) +7. **CRITICAL:** Verify Project Settings → Input → Default Classes are set to Enhanced Input + +
+ +--- + +## Bullets don't spawn + +
+Click to expand solutions + +1. Check BulletClass variable is set to BP_Bullet +2. Verify SpawnActor node has valid class +3. Check that fire logic is connected to execution flow +4. Ensure the FireVolley function is connected to the fire rate limiter +5. Verify BP_Bullet blueprint compiles without errors + +
+ +--- + +## No collisions detected + +
+Click to expand solutions + +1. Verify collision components have "Generate Overlap Events" checked +2. Check collision channels are set correctly +3. Ensure "Collision Enabled" is set to "Query Only" or "Query and Physics" +4. Verify both actors have collision components (not just one) +5. Check that the Cast To nodes succeed (add print statements to debug) + +
+ +--- + +## UI doesn't appear + +
+Click to expand solutions + +1. Check HUD widget is created and added to viewport +2. Verify ScoreManager is in the level +3. Check widget blueprint compiles without errors +4. Ensure "Add to Viewport" node is connected in execution flow +5. Verify text color is visible against background (try white text) + +
+ +--- + +## Enemies don't spawn + +
+Click to expand solutions + +1. Check EnemyClass is set in Spawner +2. Verify SpawningActive is true +3. Check spawn position is within camera view +4. Ensure BP_EnemySpawner is placed in the level +5. Verify the SpawnCurve asset is assigned + +
+ +--- + +## Game runs too fast/slow + +
+Click to expand solutions + +1. All movement should multiply by DeltaSeconds +2. Check speed values (may need adjustment for Unreal scale) +3. Unreal uses centimeters; multiply Unity values by ~100 +4. Verify no duplicate Event Tick processing +5. Check frame rate (use `stat fps` console command) + +
+ +--- + +## Input works in editor but not in packaged build + +
+Click to expand solutions + +1. Ensure all Input Actions and IMC_Default are saved +2. Check that assets are not in a "Developer" folder (excluded from builds) +3. Verify Project Settings input classes are set correctly +4. Try clearing Saved and Intermediate folders, then rebuild + +
+ +--- + +## Blueprint won't compile + +
+Click to expand solutions + +1. Look at the Compiler Results panel for specific errors +2. Check for circular references between blueprints +3. Ensure all required inputs are connected +4. Verify variable types match (no type mismatches) +5. Look for "Access None" errors (null references) + +
+ +--- + +## Camera doesn't show the game correctly + +
+Click to expand solutions + +1. Verify camera position: `X=0, Y=0, Z=1000` +2. Verify camera rotation: `X=-90, Y=0, Z=0` (looking down) +3. Check "Auto Activate for Player" is enabled +4. For orthographic: Set Projection Mode and Ortho Width +5. Ensure no other cameras are taking priority + +
+ +--- + +## General Debugging Tips + +### Enable Print Statements +``` +Right-click → Print String → Type message +``` +Add these throughout your blueprints to track execution flow. + +### Check Output Log +- **Window → Developer Tools → Output Log** +- Shows blueprint errors, warnings, and print statements + +### Use Breakpoints +- Right-click any blueprint node → **Add Breakpoint** +- Execution pauses at that point during Play mode +- Step through with F10/F11 + +### Visualize Collisions +- In viewport: **Show → Collision** +- Shows collision shapes in play mode + +### Console Commands +- Press `~` (tilde) to open console +- `stat fps` - Show frame rate +- `show collision` - Toggle collision visualization + +--- + +[← Appendix A: Variables](appendix-a-variables.md) | [Back to Index](README.md) | [Appendix C: Unity Conversion →](appendix-c-unity-conversion.md) diff --git a/games/unreal/tutorial/appendix-c-unity-conversion.md b/games/unreal/tutorial/appendix-c-unity-conversion.md new file mode 100644 index 0000000..e33cf81 --- /dev/null +++ b/games/unreal/tutorial/appendix-c-unity-conversion.md @@ -0,0 +1,259 @@ +# Appendix C: Unity to Unreal Conversion Notes + +[← Appendix B: Troubleshooting](appendix-b-troubleshooting.md) | [Back to Index](README.md) + +--- + +## Scale Conversion + +| Unity | Unreal | Notes | +|-------|--------|-------| +| 1 unit = 1 meter | 1 unit = 1 centimeter | Multiply Unity values by 100 | + +**Example:** +- Unity speed: `7.5` → Unreal speed: `750` +- Unity position: `(5, 10, 0)` → Unreal position: `(500, 1000, 0)` + +--- + +## Coordinate System + +| Axis | Unity | Unreal | +|------|-------|--------| +| Up | Y | Z | +| Forward | Z | X | +| Right | X | Y | + +**For 2D top-down games:** +- Both use X for horizontal +- Y/Z swap for vertical + +--- + +## Common Operations Comparison + +### Input + +**Unity:** +```csharp +float h = Input.GetAxisRaw("Horizontal"); +float v = Input.GetAxisRaw("Vertical"); +``` + +**Unreal (Enhanced Input):** +1. Create Input Action (IA_Move) with Axis2D type +2. Create Input Mapping Context with key bindings +3. Add Mapping Context in BeginPlay +4. Use "Get Action Value" node to read input + +--- + +### Instantiate / Spawn + +**Unity:** +```csharp +Instantiate(prefab, position, rotation); +``` + +**Unreal:** +``` +Spawn Actor from Class +├── Class: YourBlueprintClass +├── Spawn Transform Location: position +└── Spawn Transform Rotation: rotation +``` + +--- + +### Destroy + +**Unity:** +```csharp +Destroy(gameObject); +``` + +**Unreal:** +``` +Destroy Actor +└── Target: Self (or reference) +``` + +--- + +### Delta Time + +**Unity:** +```csharp +Time.deltaTime +``` + +**Unreal:** +``` +Get World Delta Seconds +``` + +--- + +### Find Objects + +**Unity:** +```csharp +FindObjectOfType(); +FindObjectsOfType(); +``` + +**Unreal:** +``` +Get All Actors of Class +├── Actor Class: BP_Player +└── Out Actors: (array) + +Then: Get (a ref) → index 0 +``` + +--- + +### Singleton Pattern + +**Unity:** +```csharp +public static GameManager Instance { get; private set; } + +void Awake() { + Instance = this; +} +``` + +**Unreal Options:** +1. **Game Instance** - Persists across levels +2. **Subsystem** - Engine-managed singleton +3. **Get All Actors of Class** - Find at runtime + +--- + +### Coroutines vs Timers + +**Unity:** +```csharp +StartCoroutine(DelayedAction()); + +IEnumerator DelayedAction() { + yield return new WaitForSeconds(2.0f); + DoSomething(); +} +``` + +**Unreal:** +``` +Set Timer by Function Name +├── Function Name: "DelayedAction" +├── Time: 2.0 +└── Looping: false +``` + +Or use **Delay** node in blueprints. + +--- + +### Physics Layers vs Collision Channels + +**Unity:** +- Layer-based collision matrix +- `Physics.IgnoreLayerCollision()` + +**Unreal:** +- Collision Channels (Object Types) +- Collision Presets +- Per-component collision settings + +--- + +### Tags + +**Unity:** +```csharp +if (other.CompareTag("Enemy")) { } +``` + +**Unreal:** +``` +Actor Has Tag +├── Target: Other Actor +└── Tag: "Enemy" +``` + +Or use **Cast To** for type checking (preferred). + +--- + +### Vector Math + +| Operation | Unity | Unreal | +|-----------|-------|--------| +| Normalize | `vector.normalized` | `Normalize` node | +| Magnitude | `vector.magnitude` | `Vector Length` node | +| Dot Product | `Vector3.Dot(a, b)` | `Dot Product` node | +| Cross Product | `Vector3.Cross(a, b)` | `Cross Product` node | + +--- + +### Random + +**Unity:** +```csharp +Random.Range(0f, 1f); +Random.Range(0, 10); // int, exclusive max +``` + +**Unreal:** +``` +Random Float in Range +├── Min: 0.0 +└── Max: 1.0 + +Random Integer in Range +├── Min: 0 +└── Max: 9 // inclusive! +``` + +> ⚠️ **Note:** Unreal's integer range is INCLUSIVE on both ends! + +--- + +### Debug + +**Unity:** +```csharp +Debug.Log("Message"); +Debug.DrawLine(start, end, Color.red); +``` + +**Unreal:** +``` +Print String +└── In String: "Message" + +Draw Debug Line +├── Line Start: start +├── Line End: end +└── Line Color: Red +``` + +--- + +## Quick Reference Table + +| Concept | Unity | Unreal | +|---------|-------|--------| +| Script | C# MonoBehaviour | Blueprint / C++ Actor | +| Prefab | .prefab asset | Blueprint Class | +| Scene | .unity scene | Level (.umap) | +| Inspector | Inspector window | Details panel | +| Hierarchy | Hierarchy window | Outliner | +| Project | Project window | Content Browser | +| Console | Console window | Output Log | +| Play | Play button | Play (Alt+P) | + +--- + +[← Appendix B: Troubleshooting](appendix-b-troubleshooting.md) | [Back to Index](README.md) diff --git a/games/unreal/tutorial/part-1-project-setup.md b/games/unreal/tutorial/part-1-project-setup.md new file mode 100644 index 0000000..407ac85 --- /dev/null +++ b/games/unreal/tutorial/part-1-project-setup.md @@ -0,0 +1,97 @@ +# Part 1: Project Setup + +[← Back to Index](README.md) | [Next: Part 2 - Create the Player →](part-2-create-player.md) + +--- + +## Step 1.1: Create New Project + +1. Open Epic Games Launcher +2. Click "Unreal Engine" tab on the left sidebar +3. Click yellow "Launch" button next to your UE5 version +4. Wait for Unreal Engine to open (this may take 1-2 minutes) + +5. In the "Unreal Project Browser" window that appears: + - At the top, select "Games" category (should be selected by default) + - Click "Blank" template (empty square icon) + +6. On the right side panel, configure: + - **Project Defaults:** Blueprint (not C++) + - **Target Platform:** Desktop + - **Quality Preset:** Maximum + - **Starter Content:** UNCHECKED (we don't need it) + - **Raytracing:** UNCHECKED + +7. At the bottom: + - Choose folder location where you want to save + - Name the project: `BulletHellGame` + +8. Click "Create" button (bottom right, yellow) + +### Expected Result + +Unreal Editor opens with an empty level. You should see: +- Main 3D viewport in the center +- Outliner panel on the right (showing "Untitled" level) +- Details panel on the right side + +> **NOTE:** The Content Drawer is NOT open by default. To open it: +> - Click "Content Drawer" button at the bottom of the screen, OR +> - Press `Ctrl+Space`, OR +> - Go to Window → Content Drawer + +--- + +## Step 1.2: Set Up 2D Game View + +Since bullet-hell games are typically 2D, we'll set up a top-down orthographic view. + +1. In the main viewport, look at the top-left corner +2. Click the dropdown that says "Perspective" +3. Select "Top" from the dropdown menu (`Alt + J`) + +4. To set up proper camera for the game: + - Go to menu bar (the top bar): **Edit** (Second from left, next to "File" and "Window") → **Project Settings** + - In the left sidebar, search for "Maps & Modes" (Project → 4th from the top, next to GameplayTags and Movies) + - Click on "Maps & Modes" + +5. Under "Default Modes": + - Find "Default GameMode" dropdown + - We'll create our own later, leave it for now + +6. Close Project Settings window (X in top right) + +--- + +## Step 1.3: Create Folder Structure + +1. Look at the Content Browser at the bottom of the screen +2. You should see "Content" folder on the left panel + +3. Right-click on "Content" folder → **New Folder** + - Name it: `Blueprints` + +4. Right-click on "Content" folder → **New Folder** + - Name it: `Materials` + +5. Right-click on "Content" folder → **New Folder** + - Name it: `Sprites` (for 2D textures) + +6. Right-click on "Content" folder → **New Folder** + - Name it: `UI` + +### Expected Result + +Content Browser shows 4 folders: + +``` +Content/ +├── Blueprints/ +├── Materials/ +├── Sprites/ +└── UI/ +``` + +--- + +[← Back to Index](README.md) | [Next: Part 2 - Create the Player →](part-2-create-player.md) diff --git a/games/unreal/tutorial/part-2-create-player.md b/games/unreal/tutorial/part-2-create-player.md new file mode 100644 index 0000000..ffeb932 --- /dev/null +++ b/games/unreal/tutorial/part-2-create-player.md @@ -0,0 +1,946 @@ +# Part 2: Create the Player + +[← Previous: Part 1 - Project Setup](part-1-project-setup.md) | [Back to Index](README.md) | [Next: Part 3 - Create the Bullet →](part-3-create-bullet.md) + +--- + +## Step 2.1: Create Player Blueprint + +1. In Content Browser, double-click "Blueprints" folder to open it + +2. Right-click in empty space → **Blueprint Class** + +3. In the popup window "Pick Parent Class": + - Click "Pawn" (NOT Character - we want simple 2D control) + - Click "Select" + +4. Name the new Blueprint: `BP_Player` + +5. Double-click "BP_Player" to open the Blueprint Editor + +### Expected Result + +A new tab opens showing the Blueprint Editor with: +- Components panel on the left +- Viewport in the center +- Details panel on the right +- Tab bar above the Viewport with: Viewport | Construction Script | Event Graph (third from left) + +--- + +## Step 2.2: Add Player Visual Components + +1. In the Components panel (left side), you'll see "DefaultSceneRoot" + - Click on "DefaultSceneRoot" to select it (this ensures new components are added as children) + +2. With DefaultSceneRoot selected, click **"Add"** button (green, top of Components panel) + - Search for "Paper Sprite" + - Click "Paper Sprite" to add it (it will be added as a child of DefaultSceneRoot) + - Rename it to `PlayerSprite` (click on it, then press F2) + + > **NOTE:** If Paper Sprite is not available: + > - Go to Edit → Plugins (from main menu bar) + > - Search for "Paper2D" + > - Make sure "Paper2D" plugin is ENABLED + > - Restart the editor if prompted + +3. With "PlayerSprite" selected, look at Details panel (right side): + - Find "Source Sprite" property + - For now, leave it empty (we'll create sprites later) + - Under "Transform", set Scale to `(1.0, 1.0, 1.0)` + +4. With DefaultSceneRoot still selected, click **"Add"** button again: + - Search for "Box Collision" + - Click "Box Collision" to add it (it will be added as a child of DefaultSceneRoot) + - Rename it to `PlayerCollision` + +5. With "PlayerCollision" selected, in Details panel: + - Under "Shape", set Box Extent to: `X=25, Y=25, Z=10` + - Under "Collision", click "Collision Presets" dropdown + - Select "Custom..." + - Set "Collision Enabled" to "Query Only (No Physics Collision)" + - Enable "Generate Overlap Events" box + +6. With DefaultSceneRoot still selected, click **"Add"** button: + - Search for "Arrow" + - Click "Arrow" to add it (it will be added as a child of DefaultSceneRoot) + - This shows which direction is "forward" (useful for debugging) + +7. With DefaultSceneRoot still selected, click **"Add"** button: + - Search for "Cube" → add "Cube" (Static Mesh) + - Rename it to `TempVisual` + - In Details panel, set Scale to `(0.5, 0.5, 0.1)` so it's small and flat + - This provides a visible placeholder until we add proper sprites in [Part 9](part-9-final-setup.md) + +8. With DefaultSceneRoot still selected, click **"Add"** button: + - Search for "Camera" → add "Camera" + - Rename it to `PlayerCamera` + - In Details panel, set Location to: `X=0, Y=0, Z=1000` (1000 units above player) + - Set Rotation to: `X=0, Y=-90, Z=0` (looking straight down) + - In Details panel, find "Activation" category → "Auto Activate" checkbox + - This should already be ENABLED by default + - If not, check/enable it - this tells Unreal to use THIS camera when playing + - This camera will follow the player, making testing easy + + > **NOTE:** This temporary camera ensures you can see the player during testing. + > In [Part 8](part-8-game-mode-level.md), we'll set up a proper fixed camera for the final game. + +### Expected Result + +Components panel shows: + +``` +DefaultSceneRoot +├── PlayerSprite (Paper Sprite) +├── PlayerCollision (Box Collision) +├── Arrow +├── TempVisual (Cube) +└── PlayerCamera (Camera) +``` + +9. **IMPORTANT - Enable Auto Possession for testing:** + - Click on "BP_Player (Self)" at the top of the Components panel (the root) + - In Details panel, find "Pawn" category + - Find "Auto Possess Player" dropdown (currently set to "Disabled") + - Change it to **"Player 0"** + - This makes the engine automatically possess this pawn when playing, which activates the PlayerCamera + + > **NOTE:** This setting will be overridden later when we set up the GameMode + > in [Part 8](part-8-game-mode-level.md), which handles possession automatically. + +--- + +## Step 2.3: Create Player Variables + +*(Continue in the same BP_Player Blueprint Editor tab that was opened in Step 2.1)* + +1. In the Blueprint Editor, look at the left panel (below the Components panel) +2. Find "My Blueprint" section +3. Under "Variables", click the **"+"** button + +4. Create these variables one by one (click +, then set properties in Details): + + > **NOTE:** Variables are NOT added to the Components hierarchy. They appear in a + > separate "Variables" list within the "My Blueprint" panel. Variables store + > data values, while Components are physical/visual parts of the actor. + +| # | Variable Name | Type | Default Value | +|---|---------------|------|---------------| +| 1 | `MoveSpeed` | Float | 750.0 | +| 2 | `BoundsMin` | Vector 2D | X=-850, Y=-450 | +| 3 | `BoundsMax` | Vector 2D | X=850, Y=450 | +| 4 | `FireInterval` | Float | 0.08 | +| 5 | `FireTimer` | Float | 0.0 | +| 6 | `BulletSpeed` | Float | 2200.0 | +| 7 | `MaxLives` | Integer | 3 | +| 8 | `CurrentLives` | Integer | 3 | +| 9 | `VolleySize` | Integer | 3 | +| 10 | `VolleySpread` | Float | 12.0 | +| 11 | `SpecialUsed` | Boolean | false (unchecked) | +| 12 | `BulletClass` | Class Reference* | (set later) | + +> *For BulletClass: In the type dropdown, go to "Object Types" category → "Actor" → select "Class Reference" (the subcategory under Actor). This will hold reference to bullet blueprint class for spawning. + +> **TIP:** For Variable 1 (MoveSpeed), click the "eye" icon to make it public/editable. Compile the blueprint (Click on "Compile" third option from left or simply `Ctrl + Alt`) before setting the default value. + +5. Click **"Compile"** button (top left, blue checkmark icon) +6. Click **"Save"** button (floppy disk icon next to Compile) + +### Expected Result + +My Blueprint panel shows: +- All 12 variables you created listed under "Variables" +- A "Components" category with 4 component references (DefaultSceneRoot, PlayerSprite, PlayerCollision, Arrow) - these are automatically created from the components you added in Step 2.2 + +--- + +## Step 2.4: Set Up Enhanced Input System + +UE5 uses the Enhanced Input System. Before creating movement logic in our Blueprint, we first need to create Input Actions and an Input Mapping Context. + +### 0. ENABLE ENHANCED INPUT IN PROJECT SETTINGS (CRITICAL) + +Before creating any input assets, you **MUST** configure the project to use Enhanced Input as the default input system: + +1. Go to **Edit → Project Settings** +2. In the left sidebar, scroll down to "Engine" section +3. Click on "Input" +4. Find "Default Classes" section (near the top) +5. Set "Default Player Input Class" to: `EnhancedPlayerInput` +6. Set "Default Input Component Class" to: `EnhancedInputComponent` +7. Close Project Settings + +> ⚠️ **WARNING:** If you skip this step, WASD controls will NOT work even if everything else is set up correctly! + +### 1. SET UP INPUT ASSETS (in Content Drawer) + +#### A) CREATE INPUT ACTIONS + +1. Open Content Drawer (`Ctrl+Space` or click "Content Drawer" at bottom) +2. In Content folder, right-click → **New Folder** → name it `Input` + +3. Inside Input folder, right-click → **Input → Input Action** + - Name it `IA_Move` + - Double-click to open + - Set "Value Type" to `Axis2D (Vector2D)` + - Save and close + +4. Create another Input Action: `IA_Fire` + - Set "Value Type" to `Digital (bool)` (default) + +5. Create another Input Action: `IA_Special` + - Set "Value Type" to `Digital (bool)` + +#### B) CREATE INPUT MAPPING CONTEXT + +1. In Input folder, right-click → **Input → Input Mapping Context** + - Name it `IMC_Default` + - Double-click to open + +2. Click **"+"** next to "Mappings" to add IA_Move: + - Select "IA_Move" from dropdown + - Click "+" under IA_Move to add key bindings: + + > **NOTE:** In Unreal's top-down view, X axis = up/down, Y axis = left/right + + **For W key (move UP on screen):** + - Click "+", select "W" + - No modifiers needed (outputs X positive = up) + - TOTAL MODIFIERS FOR W: **0** + + **For S key (move DOWN on screen):** + - Click "+", select "S" + - Click "+" next to "Modifiers" → Add "Negate" + - TOTAL MODIFIERS FOR S: **1** (Negate only) + + **For A key (move LEFT on screen):** + - Click "+", select "A" + - Click "+" next to "Modifiers" → Add "Swizzle Input Axis Values" → Order: "YXZ" + - Click "+" next to "Modifiers" AGAIN → Add "Negate" + - TOTAL MODIFIERS FOR A: **2** (Swizzle AND Negate) + + **For D key (move RIGHT on screen):** + - Click "+", select "D" + - Click "+" next to "Modifiers" → Add "Swizzle Input Axis Values" → Order: "YXZ" + - TOTAL MODIFIERS FOR D: **1** (Swizzle only) + + **Summary Table - verify your IMC_Default matches this:** + + | Key | Modifiers | Output Vector | + |-----|-----------|---------------| + | W | (none) | (1, 0) = UP | + | S | Negate | (-1, 0) = DOWN | + | A | Swizzle Input Axis Values (YXZ) + Negate | (0, -1) = LEFT | + | D | Swizzle Input Axis Values (YXZ) | (0, 1) = RIGHT | + + After adding all keys, press `Ctrl+S` to SAVE IMC_Default! + +3. Click "+" to add **IA_Fire**: + - Select "IA_Fire" + - Add key: `Z` + - Add key: `Left Mouse Button` + +4. Click "+" to add **IA_Special**: + - Select "IA_Special" + - Add key: `X` + - Add key: `Right Mouse Button` + +5. Save the Input Mapping Context + +### 2. NOW OPEN BP_PLAYER BLUEPRINT + +1. In Content Drawer, navigate to Content → Blueprints +2. Double-click "BP_Player" to open the Blueprint Editor +3. Click on **"Event Graph"** tab (above the main view, third tab from left, next to Construction Script) + +You should see three default events (red nodes): +- Event BeginPlay +- Event ActorBeginOverlap +- Event Tick + +### 3. ADD MAPPING CONTEXT IN BEGINPLAY + +From "Event BeginPlay" node: + +1. Right-click in empty space → search `Get Player Controller` → add it + - Use the one under "Game" → "Player" category + - It has a "Player Index" input (default 0) and "Return Value" output +2. From the blue "Return Value" output, drag and search `Get Enhanced Input Local Player Subsystem` +3. From that blue output, drag and search `Add Mapping Context` +4. For "Mapping Context" input: + - Click dropdown and select `IMC_Default` + - Or drag IMC_Default from Content Drawer +5. Set "Priority" to `0` + +6. **CRITICAL - Connect the EXECUTION wire (white wire):** + - The WHITE TRIANGLE pins are different from the BLUE CIRCLE pins! + - Blue circles = DATA (what values to use) + - White triangles = EXECUTION (when to run) + + - On "Event BeginPlay", find the WHITE TRIANGLE on the RIGHT side + - On "Add Mapping Context", find the WHITE TRIANGLE on the LEFT side + - Click and DRAG from BeginPlay's white triangle to Add Mapping Context's white triangle + + **WITHOUT THIS WHITE WIRE, THE CODE NEVER RUNS!** + +Your graph should look like this: + +``` +┌──────────────────┐ ┌─────────────────────────────────────┐ +│ Event BeginPlay │ │ Add Mapping Context │ +│ ├────────►│► │ +│ │ WHITE │ Target ○────────────────┐ │ +└──────────────────┘ WIRE │ Mapping Context: IMC_Default │ + │ Priority: 0 │ + └─────────────────────────────────────┘ + ▲ +┌──────────────────┐ │ +│Get Player │ ┌─────────────────────────┐ +│Controller ├───►│ Enhanced Input Local │ +│ Player Index [0] │ │ Player Subsystem ├───┘ +└──────────────────┘ └─────────────────────────┘ + (blue data wires) +``` + +**VERIFY:** You must have BOTH: +- Blue data wires connecting the nodes (passes the subsystem reference) +- White execution wire from Event BeginPlay to Add Mapping Context (makes it run) + +### 4. INPUT DEBUG TEST (verify input works - separate from movement logic) + +Before creating movement, let's verify input is working with a separate debug setup. This debug logic is COMPLETELY INDEPENDENT from movement - you can delete it later without affecting anything. + +#### a) Create debug variables (these are ONLY for debugging): +- In My Blueprint panel → Variables → click "+" +- Name: `DEBUG_LastMoveInput` +- Type: Vector 2D +- Default Value: (0, 0) +- Compile to save + +#### b) Create a SEPARATE debug event using a Custom Event: +- Right-click in empty space → search `Custom Event` → add it +- Name it `DEBUG_PrintInput` +- This keeps debug logic completely isolated + +#### c) Build the debug logic from DEBUG_PrintInput: + +1. **Get the current input value:** + - Right-click → "Get Player Controller" + - Drag → "Get Enhanced Input Local Player Subsystem" + - Drag → search "IA_Move" (under Input → Enhanced Action Values) + +2. **Compare to last value (only print on change):** + - Right-click → "Not Equal (Vector2D)" + - Connect IA_Move to TOP input + - "Get DEBUG_LastMoveInput" → connect to BOTTOM input + +3. **Branch on change:** + - From DEBUG_PrintInput WHITE pin → "Branch" + - Connect != result to Condition + +4. **On TRUE (changed):** + - "Set DEBUG_LastMoveInput" = IA_Move value + - Then → "Print String" with IA_Move value + +#### d) Call the debug event from Event Tick: +- From "Event Tick" WHITE pin → search `DEBUG_PrintInput` +- This adds a node that calls your custom event + +**Visual (two separate areas in your graph):** + +``` +AREA 1 - Event Tick (will later have movement): +┌─────────────┐ ┌─────────────────────┐ +│ Event Tick │─────►│ DEBUG_PrintInput │ +└─────────────┘ │ (call custom event) │ + └─────────────────────┘ + +AREA 2 - Debug logic (completely separate): +┌─────────────────────┐ +│ DEBUG_PrintInput │ +│ (Custom Event) │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Branch │ +│ Condition: IA_Move != Last │ +└──────┬──────────────────────┘ + │ TRUE + ▼ +┌──────────────────┐ ┌──────────────┐ +│Set DEBUG_LastMove│────►│ Print String │ +└──────────────────┘ └──────────────┘ +``` + +#### e) Compile and Save +#### f) Drag BP_Player into level, Press Play (`Alt+P`) +#### g) IMPORTANT: Click inside the game viewport to give it keyboard focus! +#### h) Press WASD keys and look at top-left of screen + +**Expected:** Message appears ONLY when you press or release a key: +- Press W: prints "X=1.0 Y=0.0" +- Release W: prints "X=0.0 Y=0.0" +- Log is NOT spammed every frame + +**TO DISABLE DEBUG LATER:** Simply delete the "DEBUG_PrintInput" call node from Event Tick. The Custom Event and its logic can stay (unused) or be deleted entirely - movement will be unaffected either way. + +#### Debugging Steps (if you see "X=0.0 Y=0.0" always): + +
+DEBUGGING STEP 1 - Verify the pawn is possessed + +- When playing, look at the Outliner (right panel) +- Find BP_Player in the list +- If it shows a small controller icon next to it, it's possessed +- If NOT possessed: Check that "Auto Possess Player" = "Player 0" in BP_Player +
+ +
+DEBUGGING STEP 2 - Verify the game window has keyboard focus + +- After pressing Play, CLICK INSIDE THE GAME VIEWPORT +- The viewport must have focus to receive keyboard input +- Try pressing `Shift+F1` to release mouse, then click viewport again +- Then press WASD again +
+ +
+DEBUGGING STEP 3 - Test with a simpler Print String + +- Delete the current Print String setup +- From Event Tick, drag → Print String +- Type "Tick is running" in the In String field (just plain text) +- Play the game - you should see "Tick is running" spam in top-left +- If you DON'T see this: The blueprint isn't running at all +
+ +
+DEBUGGING STEP 4 - Test BeginPlay is running + +- From Event BeginPlay, AFTER the Add Mapping Context node +- Add another Print String with text "BeginPlay executed" +- Connect the white wire: BeginPlay → Add Mapping Context → Print String +- Play the game - you should see "BeginPlay executed" once at start +- If you DON'T see this: BeginPlay isn't running (pawn not spawned?) +
+ +
+DEBUGGING STEP 5 - Check if there are MULTIPLE BP_Player in level + +- Look in Outliner for multiple BP_Player instances +- Delete ALL of them +- Drag in exactly ONE fresh BP_Player from Content Drawer +- Try again +
+ +
+DEBUGGING STEP 6 - Nuclear option (create fresh test) + +- Create a NEW blueprint: "BP_InputTest" (Pawn class) +- Add ONLY: Auto Possess Player = Player 0 +- In Event Graph: Event BeginPlay → Add Mapping Context (IMC_Default) +- In Event Graph: Event Tick → Print String (connected to Get IA_Move) +- Drag this minimal test blueprint into level +- If THIS works, something is wrong with your BP_Player +
+ +**IF THIS TEST WORKS:** Continue to step 5. You can optionally delete the DEBUG_PrintInput call from Event Tick to stop the debug prints. +**IF THIS TEST FAILS:** Work through debugging steps above. + +### 5. MOVEMENT LOGIC - Create this node network + +> **NOTE:** If you added debug from step 4, Event Tick already has a wire to DEBUG_PrintInput. To have BOTH debug AND movement run from Event Tick, you need a Sequence node (one output pin = one wire only). + +#### a) INSERT A SEQUENCE NODE between Event Tick and debug: +- Delete the wire from Event Tick to DEBUG_PrintInput +- Right-click → search `Sequence` → add it +- Connect Event Tick → Sequence (input) +- Connect Sequence "Then 0" → DEBUG_PrintInput +- Sequence "Then 1" will be used for movement + +``` +RESULT: +Event Tick → Sequence ─┬─ Then 0 → DEBUG_PrintInput + │ + └─ Then 1 → (movement, next steps) +``` + +If you skipped debug, just connect Event Tick directly to movement. + +#### b) Starting point for movement: +- If using Sequence: drag from "Then 1" output +- If no debug: drag from Event Tick directly + +#### c) Right-click → search `Get Player Controller` and add it +(This is a NEW Get Player Controller - separate from the one in debug) + +#### d) From Player Controller output, drag → `Get Enhanced Input Local Player Subsystem` + +#### e) From subsystem output, drag and right-click → search `IA_Move` +- Look under "Input" → "Enhanced Action Values" → select "IA_Move" +- This node returns the current value of the IA_Move input action + +#### f) The output is an Input Action Value (Vector2D since we set IA_Move to Axis2D). +Right-click the output pin → "Split Struct Pin" to get X and Y components, +OR drag from output and search "To Vector 2D" to convert it + +#### g) Right-click → `Make Vector` +- Connect the X from movement input to X +- Connect the Y from movement input to Y +- Set Z to 0 + +#### h) Right-click → `Normalize` +- Connect the vector output to Normalize input + +#### i) Right-click → `Get World Delta Seconds` + +#### j) Right-click → `Get MoveSpeed` (your variable) + +#### k) Right-click → Multiply (float * float) +- Connect Delta Seconds output to first input +- Connect MoveSpeed output to second input + +#### l) Right-click → Multiply (vector * float) +- Connect Normalized vector (from step h) to vector input +- Connect (DeltaSeconds * MoveSpeed) result (from step k) to float input +- This output is the "movement delta" - how far to move this frame + +#### m) Right-click → `Get Actor Location` + +#### n) Right-click → Add (vector + vector) +- Connect current location (from step m) to first input +- Connect movement delta (from step l) to second input + +#### o) CLAMP X COORDINATE: + +First, break apart the new position vector from step n: +- Right-click on the output pin of the Add node (from step n) → "Split Struct Pin" +- This splits the vector into three separate pins: X, Y, Z + +Now clamp the X value: +- Right-click in empty space → search `Clamp (float)` → add it +- Connect the "X" output (from the split Add node) to "Value" input of Clamp + +Get the min bound: +- Right-click → search `Get BoundsMin` (your variable) → add it +- Right-click on BoundsMin output pin → "Split Struct Pin" (splits into X, Y) +- Connect BoundsMin's "X" to Clamp's "Min" input + +Get the max bound: +- Right-click → search `Get BoundsMax` (your variable) → add it +- Right-click on BoundsMax output pin → "Split Struct Pin" +- Connect BoundsMax's "X" to Clamp's "Max" input + +The Clamp node now outputs the X position clamped within bounds. + +#### p) CLAMP Y COORDINATE: +- Right-click → add another `Clamp (float)` node +- Connect the "Y" output (from the split Add node in step o) to "Value" +- Connect BoundsMin's "Y" (already split) to "Min" +- Connect BoundsMax's "Y" (already split) to "Max" + +#### q) Right-click → `Make Vector` +- Connect clamped X +- Connect clamped Y +- Set Z to 0 + +#### r) Right-click → `Set Actor Location` +- Connect the clamped vector to "New Location" + +#### s) CRITICAL - Connect the execution wire (white wire): +- If using Sequence (from step a): drag from "Then 1" to Set Actor Location +- If no debug: drag from Event Tick to Set Actor Location +- Without this execution wire, the movement code NEVER runs! + +**Final Structure (with debug - using Sequence node):** +``` +Event Tick → Sequence ─┬─ Then 0 → DEBUG_PrintInput + │ + └─ Then 1 → [movement] → Set Actor Location +``` + +**Final Structure (without debug):** +``` +Event Tick → [movement logic] → Set Actor Location +``` + +**TO REMOVE DEBUG:** Delete the wire from "Then 0" to DEBUG_PrintInput. Movement on "Then 1" is completely unaffected. + +### 6. Click Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark (no errors) +- No warnings about unconnected pins + +### How to Test at This Stage: +1. Open any level (or the default "Untitled" level) +2. From Content Drawer, drag BP_Player into the viewport +3. Press Play (`Alt+P`) +4. Click inside the game viewport to give it keyboard focus +5. You should see the cube (TempVisual added in Step 2.2) and control it with WASD + +### Expected Result when tested: +- Player pawn moves smoothly when pressing WASD keys +- Movement is frame-rate independent (consistent speed) +- Player cannot move outside the screen bounds (stops at edges) +- Releasing keys immediately stops movement (no drift) + +> **NOTE:** Full game testing will be possible after completing [Part 8 (Level Setup)](part-8-game-mode-level.md). + +### Visual Diagram of Movement Nodes: + +``` +┌─────────────┐ ┌──────────┐ +│ Event Tick │────►│ Sequence │─┬─ Then 0 ──► DEBUG_PrintInput (optional) +└─────────────┘ └──────────┘ │ + └─ Then 1 ──► Movement logic ──► Set Actor Location + +(If no debug, skip Sequence and connect Event Tick directly to movement) + +┌──────────────────────────────────┐ ┌────────────────────┐ +│ Get Enhanced Input Subsystem │ │ Set Actor Location │ +│ → Get Action Value (IA_Move) │ └────────────────────┘ +│ → Get as Vector2D │ ▲ +└────────────────┬─────────────────┘ │ + │ ┌───────────────┐ + ▼ │ Clamped Vector│ + ┌────────────────────────────┐ └───────────────┘ + │ Make Vector │───►Normalize───► ▲ + │ X=Input.X, Y=Input.Y, Z=0 │ (direction) │ + └────────────────────────────┘ │ ┌─────────────┐ + ▼ │ + Location │ + ┌──────────────┐ └─────────────┘ + │ * MoveSpeed │─────────┘ + │ * DeltaTime │ + └──────────────┘ +``` + +--- + +## Step 2.5: Create Player Firing Logic + +1. Continue in Event Graph, we'll add firing after movement + +2. After the movement logic, add firing check using Enhanced Input: + +#### a) Right-click → search `IA_Fire` +- Look under "Input" → "Enhanced Action Events" category (diamond ◇ icons) +- Select "IA_Fire" (the one with diamond icon, NOT the square icon) +- This creates an EVENT node (red, like Event Tick) that fires when the button is pressed + +> **NOTE:** +> - "Enhanced Action Events" (diamond ◇) = triggers when button pressed +> - "Enhanced Action Values" (square □) = reads current value continuously +> +> For firing, we want the EVENT. + +#### b) From the "Triggered" execution pin, build the fire rate limiter: + +1. **Get current timer value:** + - Right-click → `Get FireTimer` (your variable) + +2. **Subtract frame time from it:** + - Right-click → `Get World Delta Seconds` (time since last frame, ~0.016 at 60fps) + - Right-click → search "float - float" or "Subtract" → add Subtract node + - Connect FireTimer to the TOP input (A) + - Connect Delta Seconds to the BOTTOM input (B) + - Result = FireTimer minus DeltaSeconds + +3. **Store the new timer value:** + - Right-click → `Set FireTimer` + - Connect the Subtract result to it + - Connect execution wire: Triggered → Set FireTimer + +4. **Check if we can fire:** + - From Set FireTimer, drag execution → `Branch` + - Right-click → `<= (float)` (less than or equal) + - Connect FireTimer (use the output from Set, or Get it again) to top input + - Type `0` in bottom input + - Connect result to Branch's Condition + +5. **On TRUE branch (timer expired, can fire):** + - Right-click → `Set FireTimer` + - Right-click → `Get FireInterval` (your variable) + - Connect FireInterval to Set FireTimer (resets the cooldown) + - For now, add a Print String with text "FIRE!" to verify it works + - Leave the execution wire after this Set FireTimer OPEN (we'll connect FireVolley function here in [Step 3.4](part-3-create-bullet.md#step-34-complete-player-firing-logic-bp_player)) + +**Visual:** + +``` +┌──────────────────┐ +│ IA_Fire Triggered│ +└────────┬─────────┘ + │ (white wire) + ▼ +┌─────────────────────────────────────┐ +│ Set FireTimer │ +│ = FireTimer - GetWorldDeltaSeconds │ +└────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────┐ TRUE ┌─────────────────────┐ +│ Branch │───────────►│ Set FireTimer │──► Print "FIRE!" +│ FireTimer <= 0 │ │ = FireInterval │ (placeholder) +└────────┬────────┘ └─────────────────────┘ + │ FALSE + ▼ + (do nothing) +``` + +**TEST NOW:** Press Play, hold Z key. You should see "FIRE!" spam in top-left at rapid intervals (every 0.08 seconds = FireInterval). + +> **NOTE:** Since IA_Fire was set up with Z key and Left Mouse Button in the Input Mapping Context, both inputs will trigger this action automatically. + +### 3. PLACEHOLDER FOR BULLET SPAWNING + +The Print String "FIRE!" is a placeholder. We will replace it with actual bullet spawning in [STEP 3.4](part-3-create-bullet.md#step-34-complete-player-firing-logic-bp_player), AFTER creating BP_Bullet in Part 3. + +Leave the execution wire open after the "Set FireTimer = FireInterval" node. We'll connect FireVolley function there in STEP 3.4. + +### 4. Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Fire rate limiter logic works (Print String spams when holding Z) +- Actual bullets will be added in [STEP 3.4](part-3-create-bullet.md#step-34-complete-player-firing-logic-bp_player) after BP_Bullet is created + +--- + +## Step 2.6: Create Player Damage and Special Ability + +> **IMPORTANT - Dependency Note:** +> The `HandleDeath` function needs to call `BP_GameDirector`, which we create in [Part 6](part-6-game-director.md). +> In this step, we will create **placeholder functions** that compile but aren't fully functional. +> We will complete `HandleDeath` in [Part 6](part-6-game-director.md#step-63-game-director-update-logic) after GameDirector exists. + +--- + +### 1. CREATE "HandleDeath" FUNCTION (Placeholder): + +We create this FIRST because `TakeHit` will call it. For now, it's a placeholder. + +#### a) Create the function: +1. In My Blueprint panel (left side), find "Functions" section +2. Click the **"+"** button next to "Functions" +3. Name it `HandleDeath` +4. Double-click to open the function graph + +#### b) Add placeholder logic (will be completed in Part 6): + +1. The function graph opens with a purple "HandleDeath" entry node + +2. **Hide the player:** + - From the entry node's WHITE execution pin, drag and search `Set Actor Hidden in Game` + - Check the "New Hidden" checkbox (set to TRUE) + - This makes the player invisible when they die + +3. **Add a placeholder comment:** + - Right-click in empty space → search `Add Comment` (or press C) + - Type: "TODO: Call GameDirector.HandlePlayerDeath (Part 6)" + - Position it near the end of the function + +4. **Connect execution:** + ``` + HandleDeath (entry) ──► Set Actor Hidden in Game + │ + ▼ + [TODO comment - no connection needed] + ``` + +5. Click "Compile" - should show GREEN checkmark + +> **NOTE:** This function is incomplete! We'll add the GameDirector call in [Part 6, Step 6.3](part-6-game-director.md#step-63-game-director-update-logic). + +--- + +### 2. CREATE "TakeHit" FUNCTION: + +Now we can create TakeHit, which calls HandleDeath. + +#### a) Create the function: +1. In My Blueprint panel → Functions → click **"+"** +2. Name it `TakeHit` +3. Double-click to open the function graph + +#### b) Add input parameter: +1. With the function graph open, look at the Details panel (right side) +2. Find "Inputs" section +3. Click **"+"** to add a new input +4. Name: `Damage` +5. Type: `Integer` +6. The purple entry node now shows a "Damage" output pin + +#### c) Build the function logic: + +**Step 1 - Get current lives:** +- Right-click in empty space → search `Get CurrentLives` → add it +- This reads the CurrentLives variable value + +**Step 2 - Subtract damage from lives:** +- Right-click → search `integer - integer` or `Subtract (int)` → add it +- Connect `CurrentLives` (from Step 1) to the TOP input (A) +- Connect `Damage` (from the purple entry node) to the BOTTOM input (B) +- Result = CurrentLives minus Damage + +**Step 3 - Clamp the result (prevent negative lives):** +- Right-click → search `Clamp (int)` → add the "Clamp (integer)" node +- Connect the Subtract result (from Step 2) to the "Value" input +- Set "Min" to `0` (type directly in the field) +- Set "Max" to `99` (or any high number - we just want to prevent negatives) +- The output is the clamped value (0 or higher) + +**Step 4 - Store the new lives value:** +- Right-click → search `Set CurrentLives` → add it +- Connect the Clamp output (from Step 3) to the input +- **IMPORTANT:** Connect execution wire from entry node → Set CurrentLives + +**Step 5 - Check if player should die:** +- From `Set CurrentLives`, drag execution → search `Branch` → add it +- Right-click → search `<= (integer)` (less than or equal) → add it +- Connect `CurrentLives` (drag from Set node's output, or add new Get) to TOP input +- Type `0` in BOTTOM input +- Connect the `<=` result (boolean) to Branch's "Condition" input + +**Step 6 - On TRUE (player is dead):** +- From Branch's "True" execution pin, drag → search `HandleDeath` → add it +- This calls the function we created in step 1 + +**Step 7 - On FALSE (player still alive):** +- Leave the "False" pin unconnected (do nothing, player survives) + +#### d) Visual diagram of TakeHit function: + +``` +┌────────────────────────┐ +│ TakeHit (entry) │ +│ Damage ○───────┼──────────────────────┐ +└───────────┬────────────┘ │ + │ (white execution wire) │ + ▼ ▼ +┌───────────────────────────────────────────────────────┐ +│ Set CurrentLives │ +│ = Clamp( CurrentLives - Damage, Min=0, Max=99 ) │ +└───────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Branch │ +│ Condition: CurrentLives <= 0│ +└───────┬─────────────┬───────┘ + │ TRUE │ FALSE + ▼ ▼ +┌───────────────┐ (nothing) +│ HandleDeath │ +└───────────────┘ +``` + +#### e) Click Compile and Save + +--- + +### 3. SPECIAL ABILITY (Screen Clear) - Input Setup Only: + +> **IMPORTANT - Dependency Note:** +> The special ability needs to destroy BP_Enemy and BP_Bullet actors, which don't exist yet. +> In this step, we create ONLY the input handling and mark-as-used logic. +> We will complete the destruction logic in [Part 4, Step 4.6](part-4-create-enemy.md#step-46-complete-special-ability-in-bp_player) after BP_Enemy exists. + +Using Enhanced Input (IA_Special was already set up with X and Right Mouse in Step 2.4): + +#### a) Create the event node: +1. Go back to the **Event Graph** tab (not inside a function) +2. Right-click in empty space → search `EnhancedInputAction IA_Special` + - Look under "Input" → "Enhanced Action Events" category + - Select "IA_Special" (the EVENT with diamond ◇ icon) +3. This creates a red event node that fires when X or Right Mouse is pressed + +#### b) Check if special was already used: +1. From the "Triggered" execution pin, drag → search `Branch` → add it +2. Right-click → search `Get SpecialUsed` → add it +3. Right-click → search `NOT Boolean` → add it +4. Connect SpecialUsed → NOT → Branch Condition + - We branch on "NOT SpecialUsed" (true = hasn't been used yet) + +#### c) On TRUE (special available) - placeholder: +1. From Branch's "True" pin, drag → search `Set SpecialUsed` → add it +2. Check the box to set it to TRUE (marks ability as used) + +3. **Add placeholder Print String:** + - From Set SpecialUsed, drag → search `Print String` → add it + - Type: "SPECIAL ABILITY ACTIVATED!" in the In String field + - This verifies the input works; we'll replace it with actual destruction in Part 4 + +4. **Add a placeholder comment:** + - Right-click in empty space → search `Add Comment` (or press C) + - Type: "TODO: Add enemy/bullet destruction (Part 4)" + - Position it after the Print String + +#### d) On FALSE (special already used): +- Leave unconnected (nothing happens on second press) + +#### e) Visual diagram (placeholder version): + +``` +┌─────────────────────────────┐ +│ EnhancedInputAction │ +│ IA_Special - Triggered │ +└───────────┬─────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Branch │ +│ Condition: NOT SpecialUsed │ +└───────┬─────────────┬───────┘ + │ TRUE │ FALSE + ▼ ▼ +┌───────────────┐ (nothing - ability already used) +│Set SpecialUsed│ +│ = true │ +└───────┬───────┘ + │ + ▼ +┌────────────────────────────────────┐ +│ Print String │ +│ "SPECIAL ABILITY ACTIVATED!" │ +└────────────────────────────────────┘ + │ + ▼ + [TODO comment - destruction logic added in Part 4] +``` + +#### f) Compile and Save + +#### g) Test the placeholder: +1. Press Play +2. Press X key (or Right Mouse Button) +3. You should see "SPECIAL ABILITY ACTIVATED!" appear once +4. Pressing X again should do nothing (SpecialUsed = true) + +> **REMINDER:** Complete the special ability with actual enemy/bullet destruction in [Part 4, Step 4.6](part-4-create-enemy.md#step-46-complete-special-ability-in-bp_player) after creating BP_Enemy. + +--- + +### 4. Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "TakeHit" and "HandleDeath" functions appear under Functions in My Blueprint panel +- Event Graph shows IA_Special event with branching logic + +### Expected Result in Play mode (at this stage): +- Press X key: "SPECIAL ABILITY ACTIVATED!" appears once in top-left +- Press X again: Nothing happens (SpecialUsed is now true) +- TakeHit and HandleDeath functions exist but cannot be tested yet (nothing calls them) + +> **NOTE:** Full testing of damage/death systems requires: +> - BP_Bullet collision (Part 3) to call TakeHit +> - BP_GameDirector (Part 6) for HandleDeath to notify +> - BP_Enemy (Part 4) for special ability to destroy + +> **REMINDER:** Complete the `HandleDeath` function in [Part 6, Step 6.3](part-6-game-director.md#step-63-game-director-update-logic) after creating BP_GameDirector. + +--- + +[← Previous: Part 1 - Project Setup](part-1-project-setup.md) | [Back to Index](README.md) | [Next: Part 3 - Create the Bullet →](part-3-create-bullet.md) diff --git a/games/unreal/tutorial/part-3-create-bullet.md b/games/unreal/tutorial/part-3-create-bullet.md new file mode 100644 index 0000000..dc0efb8 --- /dev/null +++ b/games/unreal/tutorial/part-3-create-bullet.md @@ -0,0 +1,458 @@ +# Part 3: Create the Bullet + +[← Previous: Part 2 - Create the Player](part-2-create-player.md) | [Back to Index](README.md) | [Next: Part 4 - Create the Enemy →](part-4-create-enemy.md) + +--- + +## Step 3.1: Create Bullet Blueprint + +1. In Content Browser → Blueprints folder +2. Right-click → **Blueprint Class** → **Actor** +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](part-9-final-setup.md) + +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: +- Get TravelDirection +- Multiply by TravelSpeed +- Multiply by Delta Seconds +- Add to current location +- Set Actor Location + +#### b) Check lifetime: +- Subtract Delta Seconds from RemainingLifetime +- If <= 0: Destroy Actor + +### 2. CREATE "Initialize" FUNCTION: + +#### a) Add input parameters: +| Parameter | Type | Default | +|-----------|------|---------| +| `Direction` | Vector | - | +| `Speed` | Float | - | +| `bIsEnemy` | Boolean | - | +| `Lifetime` | Float | - | +| `DamageValue` | Integer | 1 | + +#### b) Inside: +- Normalize Direction → Set TravelDirection +- Set TravelSpeed = Speed +- Set IsEnemyProjectile = bIsEnemy +- Set RemainingLifetime = Lifetime +- Set Damage = DamageValue + +### 3. Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "Initialize" function appears under Functions with 5 input parameters + +### Expected Result in Play mode (when spawned by player): +- Bullets move in their assigned direction at TravelSpeed +- Bullets automatically destroy after RemainingLifetime seconds (default 4s) +- Movement is smooth and frame-rate independent + +--- + +## Step 3.3: Bullet Collision Logic + +1. Click on "BulletCollision" component in Components panel + +2. In Details panel, scroll to "Events" section + - Click "+" next to **"On Component Begin Overlap"** + - This creates event node in Event Graph + +3. In the Event Graph, from "On Component Begin Overlap": + +#### a) First, check if this is enemy projectile: +- Get IsEnemyProjectile +- Branch + +#### b) IF IS ENEMY PROJECTILE (True branch): +- Get Other Actor from the overlap event +- Cast to BP_Player +- If cast succeeds: + - Call TakeHit on player, passing Damage + - Destroy this bullet (Destroy Actor) + +#### c) IF IS PLAYER BULLET (False branch): +- Get Other Actor +- Cast to BP_Enemy +- If cast succeeds: + - Call ApplyDamage on enemy, passing Damage + - Destroy this bullet + +### 4. Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Event Graph shows "On Component Begin Overlap" event connected to Branch + +### Expected Result in Play mode: +- Player bullets (IsEnemyProjectile=false) hitting enemies: + - Enemy takes damage, bullet disappears +- Enemy bullets (IsEnemyProjectile=true) hitting player: + - Player takes hit, bullet disappears +- Bullets passing through other objects: No effect (bullets ignore non-targets) + +--- + +## 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 to "Class" pin + +3. For the "Spawn Transform" input: + - Right-click on the "Spawn Transform" pin → "Split Struct Pin" + - This splits it into Location, Rotation, Scale + - Connect "SpawnLocation" (from entry node) to "Spawn Transform Location" + - Leave Rotation and Scale at defaults + +4. Connect execution wire: + - Drag from SpawnBullet entry node → Spawn Actor from Class + +#### f) Initialize the spawned bullet: + +5. The "Spawn Actor from Class" has a "Return Value" output (the spawned actor) + - Drag from Return Value → search `Cast to BP_Bullet` + - This works now because BP_Bullet exists! + +6. From the cast's "As BP Bullet" output, drag → search `Initialize` + - This calls the Initialize function on the bullet (created in Step 3.2) + - Connect "Direction" (from entry node) to Initialize's Direction input + - Right-click → `Get BulletSpeed` → connect to Speed input + - For "bIsEnemy" input: leave unchecked (false) - player bullets aren't enemy + - For "Lifetime": type `4.0` (or leave default) + +7. Connect execution wires: + - Spawn Actor → Cast to BP_Bullet → Initialize + +**Visual:** + +``` +┌───────────────────────┐ ┌─────────────────────────┐ +│ SpawnBullet │─────►│ Spawn Actor from Class │ +│ SpawnLocation ○ │ │ Class: BulletClass │ +│ Direction ○ │ │ Location: SpawnLocation │ +└───────────────────────┘ └───────────┬─────────────┘ + │ + ▼ + ┌─────────────────────────┐ + │ Cast to BP_Bullet │ + └───────────┬─────────────┘ + │ + ▼ + ┌─────────────────────────┐ + │ Initialize │ + │ Direction: Direction │ + │ Speed: BulletSpeed │ + │ bIsEnemy: false │ + └─────────────────────────┘ +``` + +#### 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:** + +5. From Branch TRUE pin, drag execution → right-click → search `SpawnBullet` (the function you created in step 3 above) + +6. For SpawnBullet's "Spawn Location" input: + - Right-click → `Get Actor Location` + - Connect the output to Spawn Location + +7. 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 + +8. 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:** + +9. 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` + +10. **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) + +11. **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 + +12. **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. + +13. **Convert angle from degrees to a direction vector:** + + In Unreal's top-down view (looking down Z axis): + - X axis = forward/back (up/down on screen) + - Y axis = left/right + - Angle 0° = straight up = direction (1, 0, 0) + - Angle 90° = right = direction (0, 1, 0) + + ``` + Direction.X = Cos(angle) + Direction.Y = Sin(angle) + Direction.Z = 0 + ``` + + BUT Unreal's Sin/Cos use RADIANS, not degrees! + + **a) Convert degrees to radians:** + - Right-click → search `Radians` or `Degrees to Radians` + - Connect your angle (from step 12) to input + - Output is angle in radians + + **b) Calculate X component (Cos):** + - Right-click → `Cos (Radians)` + - Connect radians output + - This is the X direction component + + **c) Calculate Y component (Sin):** + - Right-click → `Sin (Radians)` + - Connect radians output (same value as Cos input) + - This is the Y direction component + + **d) Make the direction vector:** + - Right-click → `Make Vector` + - Connect Cos result to X + - Connect Sin result to Y + - Set Z = 0 + +14. **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 + +15. **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](part-2-create-player.md#step-25-create-player-firing-logic) +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](part-2-create-player.md) | [Back to Index](README.md) | [Next: Part 4 - Create the Enemy →](part-4-create-enemy.md) diff --git a/games/unreal/tutorial/part-4-create-enemy.md b/games/unreal/tutorial/part-4-create-enemy.md new file mode 100644 index 0000000..c944085 --- /dev/null +++ b/games/unreal/tutorial/part-4-create-enemy.md @@ -0,0 +1,349 @@ +# Part 4: Create the Enemy + +[← Previous: Part 3 - Create the Bullet](part-3-create-bullet.md) | [Back to Index](README.md) | [Next: Part 5 - Create Enemy Spawner →](part-5-create-spawner.md) + +--- + +## Step 4.1: Create Enemy Blueprint + +1. Content Browser → Blueprints +2. Right-click → **Blueprint Class** → **Actor** +3. Name: `BP_Enemy` +4. Double-click to open + +5. **Add Components:** + - Paper Sprite → `EnemySprite` + - Box Collision → `EnemyCollision` + - Box Extent: `X=30, Y=30, Z=10` + - Generate Overlap Events: **CHECKED** + - Cube (Static Mesh) → name `TempVisual` + - Scale: `(0.6, 0.6, 0.1)` - larger than player cube + - This provides visibility until proper visuals in [Part 9](part-9-final-setup.md) + +6. **Create Variables:** + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `MaxHealth` | Integer | 12 | +| `CurrentHealth` | Integer | 12 | +| `ScoreValue` | Integer | 50 | +| `VerticalSpeed` | Float | 220.0 | +| `HorizontalAmplitude` | Float | 250.0 | +| `HorizontalFrequency` | Float | 1.8 | +| `DespawnY` | Float | -750.0 | +| `FireInterval` | Float | 0.35 | +| `BulletsPerBurst` | Integer | 20 | +| `BurstSpread` | Float | 360.0 | +| `EnemyBulletSpeed` | Float | 1000.0 | +| `EnemyBulletLifetime` | Float | 6.0 | +| `BaseX` | Float | 0.0 | +| `WaveSeed` | Float | 0.0 | +| `FireTimer` | Float | 0.0 | +| `BulletClass` | Class Reference to Actor | (set later) | + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Components panel shows: DefaultSceneRoot → EnemySprite, EnemyCollision, TempVisual +- Variables panel shows all 16 variables with correct types and defaults + +### Expected Result in Viewport (Blueprint Editor): +- Cube visible (the TempVisual placeholder, larger than player) +- Box collision visible (30x30x10) + +--- + +## Step 4.2: Enemy Initialization + +1. In Event Graph, from **"Event BeginPlay":** + +#### a) Set CurrentHealth = MaxHealth + +#### b) Get Actor Location → Break Vector → Set BaseX = X value + +#### c) Random Float in Range (0, 6.28) → Set WaveSeed +(6.28 ≈ 2π for wave randomization) + +#### d) Random Float in Range (0, FireInterval) → Set FireTimer + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- BeginPlay event connected to variable setters + +### Expected Result in Play mode: +- Each enemy spawns with randomized WaveSeed (0 to 6.28) +- Each enemy starts with different FireTimer offset +- This creates varied, non-synchronized enemy behavior + +--- + +## Step 4.3: Enemy Movement Logic + +1. In Event Graph, from **"Event Tick":** + +#### a) VERTICAL MOVEMENT: +- Get Actor Location → Break into X, Y, Z +- Subtract (VerticalSpeed * DeltaSeconds) from Y + +#### b) HORIZONTAL SINE WAVE: +- Get Game Time in Seconds +- Add WaveSeed +- Multiply by HorizontalFrequency +- Calculate Sine of result +- Multiply by HorizontalAmplitude +- Add to BaseX +- This becomes new X position + +#### c) Set Actor Location with new X, Y (Z stays 0) + +#### d) DESPAWN CHECK: +- If Y < DespawnY: Destroy Actor +- If Abs(X) > 1400: Destroy Actor + +**Visual Diagram:** + +``` +┌─────────────┐ +│ Event Tick │ +└──────┬──────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ Get Location │ +│ NewY = Y - (VerticalSpeed * DeltaSeconds) │ +│ NewX = BaseX + Sin(Time * Freq) * Amplitude │ +│ Set Location (NewX, NewY, 0) │ +└──────────────────────────────────────────────┘ +``` + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Event Tick connected to movement and despawn logic + +### Expected Result in Play mode: +- Enemies drift downward at VerticalSpeed (220 units/sec) +- Enemies oscillate horizontally in sine wave pattern +- Each enemy has different horizontal phase (due to WaveSeed) +- Enemies disappear when Y < -750 or |X| > 1400 + +--- + +## Step 4.4: Enemy Firing Logic + +### 1. Continue in Event Tick (after movement): + +#### a) Decrease FireTimer by DeltaSeconds + +#### b) Branch: if FireTimer <= 0: +- Reset FireTimer to FireInterval +- Call "FireBurst" function + +### 2. CREATE "FireBurst" FUNCTION: + +#### a) Add function "FireBurst" + +#### b) Inside: +- Get BulletsPerBurst +- Calculate angle step: `360 / BulletsPerBurst` (for full circle) + - OR: `BurstSpread / (BulletsPerBurst - 1)` (for partial arc) + +#### c) For Loop from 0 to BulletsPerBurst - 1: +- Calculate angle: `i * AngleStep` +- Convert to direction vector: + - X = Sin(angle in radians) + - Y = -Cos(angle in radians) ← negative because enemies fire DOWN +- Spawn bullet: + - Spawn Actor from Class (BP_Bullet) + - Get spawned bullet, call Initialize: + - Direction = calculated direction + - Speed = EnemyBulletSpeed + - bIsEnemy = **true** + - Lifetime = EnemyBulletLifetime + +**Visual: Radial bullet pattern (20 bullets in 360°)** + +``` + ↑ + ↗ | ↖ + ↗ | ↖ + →───────●───────← + ↘ | ↙ + ↘ | ↙ + ↓ +``` + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "FireBurst" function appears under Functions + +### Expected Result in Play mode: +- Every 0.35 seconds (FireInterval), enemy fires bullet burst +- 20 bullets spawn in a full 360° circle pattern +- Bullets travel outward from enemy position +- Different enemies fire at different times (randomized FireTimer) + +--- + +## Step 4.5: Enemy Damage and Death + +### 1. CREATE "ApplyDamage" FUNCTION: + +#### a) Add input: `DamageAmount` (Integer) + +#### b) Inside: +- Subtract DamageAmount from CurrentHealth +- If CurrentHealth <= 0: + - Call HandleDeath + +### 2. CREATE "HandleDeath" FUNCTION: + +#### a) Inside: +- Get reference to ScoreManager (we'll create in [Part 7](part-7-score-manager-ui.md)) +- Call AddScore, passing ScoreValue +- Spawn death effect (optional) +- Destroy Actor + +### 3. COLLISION WITH PLAYER: + +#### a) On the EnemyCollision component overlap event: +- Cast Other Actor to BP_Player +- If successful: + - Call TakeHit(1) on player + - Call HandleDeath() on self + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "ApplyDamage" and "HandleDeath" functions appear under Functions +- EnemyCollision has overlap event in Event Graph + +### Expected Result in Play mode: +- Player bullets hitting enemy: Enemy health decreases +- After 12 hits (MaxHealth): Enemy disappears, score increases by 50 +- Player colliding with enemy: Player takes 1 damage, enemy dies + +--- + +## Step 4.6: Complete Special Ability in BP_Player + +Now that BP_Enemy exists (and BP_Bullet from Part 3), we can complete the special ability that was set up as a placeholder in [Part 2, Step 2.6](part-2-create-player.md#step-26-create-player-damage-and-special-ability). + +### 1. Open BP_Player Blueprint: +1. Content Browser → Blueprints → double-click `BP_Player` +2. Go to **Event Graph** tab +3. Find the `EnhancedInputAction IA_Special` event node (created in Step 2.6) +4. Locate the `Print String "SPECIAL ABILITY ACTIVATED!"` node + +### 2. Replace Print String with destruction logic: + +#### a) Delete the placeholder: +- Select the `Print String` node +- Press Delete +- (Keep the TODO comment if you want, or delete it too) + +#### b) Destroy all enemies: +1. From `Set SpecialUsed` output execution pin, drag → search `Get All Actors of Class` → add it +2. Click the "Actor Class" dropdown → search and select `BP_Enemy` +3. The output "Out Actors" is an array of all enemies currently in the level + +#### c) Loop through enemies and destroy them: +1. From `Get All Actors of Class`, drag execution → search `For Each Loop` → add it +2. Connect the "Out Actors" array (blue pin) to the loop's "Array" input (blue pin) +3. From "Loop Body" execution pin, drag → search `Destroy Actor` → add it +4. Connect "Array Element" (blue pin - the current enemy in the loop) to Destroy Actor's "Target" input + +#### d) Now destroy enemy bullets (after enemies are done): +1. From `For Each Loop`'s **"Completed"** execution pin (NOT "Loop Body"), drag → search `Get All Actors of Class` → add it +2. Click "Actor Class" dropdown → select `BP_Bullet` +3. From this Get All Actors, drag execution → search `For Each Loop` → add another loop + +#### e) Filter to only destroy ENEMY bullets (keep player bullets): +1. From the second loop's "Loop Body" pin, drag → search `Branch` → add it +2. From "Array Element" (the current bullet), drag → search `Get IsEnemyProjectile` → add it + - This reads the IsEnemyProjectile variable from BP_Bullet +3. Connect `IsEnemyProjectile` output (boolean) to Branch's "Condition" input + +#### f) Destroy only if it's an enemy bullet: +1. From Branch's **"True"** pin, drag → search `Destroy Actor` → add it +2. Connect "Array Element" (the bullet) to Destroy Actor's "Target" +3. Leave Branch's "False" pin unconnected (player bullets are preserved) + +### 3. Visual diagram of completed special ability: + +``` +┌─────────────────────────────┐ +│ EnhancedInputAction │ +│ IA_Special - Triggered │ +└───────────┬─────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ Branch │ +│ Condition: NOT SpecialUsed │ +└───────┬─────────────┬───────┘ + │ TRUE │ FALSE + ▼ ▼ +┌───────────────┐ (nothing - ability already used) +│Set SpecialUsed│ +│ = true │ +└───────┬───────┘ + │ + ▼ +┌────────────────────────────┐ +│ Get All Actors of Class │ +│ Actor Class: BP_Enemy │ +│ Out Actors ○───────────────┼──────────┐ +└───────────┬────────────────┘ │ + │ │ + ▼ ▼ +┌────────────────────────────┐ ┌─────────────┐ +│ For Each Loop │──►│Destroy Actor│ +│ Array ○──────────────────┼───│Target: Enemy│ +│ │ └─────────────┘ +│ Loop Body ───────────────┼───────────┘ +│ Completed ───────────────┼───┐ +└────────────────────────────┘ │ + │ + ┌────────────────────┘ + ▼ +┌────────────────────────────┐ +│ Get All Actors of Class │ +│ Actor Class: BP_Bullet │ +│ Out Actors ○───────────────┼──────────┐ +└───────────┬────────────────┘ │ + │ │ + ▼ ▼ +┌────────────────────────────┐ ┌─────────────────────────────────┐ +│ For Each Loop │──►│ Get IsEnemyProjectile │ +│ Array ○──────────────────┼───│ (from Array Element) │ +│ │ └───────────────┬─────────────────┘ +│ Loop Body ───────────────┼───────────────────┘ +└────────────────────────────┘ │ + ▼ + ┌─────────────────────────────┐ + │ Branch │ + │ Condition: IsEnemyProjectile│ + └───────┬─────────────┬───────┘ + │ TRUE │ FALSE + ▼ ▼ + ┌───────────────┐ (skip - player bullet) + │ Destroy Actor │ + │ Target: Bullet│ + └───────────────┘ +``` + +### 4. Compile and Save + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- No warnings about missing classes (BP_Enemy and BP_Bullet now exist) + +### Expected Result in Play mode: +- Press X (or Right Mouse Button) once: + - ALL enemies on screen instantly disappear + - ALL enemy bullets (red) instantly disappear + - Player bullets (yellow) remain unaffected + - "SPECIAL ABILITY ACTIVATED!" no longer prints (we deleted it) +- Press X again: Nothing happens (SpecialUsed = true, one use only) + +--- + +[← Previous: Part 3 - Create the Bullet](part-3-create-bullet.md) | [Back to Index](README.md) | [Next: Part 5 - Create Enemy Spawner →](part-5-create-spawner.md) diff --git a/games/unreal/tutorial/part-5-create-spawner.md b/games/unreal/tutorial/part-5-create-spawner.md new file mode 100644 index 0000000..1581b5d --- /dev/null +++ b/games/unreal/tutorial/part-5-create-spawner.md @@ -0,0 +1,142 @@ +# Part 5: Create Enemy Spawner + +[← Previous: Part 4 - Create the Enemy](part-4-create-enemy.md) | [Back to Index](README.md) | [Next: Part 6 - Game Director →](part-6-game-director.md) + +--- + +## Step 5.1: Create Spawner Blueprint + +1. Content Browser → Blueprints +2. Right-click → **Blueprint Class** → **Actor** +3. Name: `BP_EnemySpawner` +4. Double-click to open + +5. **Create Variables:** + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `EnemyClass` | Class Reference | (will reference BP_Enemy) | +| `SpawnAreaHalfWidth` | Float | 900.0 | +| `GameDuration` | Float | 300.0 (5 minutes) | +| `MaxSimultaneousEnemies` | Integer | 120 | +| `ElapsedTime` | Float | 0.0 | +| `SpawnTimer` | Float | 0.0 | +| `SpawningActive` | Boolean | true | + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Variables panel shows all 7 variables with correct types +- No components needed (spawner is invisible logic actor) + +--- + +## Step 5.2: Spawn Rate Curve + +### 1. Create Variable: +- `SpawnCurve` (Curve Float) + +### 2. To create the curve asset: + +1. In Content Browser, right-click → **Miscellaneous → Curve** +2. Select "CurveFloat" +3. Name it `SpawnRateCurve` +4. Double-click to open Curve Editor + +### 3. In Curve Editor: + +- Right-click on the curve → Add Key +- Create these keyframes: + +| Time | Value | Description | +|------|-------|-------------| +| 0.0 | 0.4 | slow spawn at start | +| 0.5 | 2.0 | medium spawn halfway | +| 1.0 | 4.5 | fast spawn at end | + +- The X axis is normalized time (0-1) +- The Y axis is spawns per second + +### 4. In BP_EnemySpawner, set SpawnCurve default to this curve asset + +### Expected Result in Curve Editor: +- Curve line visible starting at (0, 0.4), rising through (0.5, 2.0), ending at (1.0, 4.5) +- Smooth interpolation between keyframes + +### Expected Result in Play mode: +- Game start: ~0.4 enemies spawn per second (slow) +- At 2.5 minutes: ~2 enemies spawn per second (medium) +- At 5 minutes: ~4.5 enemies spawn per second (intense) + +--- + +## Step 5.3: Spawning Logic + +### 1. In Event Graph, from Event Tick: + +#### a) Check if SpawningActive is true +- If false, do nothing + +#### b) Update ElapsedTime: +- Add DeltaSeconds to ElapsedTime + +#### c) Calculate normalized time: +- Divide ElapsedTime by GameDuration +- Clamp between 0 and 1 + +#### d) Get spawn rate from curve: +- Get SpawnCurve +- Call `Get Float Value` with normalized time +- This returns spawns per second + +#### e) Update SpawnTimer: +- Subtract DeltaSeconds from SpawnTimer +- If SpawnTimer <= 0: + - Reset SpawnTimer to `(1.0 / SpawnsPerSecond)` + - Call SpawnWave function + +### 2. CREATE "SpawnWave" FUNCTION: + +#### a) Inside: +- Calculate burst size based on time: + ``` + BaseCount = 1 + (NormalizedTime * 6) + BurstSize = BaseCount + Random(0, 2) + Clamp between 1 and 12 + ``` + +#### b) For Loop from 0 to BurstSize - 1: +- Call SpawnEnemy function + +### 3. CREATE "SpawnEnemy" FUNCTION: + +#### a) Inside: +- Check: Get All Actors of Class (BP_Enemy) + - Get array length + - If >= MaxSimultaneousEnemies: Return (don't spawn) + +#### b) Calculate spawn position: +- X = Random Float in Range (-SpawnAreaHalfWidth, SpawnAreaHalfWidth) +- Y = Get this actor's Y position (top of screen) +- Z = 0 + +#### c) Spawn Actor from Class: +- Class: EnemyClass +- Location: calculated position +- Rotation: (0, 0, 0) + +### 4. CREATE "StopSpawning" FUNCTION: +- Set SpawningActive = false + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "SpawnWave", "SpawnEnemy", "StopSpawning" functions appear under Functions + +### Expected Result in Play mode: +- Enemies spawn at top of screen (Y=500) at random X positions +- Spawn rate increases over time following the curve +- Maximum 120 enemies on screen at once +- When StopSpawning called: No new enemies appear + +--- + +[← Previous: Part 4 - Create the Enemy](part-4-create-enemy.md) | [Back to Index](README.md) | [Next: Part 6 - Game Director →](part-6-game-director.md) diff --git a/games/unreal/tutorial/part-6-game-director.md b/games/unreal/tutorial/part-6-game-director.md new file mode 100644 index 0000000..616cdcc --- /dev/null +++ b/games/unreal/tutorial/part-6-game-director.md @@ -0,0 +1,102 @@ +# Part 6: Create Game Director + +[← Previous: Part 5 - Create Enemy Spawner](part-5-create-spawner.md) | [Back to Index](README.md) | [Next: Part 7 - Score Manager / UI →](part-7-score-manager-ui.md) + +--- + +## Step 6.1: Create Game Director Blueprint + +1. Content Browser → Blueprints +2. Right-click → **Blueprint Class** → **Actor** +3. Name: `BP_GameDirector` +4. Double-click to open + +5. **Create Variables:** + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `PlayerReference` | Object Reference to BP_Player | - | +| `SpawnerReference` | Object Reference to BP_EnemySpawner | - | +| `GameDuration` | Float | 300.0 | +| `ElapsedTime` | Float | 0.0 | +| `GameActive` | Boolean | true | + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Variables panel shows all 5 variables with correct types +- No components needed (director is invisible logic actor) + +--- + +## Step 6.2: Game Director Initialization + +### 1. From Event BeginPlay: + +#### a) Find player in scene: +- Get All Actors of Class → BP_Player +- Get first element (index 0) +- Set PlayerReference + +#### b) Find spawner in scene: +- Get All Actors of Class → BP_EnemySpawner +- Get first element +- Set SpawnerReference + +#### c) Initialize ScoreManager: +- Get ScoreManager reference +- Call RegisterGameStart with initial lives and duration + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- BeginPlay event connected to "Get All Actors of Class" nodes + +### Expected Result in Play mode: +- Game Director automatically finds Player, Spawner, and ScoreManager +- UI initializes with correct starting values (Lives: 3, Time: 05:00) + +--- + +## Step 6.3: Game Director Update Logic + +### 1. From Event Tick: + +#### a) Check if GameActive +- If false, skip everything + +#### b) Update elapsed time: +- Add DeltaSeconds to ElapsedTime + +#### c) Calculate remaining time: +- Subtract ElapsedTime from GameDuration +- Max with 0 (don't go negative) + +#### d) Update UI timer: +- Get ScoreManager +- Call UpdateTimer with remaining time + +#### e) Check for victory: +- If ElapsedTime >= GameDuration: + - Set GameActive = false + - Get SpawnerReference → Call StopSpawning + - Get ScoreManager → Call HandleGameClear + +### 2. CREATE "HandlePlayerDeath" FUNCTION: + +#### a) Inside: +- Set GameActive = false +- Get SpawnerReference → Call StopSpawning +- Get ScoreManager → Call HandleGameOver + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- "HandlePlayerDeath" function appears under Functions +- Event Tick connected to timer update and victory check + +### Expected Result in Play mode: +- Timer counts down from 05:00 to 00:00 +- At 00:00: "Mission Complete" appears, enemies stop spawning +- When player dies: "Game Over" appears, enemies stop spawning + +--- + +[← Previous: Part 5 - Create Enemy Spawner](part-5-create-spawner.md) | [Back to Index](README.md) | [Next: Part 7 - Score Manager / UI →](part-7-score-manager-ui.md) diff --git a/games/unreal/tutorial/part-7-score-manager-ui.md b/games/unreal/tutorial/part-7-score-manager-ui.md new file mode 100644 index 0000000..9caeec6 --- /dev/null +++ b/games/unreal/tutorial/part-7-score-manager-ui.md @@ -0,0 +1,194 @@ +# Part 7: Create Score Manager / UI + +[← Previous: Part 6 - Game Director](part-6-game-director.md) | [Back to Index](README.md) | [Next: Part 8 - Game Mode and Level →](part-8-game-mode-level.md) + +--- + +## Step 7.1: Create UI Widget Blueprint + +1. Content Browser → **UI** folder +2. Right-click → **User Interface → Widget Blueprint** +3. Name: `WBP_HUD` +4. Double-click to open Widget Designer + +### Expected Result: +Widget Designer opens with: +- Hierarchy panel on left +- Canvas preview in center +- Details panel on right + +--- + +## Step 7.2: Design HUD Layout + +### 1. In the Palette panel (left side), search for "Canvas Panel" +- Drag Canvas Panel to the Hierarchy (if not already there) + +### 2. Add Score Text: + +1. In Palette, search for "Text" +2. Drag "Text" widget onto Canvas Panel in hierarchy +3. Rename it `ScoreText` (right-click → Rename) +4. In Details panel: + - Under "Slot (Canvas Panel Slot)": + - Anchors: Top-Left (click dropdown, select corner) + - Position X: `20` + - Position Y: `20` + - Size X: `300` + - Size Y: `40` + - Under "Appearance": + - Text: `Score: 0` + - Font Size: `24` + - Color: White + +### 3. Add Lives Text: + +1. Drag another "Text" widget +2. Rename to `LivesText` +3. Position X: `20`, Y: `60` +4. Text: `Lives: 3` +5. Same font settings as Score + +### 4. Add Timer Text: + +1. Drag another "Text" widget +2. Rename to `TimerText` +3. Position X: `20`, Y: `100` +4. Text: `Time: 05:00` +5. Same font settings + +### 5. Click "Compile" and "Save" (top buttons) + +### Expected Result: + +Preview shows: +``` +┌────────────────────────────────────┐ +│ Score: 0 │ +│ Lives: 3 │ +│ Time: 05:00 │ +│ │ +│ │ +└────────────────────────────────────┘ +``` + +--- + +## Step 7.3: Create Score Manager Blueprint + +1. Content Browser → Blueprints +2. Right-click → **Blueprint Class** → **Actor** +3. Name: `BP_ScoreManager` +4. Double-click to open + +5. **Create Variables:** + +| Variable Name | Type | Default Value | +|---------------|------|---------------| +| `Score` | Integer | 0 | +| `CurrentLives` | Integer | 3 | +| `HUDWidget` | Object Reference to WBP_HUD | - | +| `HUDWidgetClass` | Class Reference | (set to WBP_HUD) | + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Variables panel shows all 4 variables with correct types + +--- + +## Step 7.4: Score Manager Initialization + +### 1. From Event BeginPlay: + +#### a) Create HUD Widget: +- Right-click → `Create Widget` +- Class: Select WBP_HUD (or use HUDWidgetClass variable) +- Owning Player: Get Player Controller (index 0) + +#### b) Store widget reference: +- Set HUDWidget to the created widget + +#### c) Add to viewport: +- Right-click → `Add to Viewport` +- Connect widget reference as target + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- BeginPlay event connected to Create Widget → Add to Viewport + +### Expected Result in Play mode: +- HUD appears immediately when game starts +- HUD displays in top-left corner of screen +- Text is visible and readable (white on game background) + +--- + +## Step 7.5: Score Manager Functions + +### 1. CREATE "RegisterGameStart" FUNCTION: + +**Inputs:** +- `InitialLives` (Integer) +- `Duration` (Float) + +**Inside:** +- Set Score = 0 +- Set CurrentLives = InitialLives +- Update all UI labels + +### 2. CREATE "AddScore" FUNCTION: + +**Input:** `Amount` (Integer) + +**Inside:** +- Add Amount to Score +- Update Score label in HUD: + - Get HUDWidget + - Cast to WBP_HUD + - Get "ScoreText" widget + - Set Text to "Score: " + Score + +### 3. CREATE "SetLives" FUNCTION: + +**Input:** `Lives` (Integer) + +**Inside:** +- Set CurrentLives = Lives +- Update Lives label in HUD + +### 4. CREATE "UpdateTimer" FUNCTION: + +**Input:** `TimeRemaining` (Float) + +**Inside:** +- Convert to minutes:seconds format: + ``` + Minutes = Floor(TimeRemaining / 60) + Seconds = Floor(TimeRemaining mod 60) + ``` +- Format string: "Time: MM:SS" +- Update Timer label + +### 5. CREATE "HandleGameOver" FUNCTION: + +**Inside:** +- Set Timer text to "Game Over" + +### 6. CREATE "HandleGameClear" FUNCTION: + +**Inside:** +- Set Timer text to "Mission Complete" + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- All 6 functions appear under Functions panel + +### Expected Result in Play mode: +- Score updates instantly when enemies are killed (+50 each) +- Lives display updates when player is hit +- Timer counts down in MM:SS format +- "Game Over" or "Mission Complete" replaces timer at game end + +--- + +[← Previous: Part 6 - Game Director](part-6-game-director.md) | [Back to Index](README.md) | [Next: Part 8 - Game Mode and Level →](part-8-game-mode-level.md) diff --git a/games/unreal/tutorial/part-8-game-mode-level.md b/games/unreal/tutorial/part-8-game-mode-level.md new file mode 100644 index 0000000..1826135 --- /dev/null +++ b/games/unreal/tutorial/part-8-game-mode-level.md @@ -0,0 +1,134 @@ +# Part 8: Create Game Mode and Level + +[← Previous: Part 7 - Score Manager / UI](part-7-score-manager-ui.md) | [Back to Index](README.md) | [Next: Part 9 - Final Setup and Testing →](part-9-final-setup.md) + +--- + +## Step 8.1: Create Custom Game Mode + +1. Content Browser → Blueprints +2. Right-click → **Blueprint Class** +3. In popup, expand "All Classes" +4. Search for "Game Mode Base" +5. Select "Game Mode Base" and click Select +6. Name: `BP_BulletHellGameMode` +7. Double-click to open + +8. In the Details panel (with BP_BulletHellGameMode open): + - Find "Classes" section + - Default Pawn Class: Select `BP_Player` + - Player Controller Class: Keep default + +### Expected Result after Compile: +- Compile button shows GREEN checkmark +- Details panel shows "Default Pawn Class" set to BP_Player + +--- + +## Step 8.2: Configure Project to Use Game Mode + +1. Go to **Edit → Project Settings** +2. In left sidebar, click "Maps & Modes" +3. Under "Default Modes": + - Default GameMode: Select `BP_BulletHellGameMode` +4. Close Project Settings + +### Expected Result: +- Project Settings shows BP_BulletHellGameMode as Default GameMode +- This means the game will automatically spawn BP_Player when Play is pressed + +--- + +## Step 8.3: Create Game Level + +1. **File → New Level** +2. Select "Empty Level" +3. **File → Save Current Level As** +4. Navigate to Content folder +5. Name: `BulletHellLevel` +6. Click Save + +### Expected Result: +- New level file "BulletHellLevel" appears in Content folder +- Level is completely empty (black viewport) +- Outliner shows only default actors (if any) + +--- + +## Step 8.4: Set Up Level Components + +In the level (main viewport), we need to add game actors: + +### 1. ADD CAMERA: + +1. In Place Actors panel (left side, or Window → Place Actors) +2. Search for "Camera Actor" +3. Drag into viewport +4. Position camera: + - In Details panel, under Transform: + - Location: `X=0, Y=0, Z=1000` + - Rotation: `X=-90, Y=0, Z=0` (looking down) +5. Set as default camera: + - In Details panel, find "Auto Activate for Player" + - Check the box, set Player Index to `0` + +**For Orthographic View:** +- Click on Camera component +- In Details, set "Projection Mode" to `Orthographic` +- Set "Ortho Width" to `1920` (or your screen width) + +### 2. ADD PLAYER: + +1. From Content Browser, drag `BP_Player` into level +2. Position: `X=0, Y=-300, Z=0` (bottom center) + +### 3. ADD ENEMY SPAWNER: + +1. Drag `BP_EnemySpawner` into level +2. Position: `X=0, Y=500, Z=0` (top of screen) +3. In Details panel, set EnemyClass to `BP_Enemy` + +### 4. ADD GAME DIRECTOR: + +1. Drag `BP_GameDirector` into level +2. Position doesn't matter (it's invisible) + +### 5. ADD SCORE MANAGER: + +1. Drag `BP_ScoreManager` into level +2. Position doesn't matter + +### 6. Save the level (`Ctrl+S`) + +### Expected Result in Level Viewport: +- Camera actor visible at top of scene (Z=1000) +- BP_Player visible at bottom center (Y=-300) +- BP_EnemySpawner visible at top (Y=500) +- BP_GameDirector and BP_ScoreManager in Outliner (invisible actors) + +### Expected Result in Outliner: +``` +- CameraActor +- BP_Player +- BP_EnemySpawner +- BP_GameDirector +- BP_ScoreManager +``` + +--- + +## Step 8.5: Set Default Level + +1. **Edit → Project Settings** +2. Maps & Modes +3. Under "Default Maps": + - Editor Startup Map: Select `BulletHellLevel` + - Game Default Map: Select `BulletHellLevel` + +### Expected Result: +- Project Settings shows BulletHellLevel as both startup and default map +- Launching the game (standalone or in editor) loads this level automatically + +--- + +[← Previous: Part 7 - Score Manager / UI](part-7-score-manager-ui.md) | [Back to Index](README.md) | [Next: Part 9 - Final Setup and Testing →](part-9-final-setup.md) diff --git a/games/unreal/tutorial/part-9-final-setup.md b/games/unreal/tutorial/part-9-final-setup.md new file mode 100644 index 0000000..c6de3f0 --- /dev/null +++ b/games/unreal/tutorial/part-9-final-setup.md @@ -0,0 +1,174 @@ +# Part 9: Final Setup and Testing + +[← Previous: Part 8 - Game Mode and Level](part-8-game-mode-level.md) | [Back to Index](README.md) | [Appendix A: Variables →](appendix-a-variables.md) + +--- + +## Step 9.1: Assign Blueprint References + +### 1. Open BP_Player: +- In Details panel (with blueprint open) +- Set BulletClass: `BP_Bullet` + +### 2. Open BP_EnemySpawner: +- Set EnemyClass: `BP_Enemy` + +### 3. Open BP_Enemy: +- Set BulletClass: `BP_Bullet` + +### 4. Compile and Save all blueprints + +### Expected Result after Compile (all blueprints): +- All blueprints compile with GREEN checkmark +- No "None" or missing references in variable defaults +- BP_Player.BulletClass → BP_Bullet +- BP_Enemy.BulletClass → BP_Bullet +- BP_EnemySpawner.EnemyClass → BP_Enemy + +### Expected Result in Play mode: +- Player can shoot bullets (BP_Bullet spawns) +- Enemies spawn and shoot bullets +- All collision/damage systems functional + +--- + +## Step 9.2: Create Final Visuals (Replace Placeholder) + +Now replace the temporary cube visuals with proper colored materials: + +### 1. REMOVE TEMPORARY COMPONENTS: + +1. Open BP_Player blueprint +2. In Components panel, select "TempVisual" (the cube added in [Step 2.2](part-2-create-player.md#step-22-add-player-visual-components)) +3. Press Delete to remove it +4. Select "PlayerCamera" and delete it (the level camera from [Step 8.4](part-8-game-mode-level.md#step-84-set-up-level-components) will be used) +5. Repeat for BP_Bullet and BP_Enemy (delete their TempVisual cubes) + +### 2. Content Browser → Materials folder + +### 3. PLAYER MATERIAL: + +1. Right-click → Material +2. Name: `M_Player` +3. Double-click to open Material Editor +4. Create Vector3 node (hold 3, click) +5. Set to blue color `(0, 0.5, 1)` +6. Connect to Base Color +7. Save and Close + +### 4. BULLET MATERIALS: + +- Create `M_PlayerBullet` - Yellow `(1, 1, 0)` +- Create `M_EnemyBullet` - Red `(1, 0, 0)` + +### 5. ENEMY MATERIAL: + +- Create `M_Enemy` - Magenta `(1, 0, 1)` + +### 6. Apply materials to sprite components in each Blueprint +(Or use Sprite assets if you have 2D images) + +### Expected Result in Play mode: +- Player visible as blue shape +- Player bullets visible as yellow shapes +- Enemy bullets visible as red shapes +- Enemies visible as magenta shapes +- All game elements distinguishable by color + +--- + +## Step 9.3: Add Background (Optional) + +### 1. In level, add a Plane mesh: +- Place Actors → Basic → Plane +- Scale: `X=20, Y=30, Z=1` +- Position: `X=0, Y=0, Z=-100` (behind everything) + +### 2. Create dark space material: +- `M_Background` - Dark blue/black + +### Expected Result in Play mode: +- Dark background visible behind all game elements +- Game elements (player, enemies, bullets) clearly visible against background +- Background doesn't interfere with gameplay (Z=-100, behind everything) + +--- + +## Step 9.4: Test the Game + +### 1. Click "Play" button (green arrow in main toolbar) +OR press `Alt+P` + +### 2. TEST CHECKLIST: + +| # | Test | Pass? | +|---|------|-------| +| 1 | Player moves with WASD or Arrow keys | ☐ | +| 2 | Player stays within screen bounds | ☐ | +| 3 | Player shoots with Z key or Left Mouse | ☐ | +| 4 | Bullets travel upward | ☐ | +| 5 | Enemies spawn at top of screen | ☐ | +| 6 | Enemies move down with wavy motion | ☐ | +| 7 | Enemies shoot radial bullet patterns | ☐ | +| 8 | Player bullets damage enemies | ☐ | +| 9 | Enemy bullets damage player | ☐ | +| 10 | Score increases when enemies die | ☐ | +| 11 | Lives decrease when player is hit | ☐ | +| 12 | Timer counts down from 5:00 | ☐ | +| 13 | Game shows "Game Over" when lives = 0 | ☐ | +| 14 | Game shows "Mission Complete" after 5 minutes | ☐ | +| 15 | Special ability (X key) clears screen | ☐ | + +### 3. To stop playing: Press `ESC` or click "Stop" button + +### Expected Result - Complete Game Test: +- All checklist items above should pass +- Frame rate stable (60+ FPS recommended) +- No crashes or Blueprint errors in Output Log +- Game is playable from start to victory/defeat + +--- + +## Step 9.5: Build Standalone Game + +1. **File → Package Project → Windows** (or your platform) +2. Select output folder +3. Wait for build to complete +4. Navigate to output folder → WindowsNoEditor → [ProjectName].exe +5. Run the executable to play standalone + +### Expected Result: +- Build completes without errors (check Output Log) +- Executable file created in output folder +- Running .exe launches the game in fullscreen +- Game plays identically to editor Play mode +- Can close with `Alt+F4` or in-game quit (if implemented) + +--- + +## 🎉 Congratulations! + +You have completed the Unreal Engine Bullet Hell tutorial! + +Your game includes: +- ✅ Player with 3 lives, WASD movement, Z/mouse shooting +- ✅ Volley shooting (3 bullets in spread pattern) +- ✅ Screen-clear special ability (X key, one use) +- ✅ Enemies with sine-wave movement +- ✅ Radial bullet patterns (20 bullets per burst) +- ✅ Progressive difficulty (spawn rate increases over 5 minutes) +- ✅ Score, lives, and timer UI +- ✅ Victory after 5 minutes survival +- ✅ Game over when lives reach 0 + +--- + +### Additional Resources: + +- [Appendix A: Complete Variable Reference](appendix-a-variables.md) +- [Appendix B: Troubleshooting](appendix-b-troubleshooting.md) +- [Appendix C: Unity to Unreal Conversion Notes](appendix-c-unity-conversion.md) + +--- + +[← Previous: Part 8 - Game Mode and Level](part-8-game-mode-level.md) | [Back to Index](README.md) | [Appendix A: Variables →](appendix-a-variables.md)