Shooter Game

Choose your OS:

ShooterGame.png

The First Person Shooter sample is an example of a PC multiplayer first-person shooter. It includes basic implementations of weapons and gametypes along with a simple front end menu system.

A complete list of the featured concepts:

  • Instant hit weapon (ShooterWeapon_Instant)

  • Projectile based weapon (ShooterWeapon_Projectile + ShooterProjectile)

  • Non-Team game mode (ShooterGame_FreeForAll)

  • Team-based game mode (ShooterGame_TeamDeathMatch)

  • Pickups (ShooterPickup)

  • Main menu (ShooterHUD_Menu)

Weapon Fire System

The base firing functionality for the weapons - such as ammo management, reloading, and replication - is implemented in the AShooterWeapon class.

The weapon is switched to its firing state on the local client and server (via RPC calls). DetermineWeaponState() is called in StartFire()/StopFire() which performs some logic to decide which state the weapon should be in and then calls SetWeaponState() to place the weapon into the appropriate state. Once in firing state, the local client will repeatedly call HandleFiring() which, in turn, calls FireWeapon(). Then it updates ammo and calls ServerHandleFiring() to do the same on the server. The server version is also responsible for notifying remote clients about each fired round via the BurstCounter variable.

Actions performed on the remote clients are purely cosmetic. Weapon fire is replicated using the BurstCounter property so that the remote clients can play animations and spawn effects - perform all of the visual aspects of the weapon firing.

Instant-Hit Weapon Fire

Instant-hit detection is used for fast firing weapons, such as rifles or laser guns. The basic concept is that when the player fires the weapon, a line check is performed in the direction the weapon is aimed at that instant to see if anything would be hit.

This method allows high precision and works with Actors that do not exist on server side (e.g., cosmetic or torn off). The local client performs the calculations and informs the server of what was hit. Then, the server verifies the hit and replicates it if necessary.

In FireWeapon(), the local client does a trace from the camera location to find the first blocking hit under the crosshair and passes it to ProcessInstantHit(). From there, one of three things happens:

  • The hit is sent to the server for verification (ServerNotifyHit() --> ProcessInstantHit_Confirmed()).

  • If the hit Actor does not exist on server, the hit is processed locally (ProcessInstantHit_Confirmed()).

  • If nothing was hit, the server is notified (ServerNotifyMiss()).

Confirmed hits apply damage to the hit Actors, spawn trail and impact effects, and notify remote clients by setting data about the hit in the HitNotify variable. Misses just spawn trails and set HitNotify for remote clients, which look for HitNotify changes and perform the same trace as the local client, spawning trails and impacts as needed.

The instant-hit implementation also features weapon spread. For trace/verification consistency, local client picks a random seed each time FireWeapon() is executed and passes it in every RPC and HitNotify pack.

Projectile Weapon Fire

Projectile fire is used to simulate weapons that fire rounds which are slower moving, explode on impact, affected by gravity, etc. These are cases where the outcome of the weapon fire cannot be determined at the exact instant the weapon is fired, such as launching a grenade. For this type of weapon, an actual physical object, or projectile, is spawned and sent moving in the direction the weapon is aimed. A hit is determined by the projectile colliding with another object in the world.

For projectile fire, the local client does a trace from camera to check what Actor is under the crosshair in FireWeapon(), similar to the instant-hit implementation. If the player is aiming at something, it adjusts the fire direction to hit that spot and calls ServerFireProjectile() on the server to spawn a projectile Actor in the direction the weapon was aimed.

When the movement component of the projectile detects a hit on the server, it explodes dealing damage, spawning effects, and tears off from replication to notify the client about that event. Then, the projectile turns off collision, movement, and visibility and destroys itself after one second to give client time for replication update.

On clients, explosion effects are replicated via OnRep_Exploded().

Player Inventory

The player's inventory is an array of AShooterWeapon references stored in the Inventory property of the player's Pawn (AShooterCharacter). The currently equipped weapon is replicated from the server, and additionally, AShooterCharacter stores its current weapon locally in CurrentWeapon property, which allows the previous weapon to be un-equipped when a new weapon is equipped.

When the player equips a weapon, the appropriate weapon mesh - first-person for local, third-person for others - is attached to the Pawn and an animation is played on the weapon. The weapon is switched to the equipping state for the duration of the animation.

Player Camera

In first-person mode, the Pawn's mesh is hard-attached to the camera so that the arms always appear relative to the player's view. The downside of this approach is that it means the legs are not visible in the player's view, since the entire mesh rotates to match the camera yaw and pitch.

The basic flow of the camera update is:

  • AShooterCamera::UpdateCamera() is executed each tick.

  • APlayerCamera::UpdateCamera() is called to update the camera rotation based on the player's input.

  • AShooterCharacter::OnCameraUpdate() is called to perform the calculations necessary to rotate the first person mesh to match the camera.

When the player dies, it switches to a death camera that has a fixed location and rotation set in the AShooterPlayerController::PawnDied() handler. This function calls AShooterPlayerController::FindDeathCameraSpot(), which cycles through several different locations and uses the first one not obstructed by the level's geometry.

Online Multiplayer

Online multiplayer matches are divided into 3 stages:

  • Warm up

  • Match play

  • Game over

When the first player joins the game, the warm up stage begins. This is a short period, marked by a countdown timer, that gives other players a chance to join. During this period, the players are in spectator mode allowing them to fly around the map. When the countdown timer expires, StartMatch() is called to restart all of the players and spawn their Pawns.

Matches are timed, with the game time being calculated server side in the AShooterGameMode::DefaultTimer() function, which is executed using a looping timer with a rate equal to the current time dilation that equates to once every game second. This is stored in the RemainingTime property of the game replication info class (AShooterGRI), which is then replicated to clients. When the time remaining reaches zero, FinishMatch() is called to end the game session. This notifies all players the match has ended and disables movement and health.

The menu system is created using the Slate UI framework . It consists of menus, menu widgets, and menu items. Each menu has a single menu widget (SSHooterMenuWidget) that is responsible for layout, internal event handling, and animations for all of the menu items. Menu items (SSHooterMenuItem) are compound objects that can perform actions and contain any number of other menu items. These can be as simple as a label or button or "tabs" that contain complete submenus made up of other menu items. This menu can be operated using a keyboard or controller, but there is only limited mouse support at this time.

Each menu is constructed via the Construct() function, which adds all of the necessary menu items, including sub-items, and attaches delegates to them where necessary. This is done using the helper methods - AddMenuItem(), AddMenuItemSP(), etc. - defined in the MenuHelper namespace in the SShooterMenuWidget.h file.

Navigation to previous menus is done using an array of shared pointers to menus and is stored in the MenuHistory variable of the menu widget. MenuHistory acts like stack to hold previously entered menus and makes it easy to go back. By using this method, no direct relationship is created between menus and the same menu can be reused in different places if necessary.

Animations are performed using interpolation curves defined in SShooterMenuWidget::SetupAnimations(). Each curve has start time, duration, and interpolation method. Animations can be played forward and in reverse and their attributes can be animated at a specific time using GetLerp(), which returns a value from 0.0f to 1.0f. There are several different interpolation methods available, defined in ECurveEaseFunction::Type in SlateAnimation.h.

menu.png

The main menu is opened automatically when the game starts by specifying the ShooterEntry map as the default. It loads a special GameMode, AShooterGameMode, that uses the AShooterPlayerController_Menu class which opens the main menu by creating a new instance of the FShooterMainMenu class in its PostInitializeComponents() function.

In-game Menu

ingame_menu.png

The in-game menu is created in the PostInitializeComponents() function of the AShooterPlayerController class, and opened or closed via the OnToggleInGameMenu() function.

Options Menu

The options menu is available as a submenu of both the main menu and in-game menu. The only difference is how changes are applied:

  • When accessed from the main menu, changes are applied when the player starts the game.

  • When accessed from the in-game menu, changes are applied immediately when the menu is closed.

The settings in the options menu are saved to GameUserSettings.ini, and loaded automatically at startup.