Artis Planetarium Show.

Year- 2025
Length- 12 Weeks
Engine- Unreal Engine
Language- Blueprints
Workflow- Scrum, 2-week sprints
Team Size- 3 devs, 7 Artists
Role- Developer

Introduction

This is a prototype commissioned by Artis for their Planetarium, created in XR-Lab as my first internship project. The project is a mixed reality show that uses the planetarium as a skybox. It was designed to be enjoyed by multiple people at once, with the ability for the presenter to have complete control over what everyone sees. The story, on the other hand, is simply about our place in the universe and how we are a significant part of it, even though we are so small.


My Contribution

I wanted to try working on something I had never worked on before, so I decided to pick up three tasks. The first and biggest was the multiplayer functionality of the prototype. The second was creating a controller to move through the show. The last was atom movement, which involved randomizing the movement of atoms.


Table of content

Scene Controller Multiplayer SetUp Widget Interaction; Session Creation. Widget Interaction; Session Listing and Joining. Widget Interaction; Matchmaking Widget Interaction; Putting It All Together. Atom Movement

Scene Controller

The scene controller was a function only available to the host of the "game," so I made sure that only they had access to this ability.

The scene changer was something I had to design from scratch. We needed a way to play the artists' animations to display the story. Simply spawning and destroying objects wasn’t an option because we had to keep the system as lightweight as possible. Keeping everything in a single level wasn’t feasible either.

After some research, I discovered sublevels—a way to hide components rather than destroy them when they weren't needed. But this led to a new challenge: how do I hide something in a way that ensures it's ready when the next sublevel is activated? I had an idea: what if every button press followed a simple sequence?

1. On the first press, states 0 and 1 are activated.

2. On the second press, state 0 is removed, state 1 remains, and state 2 is activated.

3. On the third press, state 1 disappears, state 2 stays, and state 3 becomes active.

To better illustrate this system, I asked ChatGPT to create a table that I could use as a reference.

Visual Sheets

First is the activation. It goes through these functions in that order. The first function is what loads the level and ensures that the order of unloading and loading is correct. When it reaches the end, it resets.

The next step was to ensure the correct animation is playing. I created a list, an enum list, which I assigned each level's main object to by name. That was then converted into data and placed into the event. The event then breaks the data down, ensures that the required Enum for the sequences are the same as the current and next loaded level, and plays it.


Multiplayer Setup

The first thing I did was create the game instance. A multiplayer game instance uses the built-in Unreal Engine online subsystem to host, join, and destroy sessions.

This multiplayer system that I created uses both a local network and an online subsystem. The difference between an Online Subsystem and a Local Network can be understood with a simple analogy.

Imagine you and your best friend are on the same local network. If you have a letter for them, you simply walk over and hand it to them—fast and efficient, but only possible if you're in the same house (local network).

However, if your friend lives in another city, you need a mailman (Online Subsystem), who relies on a postal service (such as Steam, Epic Games, or Unreal’s built-in services) to deliver the letter.

In short, a Local Network sends data directly between players, while an Online Subsystem routes data through external servers, allowing players from anywhere to connect.

This instance is what will be called upon by all of the buttons on the multiplayer screen, which will be shown next. Without this instance, nothing will work as there will simply be no multiplayer to begin with.


Widget Interaction; Session Creation.

To be able to use any of the functions that I created in the previous blueprint, I needed to create widgets. A widget is a 3D UI element created through the UMG in your game world. You use Widget Blueprints for such things. The first widget I will explain is Session Creation.

The buttons you have are as follows: Up Arrow for increasing the max players, Down Arrow for decreasing, Calibration map (which sends the players to the spatial anchor map, you can ignore this button), and finally, the Create button, which creates the session.

After selecting the desired button and creating an OnClicked event, it runs an event that either decreases or increases the variable of Max players, sets it to a Text type, and places that as the variable for PlayerCap.

The next event is Create Session. Similar to the previous function, upon creating an OnClicked event on the required button, the event of Create Session from the Multiplayer game instance is called, and you create a session with the data of the max players, whether LAN is used or not, and with the correct instance.


Widget Interaction; Session Listing and Joining.

The next step is viewing created sessions. A widget displays the session name, player count, and ping, along with a "Join" button.

First, a custom event, Create Session Item, was made using the Blueprint Session Result variable. It retrieves the session name, player count, and max players, converts them to strings, and updates the UI.

Finally, clicking "Join" triggers the Join Session event in the multiplayer instance, joining the session linked to the displayed data.


Widget Interaction; Matchmaking

To list sessions, a Search Sessions event is created. It uses the Find Sessions function from the multiplayer instance to gather available sessions. Each result creates a Session Item widget (from the previous step) and adds it to the panel, displaying session details.

The Refresh event updates the list when the refresh button is clicked. It first clears the panel (via Clear Children) and then calls Search Sessions to find and display new sessions after a short delay.


Widget Interaction; Putting It All Together.

With all the widgets created, the next step is putting everything together. A new SessionUI widget blueprint is made, serving as a container for the main widgets—one for finding sessions and one for creating them. These are properly aligned, but this blueprint itself has no code.

Next, an actor is created and given a widget component, assigning it SessionUI to display the interface. Since VR doesn’t automatically handle widget interaction, this actor includes blueprint code to ensure smooth player interaction.

The actor makes sure the UI is placed correctly and interacts properly. It first sets up the widget interaction class, positioning it correctly. Then, it tracks the player’s hand movements using the pointer index, allowing both hands to interact with the UI.

Finally, it registers where the pointer last touched using Get Last Hit Result, capturing the exact location.


Atom movement

Working on the atoms ended up taking the longest, which was rather suprising for me. It included a lot of math, and I wanted to make it as dry as possible, as such, I created a component. A component that can be attached to the 6 diffrent types of atoms.

First, we start with the beginplay of an indivual atom, which at the start, disables the tick, which is used to move the object. Here, it plays the looping animation of the atom, and gets the Begin function from the component.

The next part starts with it getting the current actor(atom)'s location. It then ensures that the tick function is disabled, gets the target position of where to go from Initilze pattern variable function, gets a small delay and enables the tick once more

This Blueprint sets up a starting and target position for the object. It begins by choosing a random distance (Radius) between 450 and 1800 units, placing the object somewhere along the edge of a circle.

Using sine and cosine, it calculates X and Y positions for a circular layout, then adds a Z value to form a 3D starting point. A small Random Drift value between 80 and 800 is added to give the motion a natural, swaying feel. Finally, it sets the Target Position + some offset, readying the object for smooth, dynamic movement.

The next part is to check if is grabbed or not. If it is grabbed, it disables the tick, if not, let's say that you let go, it sets a variable called return timer to 0, sets a bool to true, and renables the tick function. However, if is grabbed, it actually saves the position where it was grabbed at.

Finally, the tick, which ensures that the object is moving. Depending on weather the boolean early is true or not, it either moves it in it's looping idle function, or the one time movement of Return.

Idle ensures that it does not speed up at incredible speed and moves back and forth, moving it between StartPos and TargetPos. The alpha, which when it's 0, is at start, and 1 is at target, keeps increasing back and forth, ensuring that it moves back and forth smoothly.

Return meanwhile uses the timer as the Alpha of Og pos and the current location, before getting that as a vector.

When it gets both vectors from Idle and return, it then chooses depending on if it is moving to the og pos or not from the boolean, and makes that as the target of the moving actor.


Retrospective

Looking back at this project, there is not much I believe I could have done much better. Code readabilty is always there to improve, and keeping eveyrthing as dry as possible too. I think the use of more functions and function only variables would have also been a rather good idea.