About Metrics
GameMaker now allows users to collect PC metrics for use in live wallpapers through Opera GX.
Live wallpapers made in GameMaker can include the Wallpaper Subscription Data event. In this event, you can collect a range of live information from a user's PC including information about their CPU, GPU, RAM, disk, network, battery or system audio output.
You can read everything there is to know about this in the manual entry for wallpaper_set_subscriptions.
Read the rest of this guide to create a specific example that uses metrics.
Before we start, if you have not made your first Live Wallpaper yet, you can do so here.
Note: Metrics in wallpapers running in the Opera GX browser are currently only supported in Early Bird, which you can enable in the settings of your browser.
Note: Download the projects created in this tutorial: bathtub-lw-subscriptions, blank-lw-subscriptions
Standalone Application
First of all to make use of the new features you will need to download the latest version of the Opera GX Live Wallpaper app. This is a standalone application that runs live wallpapers for your desktop background.
To get the app you’ll need to have Opera GX installed.
Open Opera GX and in your settings search for “Live Wallpapers” or enter this link into the search bar “opera://settings/live_wallpapers”. You should now see an option to download the Live Wallpapers Helper (for this tutorial version 0.0.34 was used). Download, install and run the application and now we are ready to start making wallpapers for it to use!
GameMaker
Download, install and run the GameMaker to begin the real fun!
Create a new project by selecting New, then in project type choose Live Wallpaper -> Blank and then give it a name and save location then click Let’s Go to start making your new wallpaper project.
Wallpaper Subscription Data Event
The wallpaper triggers an event within the project running for any objects that contain the Wallpaper Subscription Data event. This event can be added to any object you wish but for this tutorial, you should create a new object for handling all the metrics subscriptions that can just be added to the room. This object can then be called by other objects in your project to find the values you might need for any live elements you wish!
Let's do the following:
- Create a new object (the name can be anything but for this example, it will be called obj_lw_handler).
- In the new object add a Create event and then make a variable to store the metrics subscription data, this variable will be what stores the JSON object we can collect during the event and can be used to update other variables from this at any time. Call this variable lw_subs_data and make it equal to zero for now.
// Variable for wallpaper subscription data
lw_subs_data = 0;
- Still inside the create event, create a temporary variable for configuring the wallpaper_set_subscriptions function. This variable will be an array of strings that are tags used by the function to set what kind of information the standalone application is allowed to collect. Ideally, you will only want to collect information about the data your project needs to collect. For this example, we have included all the possible tags including “desktop_mouse” that will allow mouse position and state information to be collected as normal.
// Config for subscriptions
var _config_lw_subs = [
"cpu",
"gpu",
"battery",
"ram",
"disk",
"network",
"audio",
"desktop_mouse"
];
- Underneath this, call the wallpaper_set_subscriptions function with the created config parameter so the standalone application will know what data to collect.
// Function calls wallpaper configs
wallpaper_set_subscriptions(_config_lw_subs);
- Now it’s time to finally add the Wallpaper Subscription Data event to the handler object.
- Inside the Wallpaper Subscription Data event set the subscription data variable lw_subs_data to the wallpaper_subscription_data JSON object.
// Sets the wallpaper subscription data for this object
lw_subs_data = wallpaper_subscription_data;
You can call the wallpaper_set_subscriptions function more than once in the project if you decide you wish to change the collected data, just change the input parameters and once called the function will stop and reset the calls from the standalone application.
You have now set up the requirements to collect metric data for the wallpaper congrats! But what's exactly inside of this lw_subs_data JSON object we are storing and how do we access it?
Subscription Data Object
The JSON object is actually a struct containing all the requested subscription metrics, it will only contain values requested by the set function and will omit any fields where devices don't exist such as battery values on a desktop or a PC without a dedicated GPU.
The contents of this object (received as a struct) are detailed on the wallpaper_set_subscriptions page.
Remember changing the config tags used by the set subscriptions function will change what information is available here! Notice mouse state information is not set here as adding the tag allows normal mouse event information such as positions and click states to work normally in GameMaker, for live wallpapers using the standalone application this information is not freely available for use in projects and the tag must be set to use them.
There is a lot of information within this object and sorting through the data structure fields to collect specific information that may or may not exist can be complex and time-consuming to set up every time. For this example, we have simplified this process by creating and storing variables containing this data inside the handler object so they can easily be set during the event update and then stored for use later.
Updates to the Handler Object
Set up the variables within the obj_lw_handler create event after the set subscriptions function to look like the following:
// Variables for possible data that might exist
// CPU
cpu_exist = false;
cpu_count = 0;
cpu_name = []; // String
cpu_logical_cores = []; // Int
cpu_physical_cores = []; // Int
cpu_usage = []; // Int (Percentage)
cpu_curr_speed = []; // Int (MHz)
cpu_max_speed = []; // Int (MHz)
cpu_power = []; // Int (V)
// GPU
gpu_exist = false;
gpu_count = 0;
gpu_name = []; // String
gpu_usage = []; // Int (Percentage)
gpu_clock_speed = []; // Int (MHz)
gpu_fan_speed_pct = []; // Int (Percentage - Nvidia)
gpu_fan_speed_rpm = []; // Int (RPM - AMD)
gpu_power = []; // Int (Watt)
gpu_temp = []; // Int (Celsius)
gpu_memory_used = []; // Int (bytes)
gpu_memory_available = []; // Int (bytes)
gpu_memory_total = []; // Int (bytes)
// Battery
battery_exist = false;
battery_count = 0;
battery_name = []; // String
battery_is_charging = []; // Bool
battery_charge = []; // Int (Percentage)
battery_time = []; // Int (Minutes)
// RAM
ram_exist = false;
ram_count = 0;
ram_name = []; // String
ram_used = []; // Int (bytes)
ram_available = []; // Int (bytes)
ram_total = []; // Int (bytes)
// Disk
disk_exist = false;
disk_count = 0;
disk_name = []; // String
disk_used = []; // Int (bytes)
disk_available = []; // Int (bytes)
disk_total = []; // Int (bytes)
// Network
network_exist = false;
network_count = 0;
network_bandwidth = []; // Int (bps)
network_send = []; // Int (bps)
network_receive = []; // Int (bsp)
// Audio
audio_exist = false;
audio_freq = 0; // Int
audio_output = []; // Int
Now you have variables to store all this information it's time to set up a new function called update_lw_subs underneath the variables inside the objects create event. This function will be called after you update lw_subs_data and will check if each variable component exists inside the JSON object before updating or resetting the data associated with it. Likewise, it will check for the device count of certain components and only update those according to what devices exist.
Create the new update_lw_subs function by first checking if the variable for the type of metric exists (i.e. battery). And then checking the array length to store the count for how many metric sets of that type of data exist. Then loop for that count setting the variables in the arrays at the loop counter from the matching data in the lw_subs_data object.
Here is an example of collecting the data for any battery names, charging states and levels:
// Check if battery data exists
if (variable_instance_exists(lw_subs_data, "battery"))
{
// Set variables from sub data safely
battery_exist = true;
battery_count = array_length(lw_subs_data.battery);
// Loop for device count
for (var _i = 0; _i < battery_count; _i++)
{
// Check for variable as might not always exist
if (variable_instance_exists(lw_subs_data.battery[_i], "name"))
{
battery_name[_i] = lw_subs_data.battery[_i].name;
}
else
{
battery_name[_i] = ""; // Not available
}
// Check for variable as might not always exist
if (variable_instance_exists(lw_subs_data.battery[_i], "is_charging"))
{
battery_is_charging[_i] = lw_subs_data.battery[_i].is_charging;
}
else
{
battery_is_charging[_i] = false; // Not available
}
// Check for variable as might not always exist
if (variable_instance_exists(lw_subs_data.battery[_i], "remaining_charge_pct"))
{
battery_charge[_i] = lw_subs_data.battery[_i].remaining_charge_pct;
}
else
{
battery_charge[_i] = -1; // Not available
}
}
}
else
{
// Reset variables
battery_exist = false;
battery_count = 0;
}
Notice how each variable is checked before it is set, this is because there isn't always variable information available with specific components, and trying to access this information will throw some unwanted errors.
In your project, you should repeat this same process for any other variables you might want to collect. In this tutorial you will need all the following subscription variables information:
- CPU
- Name
- Usage
- GPU
- Name
- Temperature
-
Battery (Done in the example above)
- Name
- Charging State
- Charge Percentage
- Ram
- Used bytes
- Total bytes
- Disk
- Used Bytes
- Total Bytes
- Network
- Sent Bytes
- Received Bytes
- Audio
- All outputs when exiting
Now all you need to do is call the function to update after the lw_subs_data variable is updated inside the Wallpaper Subscription Data event by adding this one line!
update_lw_subs();
If you want more control over when this information is updated you can also add a cooldown to this function to only trigger once every second or however often you like, it will always set the variables to the most up-to-date information set to lw_subs_data that is in turn updated whenever the event passing new data is called from the standalone app.
Note: If you’d rather, you can always access the subscription data directly from the lw_subs_data object rather than setting new variables for specific values, just make sure you are always checking if the variable exists and the data you are trying to access isn't out of scope before reading it.
Following these steps mean the metric variables needed are now in a self-contained object that can be added to any room to collect values from. This is what you can now use from other objects within your project to access data from. You can read out names, add values to existing game objects or anything you like to make your wallpapers come alive!
Audio Visualiser
Here is a guide on how to create an audio visualiser from the audio output data the handler contains.
- Start off by creating a new object called obj_audio_visualiser. This object will handle all your data needed for the visualiser and will create, update and destroy audio bars as needed.
- For the new object add a Create event.
- Inside the create event add a check for the obj_lw_handler existing within the room, make the object check if the handler object doesn’t already exist telling it to create one if necessary. This will look like this:
// Checks for handler
if (!instance_exists(obj_lw_handler))
{
// Creates the LW handler
var _lw_handler = instance_create_depth(0, 0, 0, obj_lw_handler);
}
- Add variables to the visualiser object to set up the length of how many bars you want to appear on screen, as well as the rates the audio will scale up and down at (The audio bars are scaled at a rate so that their motion seems more smooth, audio is only updated around 10 times per second meaning if you were to change the bar values to these immediately the movement would look quite staggered and choppy). You should also create an output array for the audio lengths the size of your array length to give the values a place to be stored. It should all look something like this added to the Create event under the previous statement.
// Variables for target audio values (smoothing)
staged_audio_length = 128;
staged_audio_output = array_create(staged_audio_length, 0);
audio_scale_rate_down = 0.01;
audio_scale_rate_up = 0.1;
- Add a minimum and maximum height size for your variable too as you may want them to always be visible and never exceed a certain height, these values can also be adjusted by other variables from subscription data later. This project limits this to half the room height.
// Variables for the min and max bar heights
min_height = 0;
max_height = room_height * 0.5;
- Create a new empty Object called obj_audio_bar for now and give it a single white pixel sprite. This will be used to visually represent each individual bar for audio levels. Giving it a white pixel sprite means we can scale it however we want, change its colour blend and use it in collisions with other objects too!
- Back inside obj_audio_visualiser add a Step event as we need it to update.
- Check to see if audio exists! This is done since running the project in Opera GX or changing the wallpaper_set_subscriptions to not include audio would mean the obj_lw_handler wouldn't contain any audio data meaning there is no need for anything visual to exist or update.
- When the audio data doesn't exist the project won't need any audio bars either so any of these might exist at this point. This will all look like so inside the Step event:
// Checks for if audio exists
if (obj_lw_handler.audio_exist)
{
/// Create and update bars
...
}
else
{
// With all the audio bars
with(obj_audio_bar)
{
// Destroy them
instance_destroy();
}
}
- Time to create the bars, inside the check for audio create another check to see if audio bars exist and if they don’t create a loop that runs for the audio length of bars you wish to have and just set them all to the same initial position. Pass into them however the variables for the stage length and their index number from the loop as they will need to have an ID attached to them. Also, set the staged_audio_input at this point in the array to 0 as you will want the bars to start from 0 before they start moving.
// Checks if bars don't exist
if (!instance_exists(obj_audio_bar))
{
// Loops through lengths
for (var _i = 0; _i < staged_audio_length; _i++)
{
// Creates the new bars
var _new_bar = instance_create_depth(x, y, depth + 10, obj_audio_bar);
_new_bar.stage_length = staged_audio_length;
_new_bar.staged_id = _i;
// Sets the audio output to 0 as inital value
staged_audio_output[_i] = 0;
}
}
- Now back inside the Create event of obj_audio_bar set some initial variables for these values to go into and set up a new empty function called calibrate_bar with a parameter to set the value of the bar from the visualiser when updated.
// Variable for bars on screen
stage_length = 0;
// Variable for the indivdual bar
staged_id = -1;
staged_output = 0.1;
// Function for setting bar to value
calibrate_bar = function(_output)
{
// Calibrate bar
...
}
- The calibrate_bar function is what sets the bar as the bar object has no step event. In this function, the bar's position, width and height are calculated from the visualiser settings as well as any other visual effects such as its image blend. First, the x position is calculated from the wallpaper's width, the number of bars that exist and the index or ID of the individual bar. With this, the y position is calculated from the wallpaper's position and the output value of the audio. With this, the width and height can be calculated.
For added visuals we can also colour the image blend with custom RGB values based on how close the output value is to the maximum possible value. Doing all this you should have something that looks like so:
// Sets the output to the parameter
staged_output = _output;
// Center point of screen
var _center_x = obj_camera.wallpaper_width * 0.5;
var _center_y = obj_camera.wallpaper_height * 0.5;
// Display dimensions
var _curr_display_width = camera_get_view_width(view_camera[0]);
var _curr_display_height = camera_get_view_height(view_camera[0]);
// Adjusted origin coordinates
var _origin_x = _center_x - (_curr_display_width * 0.5);
var _origin_y = _center_y - (_curr_display_height * 0.5);
// Calculated bar width
var _bar_width = _curr_display_width * (1 / stage_length);
// Sets the new coordinates and image scale
x = _origin_x + staged_id * _bar_width;
image_xscale = _bar_width;
y = _origin_y + _curr_display_height - staged_output;
image_yscale = staged_output;
// Sets the colour of each rgb value based on the values
var _red_channel = 255 * (staged_output / obj_audio_visualiser.max_height);
var _green_channel = (255 * (1 - (staged_id / stage_length * 0.5))) * (1 - (1 / (255 / _red_channel)));
var _blue_channel = 255 * 0.1;
// Creates new colour from mixed channels
var _mixed_channels = make_color_rgb(_red_channel,_green_channel,_blue_channel);
// Applies new colour to image blend
image_blend = _mixed_channels;
- Now it's time to create a call for this calibrate_bar function with the correct values from obj_audio_visualiser. Return to the Step event just after the if statement where the bars are created. Create a new variable the size of how many audio outputs there are, this will be used to calculate the value of each bar. Set up two more variables for scaling the bars called max audio and height scale, these will be adjusted as bars go over what is considered the max values. Create a counter for what bar or segment is currently being set and then another array for all the segment values that can exist (the staged audio length plus 1 since there can be remainder values that are unwanted). Then create a bar counter set to 0, threshold set to 1 and scale that is set from the length to the power of 1 divided by the amount of bars. The whole setup will look like this:
// Variable for audio output length
var _ao_length = array_length(obj_lw_handler.audio_output);
// Variables for the max recorded audio and its scale
var _max_audio = 0;
var _height_scale = 1.0;
// Variables for audio output counter and values
var _ao_segment_counter = 0;
var _ao_segment_value = array_create(staged_audio_length + 1, 0);
// Variables for audio bar counter and threshold that increases
var _ao_bar_counter = 0;
var _ao_bar_threshold = 1;
// Scale the threshold will increase by
var _ao_bar_scale = power(_ao_length, 1 / staged_audio_length);
- After that, loop through the whole length of the audio output adding each new value to the current segment value increasing the counter for bars counted. When the counter reaches the threshold check the segment value against the max value variable and update it if needed before adjusting the scale by setting it to the current length to the power of itself. Adjusting the threshold value by setting it to the power of the scale and then increasing the counter for segments and resetting the bar counter before returning to the beginning of the loop again, yes that's a lot going on! Here's what it all looks like:
// Loops through the available audio outputs
for (var _i = 0; _i < _ao_length; _i++;)
{
// Adds the value to the current segment
_ao_segment_value[_ao_segment_counter] += abs(obj_lw_handler.audio_output[_i]);
// Increases the counter
_ao_bar_counter++;
// Checks if the counter has met the threshold
if (_ao_bar_counter == _ao_bar_threshold)
{
// Checks if the value is over the current max audio and isn't the last audio output
if (_ao_segment_value[_ao_segment_counter] > _max_audio && _ao_segment_counter < staged_audio_length)
{
// Sets the max audio to the segement value
_max_audio = _ao_segment_value[_ao_segment_counter];
}
// Checks if the segement counter is over 0
if (_ao_segment_counter > 0)
{
// Sets the scale to the new power based on current length
_ao_bar_scale = power(_ao_length, _ao_bar_scale);
}
// Sets a new threshold variable to the scale from the old threshold
var _new_threshold = power(_ao_bar_threshold, _ao_bar_scale);
// Sets the threshold to the variable if different or the current variable plus one
_ao_bar_threshold = (_new_threshold == _ao_bar_threshold ? _new_threshold : _ao_bar_threshold + 1);
// Increase the segment counter
_ao_segment_counter++;
// Reset the bar counter
_ao_bar_counter = 0;
// Checks if the segment counter has reached the staged audio length
if (_ao_segment_counter == staged_audio_length)
{
// Breaks the loop
break;
}
}
}
- That was the hard part. Now change the scale if needed by checking the new max audio against the max height, if it's bigger change the scale to match the max height to the max audio level recorded.
// Checks if the max audio is greater than the max height
if (_max_audio > max_height)
{
// Sets the scale to the new max height
_height_scale = max_height * (1 / _max_audio);
}
- Create a loop for the audio bars again and set a target value to be the segment value adjusted by the scale. Also, restrict the target value by the max height and then lerp the actual staged output up to/down to the target at different rates for smoother movements.
// Loops through the audio bars length
for (var _i = 0; _i < staged_audio_length; _i++)
{
// Sets the new audio output target
var _ao_target = _ao_segment_value[_i] * _height_scale;
// Sets the target to never go over the max height
_ao_target = min(_ao_target, max_height);
// Checks if the new audio target is less than the current output
if (_ao_target < staged_audio_output[_i])
{
// Lerps the value down at the down rate
staged_audio_output[_i] = lerp(staged_audio_output[_i], _ao_target, audio_scale_rate_down);
}
else
{
// Lerps the value up at the up rate
staged_audio_output[_i] = lerp(staged_audio_output[_i], _ao_target, audio_scale_rate_up);
}
}
- Now all you have to do is call the calibate_bar function created earlier for each obj_audio_bar with the new staged value.
// With all the audio bars
with (obj_audio_bar)
{
// Calibrate them to their output id value
calibrate_bar(obj_audio_visualiser.staged_audio_output[staged_id]);
}
And that’s it. Just add the obj_audio_visualiser object into your rooms instances layer and you have a working project!... but how do you test it?
Running and Testing the Live Wallpaper
To see the wallpaper running do the following:
- Make sure you have the GX Live Wallpaper Standalone App installed.
- Change the GameMaker project target to Windows.
- Use F5 or the play button to Run the application.
- Once the new project window opens, tab back into GameMaker and use the stop button to close the window. (The close button will sometimes hang on the new window. This method ensures you can keep working without getting stuck looking at a blank background)
- Go into your taskbar systray (the up arrow is usually on the bottom right of your main monitor) and right-click on the Opera Logo.
- In the menu go to the wallpapers tab and you should see your project here.
- Click on that and voilà! Your new live wallpaper with working audio metrics (good work!).
Note: You can always hide desktop icons by right-clicking on the desktop, going to view, and then clicking hide desktop icons. This also stops the dragging effect Windows has for selecting icons if you want to interact with elements in your live wallpaper later!
Custom Water Visualiser
Now you have a working visualiser processing the audio data into a visual representation why not theme it for your wallpaper project? Let’s start by duplicating the visualiser (or just write over it if you don’t need it in your project anymore) and renaming the new obj_audio_visualiser to obj_water_line, set the original obj_audio_visualiser as a parent for the object so other objects checking its values will still be able to do so without needing to be edited. You should also create a new object called obj_water_segment and set its parent to be obj_audio_bar. The new water segment bars are going to do essentially the same thing as the old audio bars except they will not draw themselves so they can be invisible while still being used within collisions. To do this let's set obj_water_segment to have the same pixel sprite as its parent and add a new Draw event to it. As we don't want anything to draw you should just leave it completely blank (or add an exit for clarity to leave the event).
Now the new water segments are complete, go back to obj_water_line and inside its Create event change the staged audio length to a smaller value (for the example project just 4 is used) to create a smaller waveform. Underneath the variables also add a new variable for the water's surface, this is going to be the texture it is set to. Use the surface_create function and set its parameters to the dimensions of the wallpaper, it should look like this:
// Creates a surface for the water texture to be created in
underwater_surface = surface_create(obj_camera.wallpaper_width, obj_camera.wallpaper_height);
Most of the functionality of the object's Step event will be exactly the same with just a few small changes. Start by changing the object used inside the loop that creates the audio bars from obj_audio_bars to obj_water_segments since we want our new hidden water bars to be what we create inside the room. After that, towards the end of the event after the bars are calibrated, add an extra line to adjust the bars' y position to reduce by the min_height value of obj_water_line. But wait, we aren't actually changing this value yet but we will soon!
Now create a new Draw event for obj_water_line. This draw event is going to do a few things:
- Firstly the obj_lw_handler disk values will affect the minimum and maximum heights that are drawn (this is where the min_height variable is changed).
- The event will only draw if audio values exist so it will check for this.
- The coordinate positions of the audio bars will be collected here to be used to form the new wave of water as a primitive shape.
- The shape will need a texture so this is also created here.
- Positions stored are then looped through to form each “segment” as 2 triangles with the appropriate texture positions as well.
- The waveform is then tailed off at the end to look natural and not just stop in the middle of the screen.
- Finally, the top of the waveform is also created from the positions but as a row of touching triangles with just a solid color to create a depth perspective.
To loop through the disk values the obj_lw_handler is used again. It is checked to see if disk data exists and then counts the sum total and used values before then adjusting the min_height variable. The max height is then adjusted too from this set value. This is what it should look like:
// Checks if disk metrics exists
if (obj_lw_handler.disk_exist)
{
// Set variables for disk usage
var _disk_used = 0;
var _disk_total = 0;
// Loop through available disks
for (var _i = 0; _i < obj_lw_handler.disk_count; _i++)
{
// Add disk values to count
_disk_used += obj_lw_handler.disk_used[_i];
_disk_total += obj_lw_handler.disk_total[_i];
}
// Create min variable based on screen size (third of screen)
var _curr_display_min = camera_get_view_height(view_camera[0]) * (1 / 3);
// Calculate min height as a percentage of used disk space
min_height = _curr_display_min * (_disk_used / _disk_total);
// Cap min height to never be less than 100
min_height = max(min_height, 100);
}
else
{
// Sets default min height of 100
min_height = 100;
}
// Set max height from adjusted min height
max_height = min_height + 100;
Similar to how we collected the data initially from the obj_lw_handler the data used for drawing the waves are from the actual obj_water_segment objects as these values have been set at their lerped values to create smoother-looking bars. To ensure there are values to draw from before we start, check the obj_lw_handler for if audio exists. Then inside this statement start by creating some temporary variables for the point coordinates and wallpaper screen dimensions.
// Create variables used to store values for wave form cooridinates
var _point_coords = [0, 0];
var _wave_points = array_create(real(staged_audio_length), _point_coords);
// Wallpaper position data
var _center_x = obj_camera.wallpaper_width * 0.5;
var _center_y = obj_camera.wallpaper_height * 0.5;
var _curr_display_width = camera_get_view_width(view_camera[0]);
var _curr_display_height = camera_get_view_height(view_camera[0]);
var _origin_x = _center_x - (_curr_display_width * 0.5);
var _origin_y = _center_y - (_curr_display_height * 0.5);
You will also need to create two new variables called _max_x and _max_y. These variables are used to set the primitive shape and texture coordinates for them. The values for these are set from the bottom right corner coordinates that can be calculated from the origin and the display dimensions.
// Works out max positions for display
var _max_x = _origin_x + _curr_display_width;
var _max_y = _origin_y + _curr_display_height;
Also, create another variable here that will be used for adjusting the drawing position to be higher than the actual position of the invisible bars as this will give a sense to objects that they are slightly submerged underwater and not just sitting on the surface (We used a value of 20 but you can set this to whatever you want).
// Variable for water level (lets the bottoms of object look submerged)
var _y_adjustment = 20;
Now with all the obj_water_segments inside of the room store their x and y coordinate positions inside the _wave_points array with their position inside the array being their staged_id variable.
// Loops through water segments
with (obj_water_segment)
{
// Saves their coordinates with their id
_wave_points[staged_id] = [x, y];
}
Before we can start drawing the wave it’s going to need a texture. This texture is set by redrawing the background sprite with transparency with an added image blend gradient so deeper water seems darker. This sprite can’t just be drawn as normal within the event or else it will be fully visible and pointless to have, instead it is drawn to the underwater_surface made inside the created event. The surface should be resized to the maximum dimensions of the screen before then being set as the target. Then once the custom background water sprite has been drawn the target should be reset using the surface_reset_target function. Once this is all done the surface can then be made in a texture variable by calling the surface_get_texture function.
// Resize the surface
surface_resize(underwater_surface, _max_x, _max_y);
// Set the surface as render target
surface_set_target(underwater_surface);
// Creates custom colour
var _col_dark = make_color_rgb(45, 154, 228);
// Draw the background image using max values as dimensions with a gradient and transparency
draw_sprite_general(spr_background_tub, 0, 0, 0, _max_x, _max_y, 0, 0, 1, 1, 0, c_white, c_white, _col_dark, _col_dark, 0.5);
// Reset the surface render target
surface_reset_target();
// Set draw and texture options for waveform from surface
draw_set_colour(c_white);
var _tex = surface_get_texture(underwater_surface);
Now it's time to start drawing each segment of the wave by starting a primitive shape and then looping through the values in a pattern to form triangles. Start the draw by calling draw_primitive_begin_texture and setting its parameters to use triangle strips and the created texture. Then create the initial vertex point at the bottom left of the visible screen setting its texture coordinates to the same values but divided by the _max_x or _max_y values to calculate the values needed between 0 and 1.
The vertices are drawn in a pattern like so:
Apart from the origin to the first value that is only set once this method is looped for lines 2 - 5 by setting the 4 vertex points where the lines end. This process is then looped for each segment except the very last where a final y point is made from the average height of the very first and then last points. This last section is then manually set and the draw_primitive_end function is called to complete the new shape.
// Loops through the audio values stored
if (staged_audio_length > 0)
{
// Create primative shape using the texture set
draw_primitive_begin_texture(pr_trianglestrip, _tex);
// Create inital point as bottom left of screen
draw_vertex_texture(_origin_x, _origin_y + _curr_display_height, _origin_x / _max_x, (_origin_y + _curr_display_height) / _max_y);
// Loop for all but last points available
for (var _i = 0; _i < staged_audio_length - 1; _i++)
{
// Create vertex group for each set of points
draw_vertex_texture(_wave_points[_i][0], _wave_points[_i][1] - _y_adjustment, _wave_points[_i][0] / _max_x, (_wave_points[_i][1] - _y_adjustment) / _max_y);
draw_vertex_texture(_wave_points[_i + 1][0], _wave_points[_i + 1][1] - _y_adjustment, _wave_points[_i + 1][0] / _max_x, (_wave_points[_i + 1][1] - _y_adjustment) / _max_y);
draw_vertex_texture(_wave_points[_i][0], _origin_y + _curr_display_height, _wave_points[_i][0] / _max_x, (_origin_y + _curr_display_height) / _max_y);
draw_vertex_texture(_wave_points[_i + 1][0], _origin_y + _curr_display_height, _wave_points[_i + 1][0] / _max_x, (_origin_y + _curr_display_height) / _max_y);
}
// Draw last point of sets
draw_vertex_texture(_wave_points[staged_audio_length - 1][0], _wave_points[staged_audio_length - 1][1] - _y_adjustment, _wave_points[staged_audio_length - 1][0] / _max_x, (_wave_points[staged_audio_length - 1][1] - _y_adjustment) / _max_y);
// Calculated final point
var _final_y = ((_wave_points[staged_audio_length - 1][1] - _y_adjustment) + ( _wave_points[0][1] - _y_adjustment)) * 0.5;
// Final group of points
draw_vertex_texture(_origin_x + _curr_display_width, _final_y, (_origin_x + _curr_display_width) / _max_x, _final_y / _max_y);
draw_vertex_texture(_wave_points[staged_audio_length - 1][0], _origin_y + _curr_display_height, _wave_points[staged_audio_length - 1][0] / _max_x, (_origin_y + _curr_display_height) / _max_y);
draw_vertex_texture(_origin_x + _curr_display_width, _origin_y + _curr_display_height , (_origin_x + _curr_display_width) / _max_x, (_origin_y + _curr_display_height) / _max_y);
draw_vertex_texture(_origin_x + _curr_display_width, _final_y, (_origin_x + _curr_display_width) / _max_x, _final_y / _max_y);
// End primative draw
draw_primitive_end();
}
This process is then somewhat repeated for the top of the visible wave with a solid colour primitive shape that is a strip of stretched-out triangles made from the tips of each wave that appears. The effect is simple to implement but gives a great additional touch to the wave without too much work.
// Set new colour for top of wave
draw_set_colour(make_color_rgb(156,212,248));
// Loop through audio again
if (staged_audio_length > 0)
{
// Draw only from colour new primative
draw_primitive_begin(pr_trianglestrip);
// Create inital point
draw_vertex(_wave_points[0][0], _wave_points[0][1] - _y_adjustment);
// Loop for all but last segments
for (var _i = 0; _i < staged_audio_length - 1; _i++)
{
// Draw only one point for each wave
draw_vertex(_wave_points[_i + 1][0], _wave_points[_i + 1][1] - _y_adjustment);
}
// Draw last point
draw_vertex(_wave_points[staged_audio_length - 1][0], _wave_points[staged_audio_length - 1][1] - _y_adjustment);
// End draw
draw_primitive_end();
}
Now just reset the draw colour to white and you have a working wave with its minimum height set to the disk space used! Just swap out the visualiser active within the room and follow the steps from before to use the wallpaper with the standalone app to see it functioning.
Note: Try swapping out the background sprite to something you think looks the part, just make sure if you do to not forget to update the background sprite for the wave texture to the same sprite too!
Component Spawners
Still want to add more to the scene? This project’s method of doing this is by creating spawners for each type of visualiser you might want. Create a new object for the type of visualiser you want to make. The next example is going to cover a CPU spawner but this process can be repeated for any other type of component, just change the variables being passed through to match the metric subscriptions available to it.
Call the new object obj_cpu_visualiser and give it a Create event. In the same way for the audio visualiser add a check for the obj_lw_handler inside the event and create one in the room if it doesn't exist. Now create a new function called create_cpu that will create a new object called obj_cpu (make an empty one to reference at the moment) each time it's called. Add the following code and set the appropriate ID, name, and usage values from the metric subscriptions available by passing in the CPU’s ID.
// Function for creating a new object
create_cpu = function(_id)
{
// Wallpaper position data
var _center_x = obj_camera.wallpaper_width * 0.5;
var _center_y = obj_camera.wallpaper_height * 0.5;
var _curr_display_width = camera_get_view_width(view_camera[0]);
var _curr_display_height = camera_get_view_height(view_camera[0]);
var _origin_x = _center_x - (_curr_display_width * 0.5);
var _origin_y = _center_y - (_curr_display_height * 0.5);
// Creates new object
var _new_cpu = instance_create_depth(_origin_x + (_curr_display_width * 0.4), _origin_y - 200 , depth + 2, obj_cpu);
// Sets new object variables from id parameter
_new_cpu.staged_id = _id;
_new_cpu.staged_name = obj_lw_handler.cpu_name[_id];
_new_cpu.staged_usage = obj_lw_handler.cpu_usage[_id];
}
Any new CPUs created are just outside the top of the visible screen so they can fall into view after they have been updated.
Create a new Step event for the visualiser object and take a count of how many CPUs currently exist inside the room. Then check through the subscription data and look to see if enough CPU objects exist. If there aren’t enough check through the ones available looking for matching names to the subscription data available. When it has data for one it can’t find it’ll create a new obj_cpu and up its counter. Likewise the same is done in reverse when too many CPU objects are in the room compared to the data. Any CPUs without matching names to the data available will be destroyed until the obj_lw_handler metric data matches what exists in the room.
// Variable for storing object count
var _cpu_count = instance_number(obj_cpu);
// Checks for if data can exist
if (obj_lw_handler.cpu_exist)
{
// Loops while more data exists as objects
while (obj_lw_handler.cpu_count > _cpu_count)
{
// Loops through data count
for (var _i = 0; _i < obj_lw_handler.cpu_count; _i++)
{
// Variable for is found
var _is_found = false;
{
// Checks all objects
with (obj_cpu)
{
// Checks if name is the same
if (staged_name == obj_lw_handler.cpu_name[_i])
{
// Sets found state
_is_found = true;
// Breaks loop
break;
}
}
// Checks if matching object hasn't been found
if (!_is_found)
{
// Creates object with ID
create_cpu(_i);
// Increases object count
_cpu_count++;
// Breaks loop
break;
}
}
}
}
// Loops while less data exists than objects
while (obj_lw_handler.cpu_count < _cpu_count)
{
// Checks objects
with (obj_cpu)
{
// Sets found state to false
var _is_found = false;
// Loops through data values
for (var _i = 0; _i < obj_lw_handler.cpu_count; _i++)
{
// Checks for matching names
if (staged_name == obj_lw_handler.cpu_name[_i])
{
// Sets found to true
_is_found = true;
// Stops breaks loop
break;
}
}
// Checks if hasn't been found
if (!_is_found)
{
// Destroys the object
destroy_cpu();
// Decreases the object count
_cpu_count--;
// Breaks loop
break;
}
}
}
}
else
{
// Loops through all objects
with (obj_cpu)
{
// Calls destroy function
destroy_cpu();
}
}
You now have a working spawner that if modified can easily be used for any other components that can have names! Just duplicate it and change the relevant variables to match. You can even change the way this works to use the index ID numbers for checking over components that don’t have names like networks. Also don't forget to change the destroy function too or just use instance_destroy instead.
Updating Objects
So adding the visualiser to the room will now spawn new blank objects to represent the CPUs so lets do some cool things with them such as allowing them to interact with the room bounding on the waves or be picked up. Most importantly though let them update the information passed through from the obj_lw_hander with the subscription metric data.
Make a Create event for the object and set up variables for the passed-through “Staged” information. Also, create a function for destroying the CPU (you might want to add a popping effect here later for despawning) that we call from the visualiser. And set up other variables for movement speed, momentum and states when picking up or displaying information.
// Variables
staged_id = -1;
staged_name = "";
staged_usage = -1;
// Display info state
display_info = false;
// Sets random momentum for initial movement
angle_momentum = random_range(-90, 90);
// Sets default variables
is_held = false;
is_grounded = false;
// Stores previous mouse states
prev_mouse_x = mouse_x;
prev_mouse_y = mouse_y;
// Stores the collision values for audio bar hits
collisions = 0;
new_velo_x = 0;
new_velo_y = 0;
new_angle_velo = 0;
// Function for destroying the object
destroy_cpu = function()
{
instance_destroy();
}
Add the sprite you want to use too at this point and move onto creating its Step event. In here, set up some temporary variables again for calculating the wallpaper's position in comparison to the room. Because of the way wallpapers scale, traditional x and y positions can actually be at an offset of the room sometimes. This scaling effect means that you should always add a calculated origin value to positions you want to set, treating other positions across the screen as a percentage of the current display dimensions as well. Adding this math again greatly helps! (Notice how _max_y from before reappears too here but now as _floor_level)
// Wallpaper position data
var _center_x = obj_camera.wallpaper_width * 0.5;
var _center_y = obj_camera.wallpaper_height * 0.5;
var _curr_display_width = camera_get_view_width(view_camera[0]);
var _curr_display_height = camera_get_view_height(view_camera[0]);
var _origin_x = _center_x - (_curr_display_width * 0.5);
var _origin_y = _center_y - (_curr_display_height * 0.5);
var _floor_level = _origin_y + _curr_display_height;
Since we are using our modified sea level which will always show some water, adjust the _floor_level by the audio visualiser minimum height after checking if it exists.
// Checks for audio bars
if (instance_exists(obj_audio_bar))
{
// Adjusts the floor level from the audio visualiser settings
_floor_level -= obj_audio_visualiser.min_height;
}
Now update the passed-in variables from the stored index ID that can be found in obj_lw_handler.
// Checks if cpu data can exist
if (obj_lw_handler.cpu_count > staged_id)
{
// Sets the current data from id
staged_name = obj_lw_handler.cpu_name[staged_id];
staged_usage = obj_lw_handler.cpu_usage[staged_id];
}
else
{
// Destroys the object
instance_destroy();
}
Now add a check to see if the object ever goes too far off the left or right side of the screen to destroy it and check it against the ground level to see if it has hit the floor. Adjust its speed, rotation and position to make it have a slight self-correcting bounce.
// Checks if the object has left the play area to the left and right boundaries
if (bbox_right < _origin_x)
{
// Destroys the object
instance_destroy();
}
else if (bbox_left > _origin_x + _curr_display_width)
{
// Destroys the object
instance_destroy();
}
// Checks if the object has reached the floor
if (bbox_bottom > _floor_level - 5)
{
// Calculates the difference off screen
var _diff = bbox_bottom - (_floor_level);
// Adjusts position
y -= _diff;
// Slows the speed
vspeed = -abs(vspeed) * 0.5;
// Calculates the images closest facing in half rotations
var _image_face = round((image_angle % 360) / 180);
var _target_face = _image_face * 180;
// Adjusts image angle to target
image_angle = lerp(image_angle % 360, (_target_face), 0.1);
// Slows the horizontal speed
hspeed *= 0.5;
// Sets the grounded state
is_grounded = true;
}
else
{
// Unsets the grounded state
is_grounded = false;
}
Add a few more lines of code to check if the object is being held or is grounded before allowing gravity to affect it (limiting max speed too!), as well as update the previous mouse positions so any changes in the mouse’s location can be added to the object as well if needed.
// Checks held state
if (is_held)
{
// Stops movement speed
hspeed = 0;
vspeed = 0;
// Offsets for position
var _mouse_origin_x = mouse_x - x;
var _mouse_origin_y = mouse_y - y;
// New angle stored in radians
var _theta = degtorad(-_new_momentum);
// Calculates the adjusted positions from offsets and angle
var _mouse_adjust_x = (_mouse_origin_x * cos(_theta)) - (_mouse_origin_y * sin(_theta));
var _mouse_adjust_y = (_mouse_origin_y * cos(_theta)) + (_mouse_origin_x * sin(_theta));
// Sets new positions from adjusted positions
var _new_mouse_pos_x = x + _mouse_adjust_x;
var _new_mouse_pos_y = y + _mouse_adjust_y;
// Adjusts the position
x += mouse_x - _new_mouse_pos_x;
y += mouse_y - _new_mouse_pos_y;
}
else
{
// Checks is in air
if (!is_grounded)
{
// Fall
vspeed += 0.5;
}
// Slow horizontal movement
hspeed *= 0.9;
}
// Adjust image angle from momentum
image_angle += _new_momentum;
// Slows the angles momentum
angle_momentum *= 0.9;
// Limits the max speed
speed = min(speed, 9);
// Updates previous mouse positions
prev_mouse_x = mouse_x;
prev_mouse_y = mouse_y;
After adding some mouse events for pressing, releasing, entering and leaving the object you should be able to control its holding states just to see the lovely new cpu fall beneath the waves to the floor! Collision events still need to be created between this object and obj_audio_bar. Inside the parent obj_audio_bar add a collision event for obj_cpu. The component objects are going to experience more than one collision most likely when hitting a lot of bars so set up the collisions to add to a charged-up velocity and angle adjustment that will take into consideration how many collisions have taken place before calculating the total adjusted value and applying that to the object. Inside the Collision event add the following:
// Collisions scalar
var _bump_scale = 0.4;
// Bars center x position
var _center_x = x + image_xscale * 0.5;
// Scale of collision strength
var _bump_strength = (y - other.bbox_bottom) * _bump_scale;
// Only applies if less than 1
if (_bump_strength < -1)
{
other.new_velo_y += _bump_strength;
}
// Applies horizontal movement and rotation
if (other.x < _center_x)
{
other.new_velo_x += _bump_strength * 0.2;
other.new_angle_velo -= _bump_strength * 1;
}
else if (other.x > _center_x)
{
other.new_velo_x -= _bump_strength * 0.2;
other.new_angle_velo += _bump_strength * 1;
}
// Counter for collision
other.collisions++;
Once this is done go back into obj_cpu and add a Begin Step event that will handle all the newly applied forces. Check for any collisions or if the object is being held to add momentum to the swing's movements.
// Checks for collisions
if (collisions > 0)
{
// Adds new speed to the current speed scaled by the amount of collisions
hspeed += new_velo_x / collisions;
vspeed += new_velo_y / collisions;
// Adds scaled angle momentum
angle_momentum += new_angle_velo / collisions;
// Resets collision values
collisions = 0;
new_velo_x = 0;
new_velo_y = 0;
new_angle_velo = 0;
}
// Checks held state
if (is_held)
{
// Calculates the change in positions
var _delta_x = mouse_x - prev_mouse_x;
var _delta_y = mouse_y - prev_mouse_y;
// Adds the change to the current position
x += _delta_x;
y += _delta_y;
// Works out the difference between the mouse and the current object coords
var _diff_y = y - mouse_y;
var _diff_x = x - mouse_x;
// Fall
if (_diff_y < 0)
{
// Checks for momentum
if (abs(angle_momentum) > 1)
{
// Clamps
angle_momentum += clamp(angle_momentum * 1.2, -7.5, 7.5);
}
else
{
// Swing
angle_momentum = clamp(_diff_x * -1, -1.1, 1.1);
}
}
else
{
// Swing
angle_momentum -= clamp(_delta_x * min(1, abs(_delta_y) * 0.1), -15, 15) * 0.5;
}
// Horizonal difference
if (abs(_diff_x) > 30)
{
// Fall
angle_momentum -= _diff_x * 0.2 * min(1, (1 - 30 / abs(_diff_x)));
}
}
Running the live wallpaper again now in the standalone app you should have a cpu object appearing from the top of the screen, falling and then bouncing around on your musical waves! (If you still have the original audio visualiser object swapping that in the room, the object should still bounce and spin-off of those bars too!) Next, do this same process again for any other components you want to use such as GPUs or Batteries just remember to change variable names etc to match what you're using.
Now you can utilise your shiny new metrics again to make the objects do some fun things. Why not change the image blend to match your battery percentage or use a speed to adjust animations like we did with the battery object inside of its draw event. You can even set other sprites as decals with set index numbers to change what is drawn like what is done here:
// Variables set for battery cell information
// Sets the image index from the charge
var _bat_image_index = ceil(staged_charge / 10);
// Sets the image blend from the charing state
var _bat_image_blend = (staged_state? c_yellow : c_white);
// Adjusts the animation speed to the cell index
image_speed = 0.1 * _bat_image_index;
// Draws the object
draw_self();
// Draws the cell using the set index and blend from the metric values
draw_sprite_ext(spr_battery_cell, _bat_image_index, x, y, 1.2, 1.2, image_angle + 20, _bat_image_blend, 1);
Finding ideas for different metrics can be fun to experiment with so we also created a duck spawner from network data that every time a network passed a set amount of traffic a duck would spawn! (If you’re lucky one might even have a cool hat on)
// Update stored metrics if network can exist
if (obj_lw_handler.network_count > staged_id)
{
// Set variable values
staged_bandwith = obj_lw_handler.network_bandwidth[staged_id];
staged_send = obj_lw_handler.network_send[staged_id];
staged_received = obj_lw_handler.network_receive[staged_id];
}
else
{
instance_destroy();
}
// Add the send traffic and reset the network's stored value
traffic += staged_send;
obj_lw_handler.network_send[staged_id] = 0;
// Add the recieved traffic and reset the network's stored value
traffic += staged_received;
obj_lw_handler.network_receive[staged_id] = 0;
// Check if enough traffic has passed
if (traffic >= traffic_threshold)
{
// Limit the possible number of ducks at once
if (instance_number(obj_duck) < max_ducks)
{
// Call a function to create the duck object
create_duck();
}
// Decrease the traffic count
traffic -= traffic_threshold;
}
Particle Intensity
You might have noticed the CPU usage value wasn't used for anything yet. Metrics can be used to set intensity levels for particle effects that can be added to objects. For this create a new object called obj_particle_handler. This object will be what keeps the particle effects nice and clean, letting them have their own properties while preventing a pile-up of effects that haven't been cleaned up when finished. The handler will need 3 events: Create, Step and Destroy. Inside the Create event add the following variables and functions to set the particle system used as well as any offset angles it might use.
// Empty variable for setting the particle system to
particle_sys = -1;
// Variable for setting the parent object to set to noone to start
owner = noone;
// Variables used for angle adjust
is_angle_inhert = false;
angle_adjust = 0;
// Variables used for if the particle system is to be set to an offset and what the offset should be
is_offset = false;
x_offset = 0;
y_offset = 0;
// Function used for setting particle systems angle
set_angle = function(_new_is_angle_inhert, _new_angle_adjust)
{
// Sets default angle
var _new_angle = 0;
// Check for if angle should inhert from owner
if (_new_is_angle_inhert)
{
// Sets inhert values
is_angle_inhert = true;
_new_angle = owner.image_angle;
}
// Sets adjustment for angles to be used during updates
angle_adjust = _new_angle_adjust;
_new_angle += angle_adjust;
// Updates particle systems angle
part_system_angle(particle_sys, _new_angle);
}
// Function used for setting particle systems offset position
set_offset = function(_x_offset, _y_offset)
{
// Variables set to new variables
is_offset = true;
x_offset = _x_offset;
y_offset = _y_offset;
var _new_angle = angle_adjust;
if (is_angle_inhert)
{
// Stores the owners angle as a real variable
_new_angle += owner.image_angle;
// Applies angle to particle system
part_system_angle(particle_sys, _new_angle);
}
// Converts angle to radians
var _theta = degtorad(_new_angle);
// Calculates the adjusted repositioned angles from the set offsets and angle
var _adjust_x = (x_offset * cos(_theta)) - (y_offset * sin(_theta));
var _adjust_y = (y_offset * cos(_theta)) + (x_offset * sin(_theta));
// Updates the position to the adjusted owner positions
x = owner.x + _adjust_x;
y = owner.y - _adjust_y;
// Updates particle system position
part_system_position(particle_sys, x, y);
}
// Function used for setting particle system
set_vfx = function(_vfx)
{
// Creates smoke particle system
particle_sys = part_system_create_layer("Effects", false, _vfx);
// Updates particle system position
part_system_position(particle_sys, x, y);
}
Now inside the Step event add the following code that updates the particle effect position to any set variables it might need to consider such as following an owner position or adjusting to offset values.
// Checks if owner has been set
if (owner != noone)
{
// New angle variable
var _new_angle = angle_adjust;
if (is_angle_inhert)
{
// Stores the owners angle as a real variable
_new_angle += owner.image_angle;
// Applies angle to particle system
part_system_angle(particle_sys, _new_angle);
}
// Checks if position should be at an offset
if (is_offset)
{
// Converts angle to radians
var _theta = degtorad(_new_angle);
// Calculates the adjusted repositioned angles from the set offsets and angle
var _adjust_x = (x_offset * cos(_theta)) - (y_offset * sin(_theta));
var _adjust_y = (y_offset * cos(_theta)) + (x_offset * sin(_theta));
// Updates the position to the adjusted owner positions
x = owner.x + _adjust_x;
y = owner.y - _adjust_y;
}
else
{
// Updates the position to the owner positions
x = owner.x;
y = owner.y;
}
// Updates the particle system position to the object position
part_system_position(particle_sys, x, y);
}
// Checks if the burst particle system has finished
if (part_particles_count(particle_sys) == 0)
{
// Destroys the object
instance_destroy();
}
And finally, in the handler's Destroy event add the part_system_destroy function to destroy the particle system.
// Destroys particle system
part_system_destroy(particle_sys);
Now the setup has been completed the effects can now be used, in obj_cpu the CPUs usage is set as a percentage and we have 5 levels (6 if you include no effect) of intensity effects that can be used and occur. Rounding this value then to the nearest 20% and using that as a value of 0 to 5 to translate into an intensity can give the CPU an accurate representation of the workload it is currently going experiencing. This value can be added to a string of a file name and then used to set the effect wanted.
Add the following to the Step event inside obj_cpu or create a function for it that can be called when the object is updated:
// Creates intensity variable
var _intensity = 0;
// Checks the usage value is set
if (staged_usage != -1)
{
// Calculates intensity from the usage percentage
_intensity = round(staged_usage * (1 / 20));
}
// Checks the intensity is not 0
if (_intensity != 0)
{
// Variable of effect set from name
var _new_ps = asset_get_index("vfx_bubbles_" + string(_intensity));
// Create new visual effect
var _new_vfx = instance_create_depth((bbox_left + bbox_right) / 2, bbox_top, depth - 1, obj_particle_handler);
// Setup effect values
_new_vfx.owner = self;
_new_vfx.set_vfx(_new_ps);
_new_vfx.set_offset(0, sprite_width / 4);
_new_vfx.owner = noone;
}
Use this method to add other effects to other metrics such as steam and fire to the GPU's temperature value. Just edit the VFX name to match adjust the offset and follow the parameters to set something that looks more natural.
Background Effects
Now the project is really looking alive, it's time to add those finishing touches. We still have unused subscription metrics from RAM so set up a new obj_ram_visualiser object as before but this time you don't want to use it to spawn new objects, instead set up the visualiser to count the total RAM used and the total RAM available. Use these values to create a percentage that can then be used to edit background effects.
Set up a new effect layer in the room and apply a windblown particle effect to the layer. Set up the size and sprites to whatever you think looks great. (we have bubble sprites available so let's use those again)
Now back inside obj_ram_visualiser the Create event only needs to contain a check for if obj_lw_handler exists inside the room, this is the same way the other visualisers checks before creating a new instance of the handler if needed. Now inside the Step event get the effect you wish to change by using the layer_get_fx function.
// Gets the bubble effect
var _bubble_fx = layer_get_fx("Bubbles");
Then check to see if RAM data exists before creating variables to store counts for the used and total bytes then loop through the RAM adding values to the counters. When finished, calculate these values into a percentage and set that percentage to a rounded number between 0 and 1000 as this is the maximum amount of particles we can create in this effect layer. Use the fx_set_parameter function to “param_num_particles” to the new value to have a working effects layer!
// Checks if the data can exist
if (obj_lw_handler.ram_exist)
{
// Sets variables to store byte data
var _curr_bytes = 0;
var _max_bytes = 0;
// Loops through the available data
for (var _i = 0; _i < obj_lw_handler.ram_count; _i++)
{
// Adds the values from the data to the variables
_curr_bytes += obj_lw_handler.ram_used[_i];
_max_bytes += obj_lw_handler.ram_total[_i];
}
// Calculates the bubbles variable from the stored data
var _bubbles = round((1 - (_curr_bytes / _max_bytes)) * 1000);
// Sets effect particles to new value
fx_set_parameter(_bubble_fx, "param_num_particles", _bubbles);
}
else
{
// Sets effect particles to zero
fx_set_parameter(_bubble_fx, "param_num_particles", 0);
}
Export and Share
Great job! You should now have a working and fleshed-out live wallpaper that features a variety of different uses of PC subscription metrics. When you want to upload and share your mod do the following:
- Change your target for GameMaker into GX.games.
- Click the Create executable button (or use the shortcut Control + F8).
- Click the upload as Live Wallpaper Mod option and wait patiently as your project fully compiles and uploads.
- When finished click the edit mod button and you should open the project in a browser on GX.create
- Make what changes you want to the project on GX.create if any.
- On your browser go to the publishing tab on the left then click on the little box next to Public and when asked hit confirm to let the project go public.
- You should now be able to share the live mod with all your friends to download!
Just remember the live wallpaper mod won't work properly on Opera GX browsers so set the wallpaper using the icon in the systray by clicking Wallpapers -> Live -> Your Wallpaper!
Have fun creating your own and share all your creations with others!