Introduction
One of the things I've been trying to figure out how to do lately is to make my own game. Simple to say, hard to execute. I've spent the last 18 months or so slowly learning Unreal Engine in my off time and I think I've hit the point where I'm ready to start actually making something - if only to get myself out of tutorial hell.
As a solo dev, I need to figure out a game that I could reasonably make myself. Everyone loves the idea of making a open world action survival MMORPG however that takes teams of people years develop and most of them turn out to be absolute dumpster fires. The best way to approach this is to keep the scope small enough to accomplish in a reasonable time frame, while keeping the genre to something I enjoy.
Some of my favorite games to sink time into are rogue-lites and I loved the Star Wars: Starfighter and Ace Combat series of games as a child. I feel like that is a niche which is not explored as often as I would like and so I might as well try and make one of my own! Hench the inaugural devlog of FlightShooterGame (name to be replaced when I come up with something that I like).
Devlog
Overview
The goal FlightShooterGame is to be a rogue-lite spaceship game that emulates the combat styles of Star Wars: Starfighterand Ace Combat games. The player's goal should be to fight through a series of challenges escalating in difficulty with the ability to upgrade their ship and abilities between challenges. There should be multiple types of ships that the player can choose at the beginning of a run and drastically affect the play-styles involved. The player should also get resources that last between runs that they can use to upgrade their ships and abilities in different ways.
Movement
The first part of any game is, of course, basic movement and actions! And, of course, this turned out to be way more complicated then I initially envisioned.
If this where a game where the player would b e controlling a humanoid-like being, something that runs, jumps, and guns that most games emulate, this wouldn't be an issue. The CharacterMovementComponent can handle that quite easily with minimal effort. However I require the player to play as a spaceship with 6-degrees of movement freedom.
The way I initially tried implementing this system was to have the Player Controller handle add Yaw/Roll/Pitch inputs while the Ship Blueprint would be set to use the controller's yaw/roll/pitch. Simple enough I thought. I put together something similar to this:
And you would expect this to work. And it does! The ship yaws, rolls, and pitches up and down. The issues occur however, when you attempt to do operate in more than one axis at a time. Yaw always worked. Roll always worked. Pitch... only worked as expected if you didn't roll the ship. You see, the pitch only pitched the ship on the Y-axis at all times. When no roll is applied to the ship, it pitched up and down correctly. When you roll the ship 90 degrees, it still pitched on the Y axis of the world as that is how unreal handles its gimbals.Additionally, I could not truly move in all 6 degrees. When I would contort the ship to extreme angles, the rotation would stutter or prevent me from achieving certain results. Turns out, this was a case of gimbal-lock. Also it turns out, it's really hard to use your google-fu skills when you can't even identify what the actual issue.
After some hours of googling and asking around for what I was trying to do, I stumbled upon my latest headache - quaternions. I won't lie - the math makes my head hurt. It's going to be well worth it for me to study up on the concept as I progress because it's good knowledge to have. But the fact is, Unreal does not implement or expose quaternions to blueprints at all. Luckily, I managed to stumble across this video which gave me enough to get going with a blueprint function library.
The code of which, I will also post (mainly for my reference later):
Header
//Header File
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Flight_Quaternions.generated.h"
UCLASS()
class FLIGHTSHOOTERGAME_API UFlight_Quaternions : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// Convert Euler Rotations To Quaternions
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Euler To Quaternion", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static FQuat Euler_To_Quaternion(FRotator Current_Rotation);
// Function to set world rotation of scene component to input quaternion rotation
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set World Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void SetWorldRotationQuat(USceneComponent* SceneComponent, const FQuat& Desired_Rotation);
// Function to set relative rotation of scene component to input quaternion rotation
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set Relative Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void SetRelativeRotationQuat(USceneComponent* SceneComponent, const FQuat& Desired_Rotation);
// Function to add delta rotation to current local rotation of scene component
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Add Local Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void AddLocalRotationQuat(USceneComponent* SceneComponent, const FQuat& Delta_Rotation);
// Function to set world rotation of Actor to input quaternion rotation
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set Actor World Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void SetActorWorldRotationQuat(AActor* Actor, const FQuat& Desired_Rotation);
// Function to set relative rotation of Actor to input quaternion rotation
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set Actor Relative Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void SetActorRelativeRotationQuat(AActor* Actor, const FQuat& Desired_Rotation);
// Function to add delta rotation to current local rotation of Actor
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Add Actor Local Rotation (Quaternion)", Keywords = "rotation, quaternion"), Category = "Quaternion Rotation")
static void AddActorLocalRotationQuat(AActor* Actor, const FQuat& Delta_Rotation);
};
Implementation
// Implementation file
#include "Flight_Quaternions.h"
FQuat UFlight_Quaternions::Euler_To_Quaternion(FRotator Current_Rotation)
{
FQuat q; // Declare output quaternion
float yaw = Current_Rotation.Yaw * PI / 180; // Convert degrees to radians
float roll = Current_Rotation.Roll * PI / 180;
float pitch = Current_Rotation.Pitch * PI / 180;
double cy = cos(yaw * 0.5);
double sy = sin(yaw * 0.5);
double cr = cos(roll * 0.5);
double sr = sin(roll * 0.5);
double cp = cos(pitch * 0.5);
double sp = sin(pitch * 0.5);
q.W = cy * cr * cp + sy * sr * sp;
q.X = cy * sr * cp - sy * cr * sp;
q.Y = cy * cr * sp + sy * sr * cp;
q.Z = sy * cr * cp - cy * sr * sp;
return q; // Return the quaternion of the input Euler rotation
}
// Set the scene component's world rotation to the input quaternion
void UFlight_Quaternions::SetWorldRotationQuat(USceneComponent* SceneComponent, const FQuat& Desired_Rotation)
{
if (SceneComponent)
{
SceneComponent->SetWorldRotation(Desired_Rotation);
}
}
// Set the scene component's relative rotation to the input quaternion
void UFlight_Quaternions::SetRelativeRotationQuat(USceneComponent* SceneComponent, const FQuat& Desired_Rotation)
{
if (SceneComponent)
{
SceneComponent->SetRelativeRotation(Desired_Rotation);
}
}
// Add the input delta rotation to the scene component's current local rotation
void UFlight_Quaternions::AddLocalRotationQuat(USceneComponent* SceneComponent, const FQuat& Delta_Rotation)
{
if (SceneComponent)
{
SceneComponent->AddLocalRotation(Delta_Rotation);
}
}
// Set the Actor's world rotation to the input quaternion
void UFlight_Quaternions::SetActorWorldRotationQuat(AActor* Actor, const FQuat& Desired_Rotation)
{
if (Actor)
{
Actor->SetActorRotation(Desired_Rotation);
}
}
// Set the Actor's relative rotation to the input quaternion
void UFlight_Quaternions::SetActorRelativeRotationQuat(AActor* Actor, const FQuat& Desired_Rotation)
{
if (Actor)
{
Actor->SetActorRelativeRotation(Desired_Rotation);
}
}
// Add the input delta rotation to the Actor's current local rotation
void UFlight_Quaternions::AddActorLocalRotationQuat(AActor* Actor, const FQuat& Delta_Rotation)
{
if (Actor)
{
Actor->AddActorLocalRotation (Delta_Rotation);
}
}
Completion
Finally, with our function library compiled, implementation was as easy as implementing the Euler to Quaternion function as implemented in c++. We take the player's current Euler rotation pitch/roll/yaw as a float and this function will give us the quaternion of it. We then take that Quaternion and add it to their local rotation. The AddActorLocalRotationQuat functions are literally just the normal AddActorLocalRotation that takes a Quat instead of a Euler rotation.
Thrust was the last component to have situated and it currently the system that will need the most tweaking in the future. However, in order to have a MVP, I simply have the ship accelerate to its max speed every tick. When the player presses the +/- thrust buttons the ship's FloatingPawnMovement maximum speed will be set to either a higher or lower number depending on the button and amount of button pressed and the ship will accelerate or break at the speeds defined in the FloatingPawnMovement component. It's not perfect, and it's certainly not what I want, but it'll be good enough for testing and prototyping until I come back to it.
Cameras
One of the nice features of Ace Combat and Star Wars: Starfighter is that you can switch your view at any time from first or third person with the former having a third view where it is first person with a rendered cockpit. I do not have a cockpit modeled but I do like being able to switch between first and third person views for a variety of reasons and this is easy to implement.
When pressed on the controller, the controller will change the current view mode to the next on the list and set it as the variable. It will then kick off a SwitchView event on the player's ship blueprint. This event will handle hiding the ship/gun models, adjust the camera's SpringArm length, and set its offset. It will do more in the future, probably handling UI elements, but for now, this is perfect.
Guns guns guns
And now the fun bit, the pew pews!
Using a simple laser beam Niagara effect, I made a actor blueprint with a simple box collision, Niagara system, Radial Force component, and a Projectile Movement Component. After setting the Radial Force and Projectile Movement component's defaults to something that works good enough for testing, the blueprint looks like a pretty bog-standard projectile. When the box collision hits something it'll check to see if the object is it's instigator, if it isn't it will do a small impulse on the object (for physics later), spawn a emitting that shoots off some sparks, then checks to see if it implements a blueprint interface that will handle damage logic and send some damage information to it. Regardless of if it implements a damage component, once it hits something that blocks it's collision channel, we want to destroy the projectile.
Because the ship that I am using (a free spaceship I downloaded from Itch.io for prototyping) has two guns, I modified the mesh of the guns to add sockets at the ends of both barrels as the locations for the projectiles to spawn. Then when the player presses the primary fire button, we check if the guns are in cooldown then spawn projectile actors at each socket location. Once the projectiles have spawned, we then turn the cooldown on and wait 0.2 seconds until we turn the cooldown off and another set of projectiles can spawn.
And to test this out I made a new blueprint for what will eventually be a generic Enemy ship, implemented the damage interface, and had it explode once it reached 0 health.
And there we have it - one basic prototype good enough for starting a dev process with. A ship that can yaw pitch roll and accelerate/break with 6 degrees of freedom, switch view modes on demand, and fire projectiles that can hit and interact with other ships.
![[DevLog0.webm]]
Next Goals
Now that I have a good base to start working with, I do want to now work on getting some more features added at a basic level
- A simple AI for enemy ships to at least fly around in a defined area. Ideally in a somewhat random way so they are not just flying in a straight line the majority of the time. Additionally I would like for the AI to give chase to the player when they enter a defined radius around it and attack the player.
- A simple UI. I really need three things right now for my sanity - I need something to display current speed/thrust settings, a crosshair to aim with, and leaving the most complex for last, some ability to target other ships and track them while they are off-screen.
- A space skybox. It's a game where you are flying a spaceship. Actually looking like I'm in space would be a good plus.
Between these three items, that should be more than enough to keep me occupied for a while.