ASAT Code Tutorial 4: Call a SatCat REST API from Unreal Engine 5

post-thumb

Note: This is a post is part of a multi-part series.

Http and JSON: Unreal Engine 5 C++

Good morning, everyone. Evening? Whatever :).

Today, we’re going to use Unreal Engine’s HTTP and JSON modules to create an interesting little visualization.

Along the way we’re going to learn:

  • How to send HTTP requests from within Unreal Engine
  • How to parse a server’s response (JSON)
  • How to query a “Satellite Catalog” database operated by the U.S. Strategic Command
  • How to get a list of active space debris objects associated with space weapons testing (with orbital data)
  • How to compute a position from orbital data
  • Where to find test data, and how to switch over to live data from the U.S. Strategic Command servers

On no! An explosion above…

You may have heard about a recent Russian Anti-Satellite (ASAT) test that created a bevvy of space debris.

Early in the morning of November 15, Moscow time, a Russian missile blasted a Russian satellite to smithereens. The destroyed satellite, Kosmos-1408, had been in orbit for nearly four decades. With at least 1,500 trackable pieces, and countless more too small for detection, the remains of Kosmos-1408 pose a threat to other objects in orbit. The destruction itself caused astronauts and cosmonauts aboard the International Space Station to shelter in space. It also risked harm to China’s taikonauts aboard the Tiangong space station.
This week’s destroyed Russian satellite created even more dangerous space debris
BY KELSEY D. ATHERTON, Popular Science

What the eff was Russia thinking? At risk of injecting international politics into a game development blog… The test was performed during the run-up to the Russian “Special Operations” in Ukraine. The event was NOT a “test” - it was a statement. “If you interfere with our space operations while we are in Ukraine, we can in turn destroy your own satellites”. Russia made no mistake here; they knew exactly what they were doing.

A worthy goal

Visualizing Anti-Satellite Test debris in orbit around Earth

A Satellite Catalog (SatCat) contains information about objects in Earth’s orbit. We can query a SatCat for orbital debris created by the Russian Anti-Satellite “test”. While we’re at it, we’ll include debris from another destructive test, done by China in 2007. There have been other anti-satellite tests, but very little remains debris in orbit aside from these two events.

We’ll get the debris object data from the REST API of a SatCat operated by the U.S. Strategic Command: Space-Track.org

If it sounds a bit serious, it is.

Air Force Maj. Gen. David D. Thompson, U.S. Strategic Command’s director of plans and policy at Offutt Air Force Base, Nebraska, said the release of new high-quality positional information on space debris of an unknown origin will help owner-operators better protect their satellites from these objects and ultimately create less space debris.
“We run a predictive program that shows where the objects are, where they will be in the future, and the potential for these objects to run into each other,” Thompson said.
Officials Expand Space-tracking Website
U.S. Department of Defense

If you adhere to the User Agreement you can obtain credentials to query the SatCat. You’ll need a Space-Track.org username and password here shortly, fyi.

The app we build here will display a 3D map of Earth, update debris positions in real time, and let the user use the mouse to orbit and zoom the camera.


Building a 3D Map of Earth in Unreal Engine

We’ll develop two Unreal Engine “Actors”:

  • EarthActor
    • Advances the current time
    • Rotates the earth (according to the time)
    • Sets the sun’s direction (based on the time)
  • DebrisActor
    • Queries a SatCat to get a list of debris objects associated with an ASAT test via Http REST API
    • Parses the JSON-formatted list of debris
    • Obtains “Two-Line Element” (TLE) values that describe the orbit of each object
    • Updates the location of each debris object instance from TLEs
    • Contains an Instanced Static Mesh Component, which renders the debris object instances

Http

Sending a request to the server

To make use of the Unreal Engine Http module, we’ll need to edit the Build.cs file to include the module as a dependency.

public class DebrisCloud : ModuleRules
{
	public DebrisCloud(ReadOnlyTargetRules Target) : base(Target)
	{
    // ...
		PrivateDependencyModuleNames.Add("HTTP");

This exposes the Http module’s header files and libraries to our application:

#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"

To sent an http response to the SatCat REST service we will create an Http request via the Http module. We’ll need to set a few fields, and then we can ask Http to process the request & send it to the server. When our app gets a response we’ll get called back on a lambda function, then parse the response (JSON).

Example code for an Http request via c++:

void ADebrisActor::LoginWithSpaceTrack(const FString& NewObjectId, const FString& NewUser, const FString& NewPassword)
{
    FString uriBase = LIVE_URL_BASE;
    FString uriLogin = uriBase + TEXT("/ajaxauth/login");

    FString uriQuery = uriBase + TEXT("/basicspacedata/query/class/gp/OBJECT_ID/~~") + NewObjectId + TEXT("/orderby/DECAY_DATE%20desc/emptyresult/show");

    FHttpModule& httpModule = FHttpModule::Get();

    // Create an http request
    // The request will execute asynchronously, and call us back on the Lambda below
    TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest = httpModule.CreateRequest();

    // This is where we set the HTTP method (GET, POST, etc)
    // The Space-Track.org REST API exposes a "POST" method we can use to get
    // general perturbations data about objects orbiting Earth
    pRequest->SetVerb(TEXT("POST"));

    // We'll need to tell the server what type of content to expect in the POST data
    pRequest->SetHeader(TEXT("Content-Type"), TEXT("application/x-www-form-urlencoded"));

    FString RequestContent = TEXT("identity=") + NewUser + TEXT("&password=") + NewPassword + TEXT("&query=") + uriQuery;
    // Set the POST content, which contains:
    // * Identity/password credentials for authentication
    // * A REST API query
    //   This allows us to login and get the results is a single call
    //   Otherwise, we'd need to manage session cookies across multiple calls.
    pRequest->SetContentAsString(RequestContent);

    // Set the http URL
    pRequest->SetURL(uriLogin);

    // Set the callback, which will execute when the HTTP call is complete
    pRequest->OnProcessRequestComplete().BindLambda(
        // Here, we "capture" the 'this' pointer (the "&"), so our lambda can call this
        // class's methods in the callback.
        [&](
            FHttpRequestPtr pRequest,
            FHttpResponsePtr pResponse,
            bool connectedSuccessfully) mutable {

        if (connectedSuccessfully) {

            // We should have a JSON response - attempt to process it.
            ProcessSpaceTrackResponse(pResponse->GetContentAsString());
        }
        else {
            switch (pRequest->GetStatus()) {
            case EHttpRequestStatus::Failed_ConnectionError:
                UE_LOG(LogTemp, Error, TEXT("Connection failed."));
            default:
                UE_LOG(LogTemp, Error, TEXT("Request failed."));
            }
        }
    });

    // Finally, submit the request for processing
    pRequest->ProcessRequest();
}

As you can see in the above code, there are several steps to complete before we can send the Http request. If everything goes swimmingly we’ll end up in the callback with JSON data per our request. We’re using the Space-Track.org ‘gp’ REST API, which stands for “General Perturbations”. We can query this API for objects while getting the data we need to “propagate” orbits. “Propagating” an orbit means computing an object’s location at a point in time in the near future.

Anyhoo, what guarantees thread safety? The callback could be invoked from who-knows-what thread, correct?

Correct.

We can check for that:

// Validate http called us back on the Game Thread...
check(IsInGameThread());

If we add that to our callback, the debugger will break if ever we find the callback executing in another thread.

Great. It looks like we have enough to send Http requests to the server. Now, what do we do with the data in the server’s response?

If you scan through the http module’s headers, you’ll see none of it is exposed to Unreal Engine’s Blueprints. The same is true of the JSON module.


JSON

Handing the server’s response

As with the Http module, we’ll need to add the Unreal Engine JSON module to our Build.cs file.

public class DebrisCloud : ModuleRules
{
	public DebrisCloud(ReadOnlyTargetRules Target) : base(Target)
	{
    // ...
		PrivateDependencyModuleNames.AddRange(new string[] { "Json", "JsonUtilities" });

We can now include JSON headers in our c++ source files:

#include "Json.h"

Array returned by SatCat ‘gp’ query

Parsing JSON

The gp query returns an array of objects in JSON format (unless we request otherwise).

To parse the response we can create a TJsonReader instance, which can de-serialize the JSON data. We expect the JSON to look something like this:

[
    {
        "OBJECT_NAME": "SOLWIND DEB",
        "OBJECT_ID": "1979-017AN",
        ...
        "TLE_LINE1": "1 16085U 79017AN  02340.24727448 +.06502115 +27951-5 +69293-4 0  9993",
        "TLE_LINE2": "2 16085 097.8488 339.2664 0004124 321.9060 038.3522 16.50810364961385"
    },
    {
        "OBJECT_NAME": "SOLWIND DEB",
        "OBJECT_ID": "1979-017AM",
        ...
        "TLE_LINE1": "1 16084U 79017AM  00103.21332867 +.08321552 +28533-5 +44953-3 0  9999",
        "TLE_LINE2": "2 16084 097.8792 097.4511 0015229 231.8002 127.8734 16.40853641815379"
    },
    {
        "OBJECT_NAME": "SOLWIND",
        "OBJECT_ID": "1979-017A",
        ...
        "TLE_LINE1": "1 11278U 79017  A 92202.74897259  .16565086 +95000-5 +10587-3 0  9999",
        "TLE_LINE2": "2 11278 097.7889 282.4905 0015798 261.0111 099.4254 16.53446267745554"
    }
]

So, we’ll need need to call the appropriate TJsonReader::Deserialize method… There are various overloads to choose from, depending on whether you’re expecting the JSON response to be an Array, an Object, or other type. We want the overload that expects an array of JSON values:

bool Deserialize(const TSharedRef<TJsonReader<CharType>>& Reader, TArray<TSharedPtr<FJsonValue>>& OutArray)

To deserialize a JSON object, we would invoke one of the alternate overloads.
bool Deserialize(TJsonReader& Reader, TArray<TSharedPtr< FJsonValue »& OutArray)

Our method to parse the JSON file into an array of objects looks something like this:

void ADebrisActor::ProcessSpaceTrackResponse(const FString& ResponseContent)
{
    // Validate http called us back on the Game Thread...
    check(IsInGameThread());

    TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(ResponseContent);
    TArray<TSharedPtr<FJsonValue>> OutArray;

    if (FJsonSerializer::Deserialize(JsonReader, OutArray))
    {
        ProcessSpaceTrackResponse(OutArray);
    }
}

Parsing child objects within the array

We expect the array to contain JSON objects. Right? (Wake up, we’ll get to the good stuff soon!) For each FJsonValue in the array we can call the FJsonValue::AsObject method to get an FJsonObject instance, which we can use to query the object’s properties. This is the method we use to do this:

	/** Returns this value as an object, throwing an error if this is not an Json Object */
	virtual const TSharedPtr<FJsonObject>& AsObject() const;

In this case, we only want to display objects that remain in orbit. Objects in orbit have not yet “decayed”. These objects will have an explicit DECAY_DATE value of null.

[
    {
        "OBJECT_NAME": "FENGYUN 1C DEB",
        "OBJECT_ID": "1999-025AZY",
        ...
        "DECAY_DATE": null,
        ...
        "TLE_LINE1": "1 30934U 99025AZY 22135.49204091  .00001304  00000-0  16553-2 0  9996",
        "TLE_LINE2": "2 30934  98.7458 162.7224 0359497 153.6343 208.3588 13.40981068741532"
    },

In our c++ code, we can check for this via the return value of TryGetStringField like so:

        FString DECAY_DATE;

        // Only add objects that haven't decayed yet...
        if (!JsonResponseObject->TryGetStringField(TEXT("DECAY_DATE"), DECAY_DATE))
        {
            FString TLE_LINE1 = JsonResponseObject->GetStringField(TEXT("TLE_LINE1"));
            FString TLE_LINE2 = JsonResponseObject->GetStringField(TEXT("TLE_LINE2"));

If the object doesn’t have a null value for DECAY_DATE we skip it. For numeric fields we can similarly use TryGetNumberField:

        double APOAPSIS = 0.;
        JsonResponseObject->TryGetNumberField(TEXT("APOAPSIS"), APOAPSIS);

        double PERIAPSIS = 0.;
        JsonResponseObject->TryGetNumberField(TEXT("PERIAPSIS"), PERIAPSIS);

Yay! We now have an array of debris objects. We know how to determine if they’re still in orbit. And, we have the “Two Line Element” data, which we can use to compute the orbit of each object.


Visualizing the Earth and Debris Object Orbits

Building a live visualization of the debris objects orbiting Earth is straightforward using MaxQ, an integration of NASA’s SPICE library with Unreal Engine 5.

For further information about MaxQ, see the following:
MaxQ project page
MaxQ GitHub page
MaxQ documentation
Walkthrough of MaxQ development

Getting orbit data

Upon extracting the TLE data from JSON, we can use the SPICE getelm method to return a FSTwoLineElements structure:

            FString TLE_LINE1 = JsonResponseObject->GetStringField(TEXT("TLE_LINE1"));
            FString TLE_LINE2 = JsonResponseObject->GetStringField(TEXT("TLE_LINE2"));
            ES_ResultCode ResultCode;
            FString ErrorMessage;
            FSTwoLineElements Elements;
            FSEphemerisTime et;

            USpice::getelm(ResultCode, ErrorMessage, et, Elements, TLE_LINE1, TLE_LINE2);

            if (ResultCode == ES_ResultCode::Success)
            {
                DebrisElements.Add(Elements);
            }

Computing a position

The FSTwoLineElements structure is used to compute the object’s orbital “State Vector” for a given time… A “State Vector” is the object’s position and velocity, and the position tells us where to render the debris object. To compute the state vector we use evsgp4, in conjunction with ‘Geophysical’ constants:

    // Get Earth's geophysical constants.
    // This assumes the 'geophysical.ker' file was loaded via a call to USpice::furnsh
    FSTLEGeophysicalConstants GeophysicalConstants;
    USpice::getgeophs(GeophysicalConstants, TEXT("EARTH"));

    FVector scenegraphPosition = FVector(0.);

    // Compute the object's state vector from its TLEs
    FSStateVector state;
    USpice::evsgp4(ResultCode, ErrorMessage, state, et, GeophysicalConstants, Elements);

    // And, add the object's instance to our Instanced Mesh.
    FTransform t;
    t.SetLocation(scenegraphPosition);
    t.SetScale3D(FVector(ObjectScale));
    InstancedMesh->AddInstance(t, false);

What are these mysterious geophysical constants? You can get more information from the SPICE evsgp4 documentation. The value of these constants come from a “kernel” file in the project, `Constants\Spice\Kernels\geophysical.ker':

KPL/PCK

      The mkspk application needs the data in this kernel to produce
      type 10 SPK segments based upon the two-line element sets available
      from NORAD/SPACETRACK.
      ...

This file must be loaded by a call to USpice::furnsh before we call evsgp4.

Coordinate Systems

The state vector uses SPICE’s “Right-Handed” coordinate system, while Unreal Engine uses a “Left-Handed” coordinate system.

2D to Left-Handed 3D Coordinate Systems

We can convert this Right Handed position to an Unreal Engine coordinate using the USpiceTypes::Swizzle function:

    FSStateVector state;
    USpice::evsgp4(ResultCode, ErrorMessage, state, et, GeophysicalConstants, Elements);

    if (ResultCode == ES_ResultCode::Success)
    {
        // The state is in Ref=TEME, but the rest of the scene is rendered using J2000.
        // Normally, we would need to define the TEME frame in a spice kernel, and then use
        // USpice::pxform to transform the position from TEME to J200,... but for a visualization
        // we can consider them equivalent and skip the xform.
        scenegraphPosition = USpiceTypes::Swizzle(state.r);
    }

For further implementation details please see the example application implementation on GitHub.


Example application on GitHub

Location of example application implementation:

ASAT Debris Unreal Engine Example on GitHub

If you simply clone the repository and attempt to build it, you will get a build error (by design).

C:\git\gamedevtricks\ASATDebrisVisualization\DebrisCloud\Source\DebrisCloud.Target.cs(43,34): error CS0103: The name 'CSpice_Library' does not exist in the current context
ERROR: Expecting to find a type to be declared in a target rules named 'DebrisCloudTarget'.  This type must derive from the 'TargetRules' type defined by Unreal Build Tool.

This happens because MaxQ is not committed to the same repository. One easy solution is to clone the MaxQ repository separately, and then create a symbolic link that maps the appropriate source subdirectory to our the example application (see below).

Cloning and building the example

Temporary instructions, pending Epic’s approval of MaxQ in Unreal Engine Marketplace

Command prompt commands that create the appropriate directory, clone the required projects, and create the necessary symbolic link (on Windows):

mkdir c:\git\gamedevtricks\
cd /D c:\git\gamedevtricks\

rem - clone example project from github
git clone https://github.com/gamergenic/ASATDebrisVisualization.git

rem - clone the MaxQ dependency
git clone https://github.com/Gamergenic1/MaxQ.git

rem - create a directory junction in the sample project for MaxQ
mkdir ASATDebrisVisualization\DebrisCloud\Plugins
mklink /J ASATDebrisVisualization\DebrisCloud\Plugins\MaxQ MaxQ\Plugins\MaxQ

Junction created for ASATDebrisVisualization\DebrisCloud\Plugins\MaxQ <<===>> MaxQ\Plugins\MaxQ

Generate Visual Studio project files

Once you have the complete set of source code in place, generate the visual studio project files:

Use the Explorer context menu to generate Visual Studio project files from the .uproject

You should now have a Visual Studio Solution (.sln file), and can build the project’s Unreal Engine Editor.

Build the Unreal Engine Editor

Hit Control+Shift+B to build the editor, and you should have a successful build.

1>Building DebrisCloudEditor...
...
1>Total execution time: 50.60 seconds
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 1 skipped ==========

Hit the F5 key, which starts a debug session and launches the UE Application.

Running the app with test data

Once the Editor is launched you can click the ‘Play’ button and see what happens.

If all goes well, the Unreal Engine Output window will indicate an http request was sent, and a response received:

LogTemp: Space-Track request: https://gamedevtricks.com/post/call-satcat-rest-api-from-ue-http/fengyun-1c-remaining.json; content:TEST_CONTENT
LogTemp: Space-Track request: https://gamedevtricks.com/post/call-satcat-rest-api-from-ue-http/cosmos-1408-remaining.json; content:TEST_CONTENT
LogTemp: Space-Track response: [
    {
        "CCSDS_OMM_VERS": "2.0",
        "COMMENT": "

And, you should be treated to a 3D display of earth and the debris fields from two Anti-Satellite weapons tests.

2007 Chinese anti-satellite missile test: Fengyun 1C
2021 Russian anti-satellite missile test: Kosmos 1408

The visualization you’re viewing, though, is utilizing “stale” test data. from a test service URL. It is not yet live data from the SatCat. That’s next.

Space-Track.org

Live data is available via the REST API service at Space-Track.org. Note, however, Space-Track.org is a service developed to serve satellite operators, who make use of its telemetry to avoid collisions with debris, etc. It’s not for us nosey tinkering gamedevs, so tread lightly.

Space-Track.org is a site that shares space situational awareness services with satellite operators, academia, and other interested entities.

The user agreement is here:
https://www.space-track.org/documentation#user_agree

User Agreement
Please read the following terms and conditions of the User Agreement carefully! This website permits access to U.S. Government space situational awareness information to approved users only. To obtain access, all users must abide by the following terms and conditions:
The User agrees not to transfer any data or technical information received from this website, or other U.S. Government source, including the analysis of data, to any other entity without prior express approval. See, 10 USC 2274(c)(2).

With great power comes great responsibility… The API Query Builder tool allows users a great amount of power and flexibility in creating queries. Your space-track account may be suspended if you violate the usage policy by querying data too often or by running queries that negatively impact the performance of the website. Repeat offenders may have their account suspended permanently.

Using Space-Track.org for a non-trivial volume of end users is against the User Agreement and will get you in trouble with Uncle Sam. To that end, I’ve provided two sample files you can test your code against:
https://gamedevtricks.com/post/call-satcat-rest-api-from-ue-http/fengyun-1c-remaining.json
https://gamedevtricks.com/post/call-satcat-rest-api-from-ue-http/cosmos-1408-remaining.json
These are the test files the GitHub app uses by default.

The JSON response data format you’ll receive from a Space-Track.org ‘gp’ query.

[
    {
        "CCSDS_OMM_VERS": "2.0",
        "COMMENT": "GENERATED VIA SPACE-TRACK.ORG API",
        "CREATION_DATE": "2004-08-16T23:24:14",
        "ORIGINATOR": "18 SPCS",
        "OBJECT_NAME": "USA 19",
        "OBJECT_ID": "1986-069A",
        "CENTER_NAME": "EARTH",
        "REF_FRAME": "TEME",
        "TIME_SYSTEM": "UTC",
        "MEAN_ELEMENT_THEORY": "SGP4",
        "EPOCH": "1986-09-25T09:25:10.253855",
        "MEAN_MOTION": "16.08291950",
        "ECCENTRICITY": "0.01118730",
        "INCLINATION": "39.0531",
        "RA_OF_ASC_NODE": "276.4305",
        "ARG_OF_PERICENTER": "171.4053",
        "MEAN_ANOMALY": "189.5525",
        "EPHEMERIS_TYPE": "0",
        "CLASSIFICATION_TYPE": "U",
        "NORAD_CAT_ID": "16937",
        "ELEMENT_SET_NO": "999",
        "REV_AT_EPOCH": "306",
        "BSTAR": "0.00269490000000",
        "MEAN_MOTION_DOT": "0.06329737",
        "MEAN_MOTION_DDOT": "0.0000267130000",
        "SEMIMAJOR_AXIS": "6629.674",
        "PERIOD": "89.535",
        "APOAPSIS": "325.707",
        "PERIAPSIS": "177.371",
        "OBJECT_TYPE": "PAYLOAD",
        "RCS_SIZE": null,
        "COUNTRY_CODE": null,
        "LAUNCH_DATE": null,
        "SITE": null,
        "DECAY_DATE": "1986-09-28",
        "FILE": "34511",
        "GP_ID": "15040479",
        "TLE_LINE0": "0 USA 19",
        "TLE_LINE1": "1 16937U 86069A   86268.39247979  .06329737 +26713-4 +26949-2 0  9997",
        "TLE_LINE2": "2 16937 039.0531 276.4305 0111873 171.4053 189.5525 16.08291950003063"
    }
]
Reference Frames

Note that the REF_FRAME field (above) specifies the orientation of the coordinate system used to compute the orbital data. "TEME" means True Equator Mean Equinox.

Orbit Reference Frames

This is different than the reference frame we used to orient the Earth, “J2000”. For the purposes of the visualization we can ignore the differences, as they’re minimally different, visually. This is something you must take into account if you require some precision.

Connecting to Space-Track.org for live data

To actually see live data and query the actual Space-Track.org servers, you’ll need to make a couple of changes within the Unreal Engine Editor.

You’ll need to edit the ‘debris’ map/level. Specifically, edit BOTH debris field actors (BP_DebrisActor-*):

  • Uncheck “Use Test URL”
  • Set “Space Track User” and “Space Track Password” to your login credentials at Space-Track.com
Add your username/password to BP_DebrisActor, uncheck "User Test Url"

Run the app again and check the Editor’s Output Log. As you’ll see, you’re now querying and visualizing up-to-the-minute data from Space-Track.org.

Up-to-the-minute 3D orbital map, showing debris from two anti-satellite tests, obtained via http request to space-track.org
One more time: don’t spam Space-Track.org!

Space-Track.org asks API users to be responsible and adhere usage caps. If you were actually building a visualization app for many users, you’d need to query the Space-Track.org server from your own server and cache some data. That way your servers can handle as many users as needed without adding additional load to the Space-Track.org servers. Be careful not to violate the terms of use, of course, which prohibit arbitrarily redistributing data from the SatCat.

End Result:

Very cool, eh? As promised, we learned:

  • How to send HTTP requests from within Unreal Engine
  • How to parse JSON in the server’s response
  • How to get a list of active space debris objects associated with weapons tests
  • Where to find the orbital data for the debris
  • How to position the debris at any given time
  • How to use test data, and how to switch over to live data from the U.S. Strategic Command servers

Just for kicks, you might want to try a few other visualizations:

The Starlink satellite constellation
Project West Ford Remnants (crazy that this ever happened, right?!)
All Geosynchronous satellites (Orbital Periods of 23 hours, 56 minutes, and 4 seconds)
Try the Celestrak.com API: A New Way to Obtain GP Data (aka TLEs)

Another interesting visualization would be to use the ‘cdm_public’ API to fetch a list of close approaches (“near-misses”), and then render animations of the events.

Good luck, and I hope you can explore this space further! :-D.

GitHub Project:
ASAT Debris Unreal Engine Example on GitHub


Back Link:
Defense: We need clean Anti-Satellite Weapons! Part 2: Intro to Asat Code Tutorials

This article appeared on gameDevTricks.com and has been published here with permission.

Comments are disabled. To share feedback, please send email, or join the discussion on Discord.