Hyprland Tweaks

Linux is a real joy compared to Windows. Finally, my computer is my own again. No more nagging ads, settings changing automatically, or random telemetry taking up valuable CPU time. Using Linux is like getting rid of that old 1961 American Rambler and finally joining modern society with an electric car.

Using Hyprland is like ditching your car for a spaceship. Not very practical for commuting, but really fun nonetheless. And, hell, everyone stares at you whenever you land it in the grocery store parking lot, so it’s all worth it.

Since Hyprland is a relatively new Wayland compositor, there are a few things that bothered me when attempting to implement the experience I wanted.

Disable Touchpad While Typing

Hyprland has a built in setting (input:touchpad:disable_while_typing) to hopefully prevent palms from messing with the cursor. If you’re lucky, Hyprland thinks your touchpad is a mouse, so this setting doesn’t work at all.

Output of "hyprctl devices"
hyprctl devices

We can re-implement our desired behavior:

  • Read input from our main keyboard
  • Disable the touchpad device on input
  • Re-enable the touchpad device after X ms have elapsed

First we’ll figure out our devices.

ls /dev/input/by-id/
Output of "ls /dev/input/by-id/"

There may be a few that match what we’re looking for. usb-Razer_Razer_Blade-if01-event-kbd was the one that worked in my case.

And then from the previous screenshot where we ran hyprctl devices, we’ve already discovered the touchpad is elan0406:00-04f3:31a6-touchpad.

We could choose any language to write a daemon, but I’ll pick C. It’s fast, performant, and has a tiny memory footprint. This will allow the daemon/program to sit at 0% CPU usage when idle and take up mere megabytes of our RAM.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>

#define KEYBOARD_DEV "/dev/input/by-id/usb-Razer_Razer_Blade-if01-event-kbd"
#define DISABLE_CMD "hyprctl keyword \"device[elan0406:00-04f3:31a6-touchpad]:enabled\" false >/dev/null 2>&1"
#define ENABLE_CMD  "hyprctl keyword \"device[elan0406:00-04f3:31a6-touchpad]:enabled\" true >/dev/null 2>&1"
#define TIMEOUT_MS 300

int ignore_key(int keycode) {
    return (keycode == KEY_LEFTCTRL ||
            keycode == KEY_RIGHTCTRL ||
            keycode == KEY_LEFTALT ||
            keycode == KEY_RIGHTALT ||
            keycode == KEY_LEFTMETA ||
            keycode == KEY_RIGHTMETA ||
            keycode == KEY_FN ||
            keycode == KEY_FN_ESC ||
            keycode == KEY_TAB ||
            keycode == KEY_LEFTSHIFT || 
            keycode == KEY_RIGHTSHIFT ||
            keycode == KEY_ENTER
        );
}

int main() {
    system(ENABLE_CMD);
    
    int fd = open(KEYBOARD_DEV, O_RDONLY | O_NONBLOCK);
    if (fd < 0) {
        perror("Failed to open keyboard device");
        return 1;
    }
    
    struct input_event ev;
    int touchpad_disabled = 0;
    struct timespec last_keypress = {0, 0};
    
    while (1) {
        ssize_t r = read(fd, &ev, sizeof(ev));
        
        if (r == sizeof(ev)) {
            if (ev.type == EV_KEY && ev.value == 1) {
                // Ignore modifier keys
                if (ignore_key(ev.code)) {
                    goto skip_key;
                }
                
                if (!touchpad_disabled) {
                    system(DISABLE_CMD);
                    touchpad_disabled = 1;
                }
                clock_gettime(CLOCK_MONOTONIC, &last_keypress);
            }
        skip_key:;
        } else if (r < 0) {
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("Read error");
                close(fd);
                
                sleep(1);
                fd = open(KEYBOARD_DEV, O_RDONLY | O_NONBLOCK);
                if (fd < 0) {
                    perror("Failed to reopen keyboard device");
                    return 1;
                }
            }
        } else if (r == 0) {
            fprintf(stderr, "Device disconnected, attempting to reconnect...\n");
            close(fd);
            sleep(1);
            fd = open(KEYBOARD_DEV, O_RDONLY | O_NONBLOCK);
            if (fd < 0) {
                perror("Failed to reopen keyboard device");
                return 1;
            }
        }
        
        if (touchpad_disabled) {
            struct timespec now;
            clock_gettime(CLOCK_MONOTONIC, &now);
            long elapsed_ms = (now.tv_sec - last_keypress.tv_sec) * 1000 +
                              (now.tv_nsec - last_keypress.tv_nsec) / 1000000;
            
            if (elapsed_ms >= TIMEOUT_MS) {
                system(ENABLE_CMD);
                touchpad_disabled = 0;
            }
        }
        
        usleep(10000);
    }
    
    close(fd);
    return 0;
}

Let’s use it: gcc typingtpblock.c -o typingtpblock && sudo mv ./typingtpblock /usr/local/bin/

Now we just need to run it on start somehow according to your distribution. Adding it to your Startup_Apps.conf is a fine choice.

exec-once = /usr/local/bin/typingtpblock

https://github.com/gen3vra/hypr-disable-touchpad-while-typing

Fullscreen Window Gaps

Hyprland is a tiling Wayland compositor. You can adjust the gaps and spacing between windows for your preferred look.

general {
  border_size = 1
  gaps_in = 3
  gaps_out = 2
}
Windows with padding in between
gapsin:3 gapsout:2

Here’s a more exaggerated value of 20 for each.

Windows with padding in between

Unfortunately, when you fullscreen an application the gaps you chose will stay the same. This means no matter your gap preference, unless it’s 0, you can see behind the window.

Example of window with visible gaps even though it's fullscreen

You also have to disable rounded corners, otherwise there will be tiny gaps in all four quadrants.

Additionally, by default there’s no visual variation between a window that exists by itself on a workspace, and a fullscreen window. This can lead to confusion unless you explicitly style fullscreen windows borders differently.

We can add some configurations to our hyprland.conf to differentiate it a bit.

windowrule = bordercolor rgb(ffc1e4), fullscreen:1
windowrule = rounding 0, fullscreen:1
windowrule = bordersize 2, fullscreen:1 # or 0 for none

As stated above, if we’ve set any gap size at all, there will still be space between the fullscreen window and the screen edge. This is not ideal.

Let’s fix it. You’d think we can just do something similar to the above, right?

windowrule = gapsin 0, fullscreen:1
windowrule = gapsout 0, fullscreen:1

Wrong! These are not valid properties. You must set them in the general or workspace namespace.

Okay, so we want an application that can do the following:

  • Keep track of the fullscreen state
  • Change the configuration when fullscreen
  • Leave all other windows alone

We could bind a fullscreen shortcut to run a script that would both update the gap settings and toggle fullscreen for the active window. This seems fine and recommended. Unfortunately this is a bad solution, because there are way too many edge cases to handle.

  • Double clicking a titlebar to maximize would not trigger our solution
  • Maximizing, then closing the application window would not update our tracked boolean, making the next maximize do nothing until pressed twice
  • Maximizing, then tabbing to another workspace would mean our settings changes remain, making all normal windows have no gap

    We could try to track window closes and any potential edge case, but it becomes messy and complex quickly, without solving the problem cleanly.

The solution is yet another lightweight daemon. We can track fullscreen changes directly from the compositor socket itself, ensuring we catch everything. Once we know the fullscreen state and which window specifically, it’s trivial to hand that information off to a script that handles setting changes for us.

But wait, how does this solve the problem of settings applying to all our other windows which aren’t fullscreen? The hint was mentioned above.

Hyprland has individual workspace separated settings, so you can do something like this:

workspace = 1, gapsin:3, gapsout:2
workspace = 2, gapsin:10, gapsout:10 # example
workspace = 3, gapsin:5, gapsout:12 # example
workspace = 4, gapsin:20, gapsout:9 # example

This is important because logically, if a window were fullscreened on a certain workspace, no other windows are visible. That means an individual workspace config essentially becomes that window’s config.

The last piece we need is to find out where we can get window information from. The hyprctl activewindow -j command is perfectly suitable for this.

I’m going to write the daemon in C again for the same reasons mentioned above.

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define BUF_SIZE 4096
static void tag_window(const char *addr, int add) {
  if (!addr || !addr[0])
    return;
  char cmd[512];
  int ret =
      snprintf(cmd, sizeof(cmd),
               "hyprctl dispatch tagwindow %s%sfullscreen_mode address:%s > /dev/null 2>&1",
               add ? "+" : "-- -", add ? "" : "", addr);
  if (ret < 0 || ret >= sizeof(cmd))
    return;
  system(cmd);
}
static void run_fullscreen_handler(const char *addr, int fs, int workspace) {
  if (!addr || !addr[0])
    return;
  char cmd[512];
  int ret = snprintf(
      cmd, sizeof(cmd),
      "/home/user/.config/hypr/UserScripts/FullscreenHandler.sh %s %d %d > /dev/null 2>&1",
      addr, fs, workspace);
  if (ret < 0 || ret >= sizeof(cmd))
    return;
  system(cmd);
}
static void query_active_window(void) {
  FILE *fp = popen("hyprctl activewindow -j", "r");
  if (!fp) {
    fprintf(stderr, "Failed to query active window\n");
    return;
  }
  char buf[BUF_SIZE];
  char address[128] = {0};
  int fullscreen = -1;
  int workspace = -1;
  int in_workspace = 0;
  while (fgets(buf, sizeof(buf), fp)) {
    if (strstr(buf, "\"address\"")) {
      sscanf(buf, " \"address\": \"%[^\"]\"", address);
    }
    if (strstr(buf, "\"fullscreen\"")) {
      sscanf(buf, " \"fullscreen\": %d", &fullscreen);
    }
    // Handle json workspace object
    if (strstr(buf, "\"workspace\"")) {
      in_workspace = 1;
    }
    if (in_workspace && strstr(buf, "\"id\"")) {
      sscanf(buf, " \"id\": %d", &workspace);
      in_workspace = 0; 
    }
  }
  pclose(fp);
  if (fullscreen == -1 || !address[0] || workspace == -1)
    return;
  //printf("fullscreen=%d window=%s workspace=%d\n", fullscreen, address, workspace);
  //fflush(stdout);
  if (fullscreen == 1) {
    tag_window(address, 1);
  } else if (fullscreen == 0) {
    tag_window(address, 0);
  }
  run_fullscreen_handler(address, fullscreen, workspace);
}
int main(void) {
  const char *runtime = getenv("XDG_RUNTIME_DIR");
  const char *sig = getenv("HYPRLAND_INSTANCE_SIGNATURE");
  if (!runtime || !sig) {
    fprintf(stderr, "Hyprland environment not detected\n");
    return 1;
  }
  char sockpath[512];
  int ret = snprintf(sockpath, sizeof(sockpath), "%s/hypr/%s/.socket2.sock",
                     runtime, sig);
  if (ret < 0 || ret >= sizeof(sockpath)) {
    fprintf(stderr, "Socket path too long\n");
    return 1;
  }
  int fd = socket(AF_UNIX, SOCK_STREAM, 0);
  if (fd < 0) {
    perror("socket");
    return 1;
  }
  struct sockaddr_un addr = {0};
  addr.sun_family = AF_UNIX;
  strncpy(addr.sun_path, sockpath, sizeof(addr.sun_path) - 1);
  if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("connect");
    close(fd);
    return 1;
  }

  // Normalize workspaces
  char cmd[512];
  int resetRet = snprintf(
      cmd, sizeof(cmd),
      "/home/user/.config/hypr/UserScripts/FullscreenHandler.sh %s %d %d > /dev/null 2>&1",
      "discard", -1, -1);
  if (resetRet < 0 || resetRet >= sizeof(cmd))
    return 1;
  system(cmd);
  
  // Watch for changes
  char buf[BUF_SIZE];
  while (1) {
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n < 0) {
      if (errno == EINTR)
        continue;
      perror("read");
      break;
    }
    if (n == 0)
      break;
    buf[n] = '\0';
    if (strstr(buf, "fullscreen>>")) {
      query_active_window();
    }
  }
  close(fd);
  return 0;
}

gcc fullscreen-window-watcher.c -o fullscreen-window-watcher && sudo mv ./fullscreen-window-watcher /usr/local/bin/

This program will be updated with each fullscreen change from Hyprland itself. It then passes the actual action off to FullscreenHandler.sh with the window address, fullscreen status, and workspace. It also tags the window in case we want to do any future actions, but you may omit this part without any loss of functionality.

The handler script is quite basic, and will update the actual settings.

#!/bin/bash
ADDR="$1"
FS="$2"          # 0, 1, or 2
WS="$3"          # 1-10, or -1 to reset all

# Config file to edit
HYPR_CONF="$HOME/.config/hypr/UserConfigs/UserDecorations.conf"  # adjust if needed

# Normal vs Fullscreen configuration
NO_BORDER_GAP="gapsin:0, gapsout:0"
NORMAL_BORDER_GAP="gapsin:3, gapsout:2"

if [ "$WS" -eq -1 ]; then
    for i in {1..10}; do
        LINE_TO_INSERT="workspace = ${i}, $NORMAL_BORDER_GAP"
        sed -i "/^#${i}:DYNAMIC WORKSPACE PLACEHOLDER \[ns\]/{n;s/.*/$LINE_TO_INSERT/;}" "$HYPR_CONF"
    done
    #echo "Reset all workspaces to normal padding"
    exit 0
fi

# 0 = not fs, 1 = fs, 2 = exclusive fs
if [ "$FS" -eq 1 ]; then
    LINE_TO_INSERT="workspace = ${WS}, $NO_BORDER_GAP"
else
    LINE_TO_INSERT="workspace = ${WS}, $NORMAL_BORDER_GAP"
fi

# Use sed to replace the line after the workspace comment, in-place
sed -i "/^#${WS}:DYNAMIC WORKSPACE PLACEHOLDER \[ns\]/{n;s/.*/$LINE_TO_INSERT/;}" "$HYPR_CONF"
#echo "Updated workspace $WS with $( [ $FS -eq 1 ] && echo 'no-border padding' || echo 'normal padding')"

There’s probably a better way than using sed. Regardless, if you structure a section in your UserDecorations.conf as the script expects, it will work perfectly.

## EXAMPLE ##
# You CAN use a tag but it has a few ms delay and we can handle everything needed with fullscreen:1 match right now
#windowrule = bordercolor rgb(00ff00), tag:fullscreen_mode
windowrule = bordercolor rgb(ffc1e4), fullscreen:1
windowrule = rounding 0, fullscreen:1
# Can do bordersize 10 for a fun indicator around or something
windowrule = bordersize 0, fullscreen:1

# This section is replaced by SED from $UserScripts/FullscreenHandler.sh
#1:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 1, gapsin:3, gapsout:2
#2:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 2, gapsin:3, gapsout:2
#3:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 3, gapsin:3, gapsout:2
#4:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 4, gapsin:3, gapsout:2
#5:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 5, gapsin:3, gapsout:2
#6:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 6, gapsin:3, gapsout:2
#7:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 7, gapsin:3, gapsout:2
#8:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 8, gapsin:3, gapsout:2
#9:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 9, gapsin:3, gapsout:2
#10:DYNAMIC WORKSPACE PLACEHOLDER [ns]
workspace = 10, gapsin:3, gapsout:2

Add a line in our Startup_Apps.conf: exec-once = /usr/local/bin/fullscreen-window-watcher, and voilà.

Example of window touching screen edges

Regardless of bindings or how we achieved fullscreen, our app now has no gap or border. Additionally, tabbing to other workspaces works perfectly, and exiting the app in any way properly resets the settings. Sleek.

https://github.com/gen3vra/hypr-fullscreen-window-watcher

Performance

0% CPU and less than 2MB of RAM for each.


It helps me if you share this post

Published 2025-12-29 00:23:47

rose.dev/pixels

There are a few hidden urls on my site (/secure, /chat, /key, etc) but one I want to highlight is /pixels. This is a fun idea where the same canvas is shared by everyone who visits it, but each visitor is only able to place one pixel on the board every 30 minutes. This leads to a (hopefully) more collaborative effort or getting your friends to draw on the canvas with you.

That’s all for now. : )


It helps me if you share this post

Published 2025-07-11 15:43:13

Modern Pooling Principles in Unity C#

When developing software, performance is one of the most important facets, especially if targeting a platform like web/mobile.

Creating and Destroying objects requires a lot of memory and processing power relative to our other game actions, but we can reduce the impact of Instantiation in Unity by simply reusing them.

In Unity, we can do this by Instantiating all of the objects first, then storing references to them.

We will explore this concept in an example open source game I created ‘slashdot’, which also contains shaders from the last two posts.

https://github.com/gen3vra/slashdot

Setup

We will begin creating the class which will actually handle our pooled objects. When working with pooled GameObjects vs simply Instantiating and Destroying them, we want to be careful of a few key concepts. Firstly, we want to disable most properties for reuse later as opposed to destructing them. Rarely you will need to create and destroy components on initialization, but the vast majority of components or the GameObject itself can be disabled and enabled.

public GameObject enemyPrefab;
public Queue<Enemy> PooledEnemies;
public List<Enemy> TrackedActiveEnemies;

Assign an enemy through the inspector. Next we will create our pools.

Creating the Objects

Call the setup function in the Awake of the class to setup the pool.

void SetupPools()
{
    for (int i = 0; i < 100; i++)
    {
        var enemy = Instantiate(enemyPrefab, Vector3.zero, Quaternion.identity);
        PooledEnemies.Add(enemy.GetComponent<Enemy>());
        enemy.SetActive(false);
    }
}

This will Instantiate all of the objects and keep a reference for us.

Using the Objects

Now, when we want to use a GameObject we can simply call our function in our class from our instance to return a GameObject for us to manipulate.

A super simple implementation might look something like the below.

public GameObject GetEnemy()
{
    GameObject enemy = PooledEnemies.Dequeue();
    return enemy;
}

If only using the <Queue> type and planning for one enemy. However, we want to use multiple enemy types. We can make our pooled enemies a list to have more flexibility. An example implementation for this logic would be an EnemyType enum that the GetEnemy function checks, like so.

public List<Enemy> PooledEnemies = new List<Enemy>();
public GameObject GetEnemy(Enemy.EnemyType enemyType)
{
    foreach (var enemy in PooledEnemies)
    {
        if (enemy.CurrentEnemyType == enemyType)
        {
            PooledEnemies.Remove(enemy);
            return enemy.gameObject;
        }
    }
}

Now we can simply use this as we would an instantiated object.

randomEnemyType = Random.Range(0, 3) == 0 ? 1 : 0;
var enemy = GetEnemy((Enemy.EnemyType)randomEnemyType);
enemy.transform.position = new Vector3(Random.Range(0,100), Random.Range(0,100), enemy.transform.position.y, 0f);
enemy.SetActive(true);
var enemyComponent = enemy.GetComponent<Enemy>();
enemyComponent.Init();
TrackedActiveEnemies.Add(enemyComponent);

Returning the Object to the Pool

We can use a function like the one below to return a used object to the pool after we are done with it.

public void RemoveEnemy(Enemy enemy)
{
    enemy.gameObject.SetActive(false);

    TrackedActiveEnemies.Remove(enemy);
    PooledEnemies.Add(enemy);
}

Simply call RemovePooledEnemy() wherever needed.

Manager.Instance.RemoveEnemy(this);

Re-using Objects

Most of the quirks that you’ll encounter from pooling GameObjects like this stem from figuring out how to reset everything nicely. Unity doesn’t run most code on disabled objects; it’s usually preferable to reset things on Init to avoid unexpected behavior.



Source

Itch.io


It helps me if you share this post

Published 2024-02-07 06:00:00

Unity Shaders Intro Part 1: Shader Graph | Creating Player Highlight / Obscuring Area Effect Mask Shader

Shaders can be a useful way to enhance the visual presentation of your project through subtle or otherwise effects. Beyond code, the engine provides a built in visual scripting tool to create shaders from version 2019 onwards.

We will create an effect that allows us to highlight the player and obscure the rest of our stage. With scripting, we can also modify our exposed shader properties to adjust the intensity of the transparency effect, and transition to having no highlight. Examples will be shown later in the post.

Prerequisites

Ensure you have the Shader Graph package installed in your version of Unity. I am using 2022.3.17f for this post.

Creating the Shader

Right click in your Unity Project and do Create > Shader Graph > Blank Shader Graph

Now that we have a Shader Graph file, simply open the editor by double clicking it.

Let’s add some basic shader properties first. Navigate to the Graph Settings and add Built In as a target. We want the ability to control the transparency of our pixels, so also add the Alpha property to our fragment.

In order to properly utilize the Alpha property, we will need to edit the Built In settings Surface Type to Transparent.

Shader Inputs

The first thing to consider is the Player’s world position. Since we want the highlight effect to follow the player, we’ll need some sort of input into the shader.

In the Shader Graph editor, ensure the ‘Blackboard’ option is checked and visible, then click the plus button on the left side of the editor to create an input variable. Make it a Vector3 category. The ‘Name’ is for visual purposes, and the ‘Reference’ field will allow scripts access to the property. Give that some value like “_PlayerPosition” and drag it into the stage.

Since that’s simply a Vector, we need to translate that into something usable for our shader. We need to subtract the input player position from our world position so we can get the individual area to affect.

Right click, and create a Position and Subtract node.

Connect the player position and world position node to the subtract node. At this point your graph should look similar to below.

Next we will need a Length node to translate our position into a distance.

At this point, if we connect the output of our length to our Base Color on our Fragment, we can see a strange divine light.

How can we control the actual effect size?

We need a multiply node and some additional input here to control the highlight amount.

Let’s create a new Multiply node, and a Float input.

Name the Float input something like _EffectStrength, and feed the length output into the new multiply node.

You should have something similar to this, and the shader will go black again. This is simply because we haven’t given it an effect strength yet.

Save this Shader Graph asset and assign it to an object in our scene if you haven’t already.

Notice the warning. This refers to the fact that we aren’t rendering a sprite. This is correct, and can be safely ignored.

Assuming a reference to the sprite renderer component, we can then use the material set property functions to pass along our game values in an Update function or whenever needed.

RevealBG.material.SetVector("_PlayerPosition", position);
RevealBG.material.SetFloat("_EffectStrength", highlightingPlayerAmount);

Set the effect to something visible like 1 for now. We can also set a default through the Shader Graph editor.

All of this grey is pretty boring, so let’s add some color. The ability to edit our colors through scripting is pretty important, so let’s create two new Color variables.

The shader will lerp between these two colors for the highlight effect. We could use only one color considering our goal of mixing the effect with transparency, but the additional color gives more control over the effect appearance.

Create a Lerp node. Connect the output of the previous multiply node to the lerp T input, and the two new colors to the A and B inputs, respectively.

I set BGColor to blue, and PlayerRevealColor to red through the graph inspector to clearly show the shader effect.

If all goes well, you should have a circular gradient in the input colors you’ve specified.

And something like this in your Shader Graph.

That gradient isn’t really the look we want. Instead, we want a tight circular highlight around the player position.

To achieve this, we can add a Step node.

Insert it between the multiply and lerp node at the end, and it will produce a gated circular output.

Adjusting the EffectStrength should make the circle appear larger. Try values from 0 -> 1. Above 1 will make the highlight smaller.

0.5 effect setting
EffectStrength at 0.5
EffectStrength at 0

Now we just need to connect our transparency logic.

Add another Multiply node that we will use for the Alpha property on the Fragment. The input should be our previous multiply node’s output, before the Step node. This allows control over the strength of the highlight fade. I went with 1.5.

You’re pretty much finished!


We can adjust the colors to do screen wave effects like this that could be enhanced with particle effects.

Or as a game over effect where you hide the rest of the stage and highlight the player. I added a purple background sprite behind the player to show the masking effect.

Force fields, lights for dark mazes etc all follow a similar concept.


Source


It helps me if you share this post

Published 2024-01-21 06:00:00

Pure JavaScript Asteroids Clone with Enemy Ships Source Code

There are many acceptable JavaScript game engines out nowadays, but often you can get good performance from writing your own simple engine or renderer depending on your use case. The code for this project will be on my GitHub linked below.

What goes into writing a game engine?

Ideally, we want to handle a few important things.

  1. States, whether that be states of objects (alive, dead, moving, the type of enemy)
  2. Rendering
  3. Spawnable objects (with previously mentioned states)
  4. Input
  5. Save data

We approach this task with an object-oriented mindset instead of a functional programming mindset. Although there are a few global variables such as the overall running game state or the object pool arrays, most of the memory or information we need to remember occurs on a per-object basis.

We will be using a ‘Canvas‘ to draw our simple asteroid graphics. Writing a 3d renderer in JS is a much more complex task, although libraries like threeJS exist to get you started.

To begin with, we want to define a Vector2D class that we can reuse throughout our game. I’m familiar with Unity so I imagine an implementation similar to their engine’s GameObject setup, but any class that can read / write an X and Y will work.

var Vec2D = (function() {
var create = function(x, y) {
        var obj = Object.create(def);
        obj.setXY(x, y);

        return obj;
    };

    var def = {
        _x: 1,
        _y: 0,

        getX: function() {
            return this._x;
        },

        setX: function(value) {
            this._x = value;
        },

        getY: function() {
            return this._y;
        },

        setY: function(value) {
            this._y = value;
        },

        setXY: function(x, y) {
            this._x = x;
            this._y = y;
        },

        getLength: function() {
            return Math.sqrt(this._x * this._x + this._y * this._y);
        },

        setLength: function(length) {
            var angle = this.getAngle();
            this._x = Math.cos(angle) * length;
            this._y = Math.sin(angle) * length;
        },

        getAngle: function() {
            return Math.atan2(this._y, this._x);
        },

        setAngle: function(angle) {
            var length = this.getLength();
            this._x = Math.cos(angle) * length;
            this._y = Math.sin(angle) * length;
        },

        add: function(vector) {
            this._x += vector.getX();
            this._y += vector.getY();
        },

        sub: function(vector) {
            this._x -= vector.getX();
            this._y -= vector.getY();
        },

        mul: function(value) {
            this._x *= value;
            this._y *= value;
        },

        div: function(value) {
            this._x /= value;
            this._y /= value;
        }
    };

    return {
        create: create
    };
}());       

This will allow us to reference positions easier. It’s vital to implement a few capabilities for our renderer. One important need is to be able to draw an object to our canvas at a specified position, and have the capability to clear said canvas, preparing for the next frame the game renders.

To draw a line, we can write JavaScript such as:

var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
ctx.moveTo(0, 0);
ctx.lineTo(200, 100);
ctx.stroke();

And if we wanted to clear our canvas, we can use clearRect:

ctx.clearRect(0, 0, canvas.width, canvas.height);

We can define a render function to handle our different objects.

window.getAnimationFrame =
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
function(callback) {
    window.setTimeout(callback, 16.6);
};
render(){
    context.clearRect(0,0,screenWidth,screenHeight);
    renderShips();
    renderAsteroids();
    renderBullets();
    getAnimationFrame(loop);
}

renderShips(){
    ship.renderSelf();
    for (int i = 0; i < enemies.length; i++)
    enemies.renderSelf();
}
...etc

Then an example render self function:

renderSelf: function() {
    if (this.hasDied)
        return;
    context.save();
    context.translate(this.pos.getX() >> 0, this.pos.getY() >> 0);
    context.rotate(this.angle);
    context.strokeStyle = playerColor;
    context.lineWidth = (Math.random() > 0.9) ? 4 : 2;
    context.beginPath();
    context.moveTo(10, 0);
    context.lineTo(-10, -10);
    context.lineTo(-10, 10);
    context.lineTo(10, 0);
    context.stroke();
    context.closePath();

    context.restore();
}

Which would render our object assuming a class holding some variables with our Vector2 class we described earlier.

var Ship = (function() {
var create = function(x, y, ref) {
    var obj = Object.create(def);
    obj.ref = ref;
    obj.angle = 0;
    obj.pos = Vec2D.create(x, y);
    obj.vel = Vec2D.create(0, 0);
    obj.thrust = Vec2D.create(0, 0);
    obj.invincible = false;
    obj.hasDied = false;
    obj.radius = 8;
    obj.idleDelay = 0;
    obj.isSpectating = false;

    return obj;
};
...etc

We are handling rendering and state management from inside an object now. All that just for a triangle.

player ship

We aren’t done yet. Next we need to handle Input. The goal with creating object classes is reusability and extensibility. We don’t need to spawn multiple instances of an input, so we can handle that globally. Your Input function may look something like this:

window.onkeydown = function(e) {
    switch (e.keyCode) {
        //key A or LEFT
        case 65:
        case 37:
            keyLeft = true;
            break;
            //key W or UP
        case 87:
        case 38:
            keyUp = true;
            break;
            //key D or RIGHT
        case 68:
        case 39:
            keyRight = true;
            break;
            //key S or DOWN
        case 83:
        case 40:
            keyDown = true;
            break;
            //key Space
        case 32:
        case 75:
            keySpace = true;
            break;
            //key Shift
        case 16:
            keyShift = true;
            break;
    }

    e.preventDefault();
};

window.onkeyup = function(e) {
    switch (e.keyCode) {
        //key A or LEFT
        case 65:
        case 37:
            keyLeft = false;
            break;
            //key W or UP
        case 87:
        case 38:
            keyUp = false;
            break;
            //key D or RIGHT
        case 68:
        case 39:
            keyRight = false;
            break;
            //key S or DOWN
        case 83:
        case 40:
            keyDown = false;
            break;
            //key Space
        case 75:
        case 32:
            keySpace = false;
            break;
            //key Shift
        case 16:
            keyShift = false;
            break;
    }

    e.preventDefault();
};

e.preventDefault() will stop users from accidentally hitting keys such as ctrl + L and losing focus from the window, or jumping the page with Space, for instance.

function updateShip() {
    ship.update();

    if (ship.hasDied) return;

    if (keySpace) ship.shoot();
    if (keyLeft && keyShift) ship.angle -= 0.1;
    else if (keyLeft) ship.angle -= 0.05;
    if (keyRight && keyShift) ship.angle += 0.1;
    else if (keyRight) ship.angle += 0.05;

    if (keyUp) {
        ship.thrust.setLength(0.1);
        ship.thrust.setAngle(ship.angle);
    } else {
        ship.vel.mul(0.94);
        ship.thrust.setLength(0);
    }

    if (ship.pos.getX() > screenWidth) ship.pos.setX(0);
    else if (ship.pos.getX() < 0) ship.pos.setX(screenWidth);

    if (ship.pos.getY() > screenHeight) ship.pos.setY(0);
    else if (ship.pos.getY() < 0) ship.pos.setY(screenHeight);
}

...etc

function checkDistanceCollision(obj1, obj2) {
    var vx = obj1.pos.getX() - obj2.pos.getX();
    var vy = obj1.pos.getY() - obj2.pos.getY();
    var vec = Vec2D.create(vx, vy);

    if (vec.getLength() < obj1.radius + obj2.radius) {
        return true;
    }

    return false;
}

...etc

Once we have the ability to render a reusable object to a canvas and read / write a position that can be checked, we use that as a template to create other objects (particles, asteroids, other ships).

hexagon asteroid
enemy ship example

You can make interesting graphics with just basic shapes. We handle collision by assigning either an xWidth and yWidth + xOffset and yOffset, OR a radius. This again would be assigned to the object itself to keep track of.

asteroids game example

Further Techniques

If we can control the rendering manually we can leave an ‘afterimage’ on our canvas before rendering the next frame as opposed to clearing it entirely. To do this, we can manipulate the canvas’ global alpha.

// Get the canvas element and its 2D rendering context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// Set the initial alpha value
let alpha = 0.1; // You can adjust this value to control the fading speed
// Function to create the afterimage effect
function createAfterimage() {
    // Set a semi-transparent color for the shapes
    ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
    // Fill a rectangle covering the entire canvas
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    // Decrease alpha for the next frame
    alpha *= 0.9; // You can adjust this multiplier for a different fade rate
    // Request animation frame to update
    requestAnimationFrame(createAfterimage);
}
// Call the function to start creating the afterimage effect
createAfterimage();

And a simple localStorage can be used to save scores.

function checkLocalScores() {
    if (localStorage.getItem("rocks") != null) {
        visualRocks = localStorage.getItem("rocks");
    }
    if (localStorage.getItem("deaths") != null) {
        visualDeaths = localStorage.getItem("deaths");
    }
    if (localStorage.getItem("enemyShips") != null) {
        visualEnemyShips = localStorage.getItem("enemyShips");
    }
    updateVisualStats();
}
function saveLocalScores() {
    localStorage.setItem("rocks", visualRocks);
    localStorage.setItem("deaths", visualDeaths);
    localStorage.setItem("enemyShips", visualEnemyShips);
}

End Result

You can see and play the game here.

Source code is here. ✨


It helps me if you share this post

Published 2023-11-30 23:51:07

Starbound 1.4.4 Source Code

Starbound has been one of my favorite games of all time, so I’m happy to say that I have the latest Starbound source code, last commit August 7th, 2019. I will not be explaining how I got these files. It is the actual source, not just a decompilation, and as such includes build scripts, unused stuff, old migration code, comments, a stored test player, etc.

Source Screenshots

The source has minimal comments, and the structure is reasonable. I found the code easy to read and understand, but perhaps that’s because I’ve been modding Starbound for years now and am familiar with its behavior.

Languages Breakdown (GitHub)

StarEnvironmentPainter.cpp

StarEnviroment.cpp preview

StarMixer.cpp (audio related)

StarMixer.cpp source preview

StarTools.cpp

StarTools.cpp source preview

Building

And of course, we can build it. I compiled this version without Steam API or the Discord rich presence API, but those are easily included.

Skip to 1:10 to see the game launch

Funny Developer Comments

Here’s a look at some of the best (in my opinion) developer comments in the source. This is not intended to be a mockery, far from it, I’m ecstatic I can take a peek into the minds of the developers. Enjoy.

// message is fullbody encrypted so the response is trust worthyish
// message is fullbody encrypted so the response is trust worthyish

// Meh, padding is hard-coded here
// Meh, padding is hard-coded here

// TODO: I hate these hardcoded values.  Please smite with fire.
// TODO: I hate these hardcoded values. Please smite with fire.

// TODO: Get rid of this stupid fucking bullshit, this is the ugliest
// fragilest pointlessest horseshit code in the codebase.  It wouldn't
// bother me so bad if it weren't so fucking easy to do right.
// TODO: Get rid of this stupid fucking bullshit, this is the ugliest
// fragilest pointlessest horseshit code in the codebase. It wouldn’t
// bother me so bad if it weren’t so fucking easy to do right.

// This was once simple and elegant and made sense but then I made it
// match the actual platform rendering more closely and now it's a big
// shitty pile of special cases again. RIP.
// This was once simple and elegant and made sense but then I made it
// match the actual platform rendering more closely and now it’s a big
// shitty pile of special cases again. RIP.

Example: Simple Re-implementation of Vapor Trail and Sitting Toolbar Usage

At some point during development, Chucklefish had the idea to add a vapor trail when the player was falling fast. I could’ve sworn I saw a post on their news about it back when the game was in beta, but I can’t find it now. Anyway, we can add a small snippet to restore it, as an example of further engine work Starbound can benefit from.

// Vapor trail
if (m_movementController->velocity()[1] < -50) {
  m_vaporTrailTimer += WorldTimestep;
  if (m_vaporTrailTimer > 1)
      m_humanoid->setVaporTrail(true);
  }else{
  m_vaporTrailTimer = 0;
  m_humanoid->setVaporTrail(false);
}

By adding this snippet, we can see what it was roughly meant to look like.


We can also modify Player restrictions such as

bool Player::canUseTool() const {
  return !isDead() && !isTeleporting() && !m_techController->toolUsageSuppressed() && m_state != State::Lounge;
}

to just

return !isDead() && !isTeleporting() && !m_techController->toolUsageSuppressed();

Allowing us to use our inventory while sitting down

Further Thoughts

Future work on the engine can lead to further modding capabilities and engine optimizations. There are many potential client side performance improvements that could be made without touching any network code. This would maintain compatibility with the vanilla client. The netcode could be updated as well, but this would break compatibility once major changes were made. If both (or more) parties are willing to use a modified client, any theoretical modification could be made. The possibilities are endless.

As of 2024, there now exists a few Starbound open source community projects with the aim of enhancing the base game’s experience. : )


It helps me if you share this post

Published 2023-05-27 04:55:45

Applying custom Windows styles to Firefox, Chrome, and other Chromium browser’s window buttons in Windows 10 & 11

Typically, browser vendors force default button styles onto the program. This can be troublesome when you use something like SecureUxTheme to change your Windows styles, and you care about the cohesiveness. There is a hacky solution, even if the Firefox forums told me there wasn’t. 😉

DEFAULT: Custom themed Notepad next to standard browsers

For Google Chrome, Edge, Brave, and some other Chromium-based browsers

For Chromium based browsers you can simply change the shortcut target to allow launching with your custom changes.

  1. Open start menu.
  2. Search and find your browser shortcut
  3. Right-click, and open file location
  4. Right click > open Properties of the browser shortcut (The shortcut for Chrome in the Start Menu may be found in C:\ProgramData\Microsoft\Windows\Start Menu\Programs)
  5. Add this line --disable-windows10-custom-titlebar to the end of the Target field after a space. (For Chrome, “C:\Program Files\Google\Chrome\Application\chrome.exe” becomes “C:\Program Files\Google\Chrome\Application\chrome.exe” –disable-windows10-custom-titlebar )
  6. In order for your changes to show up you may need to use Task Manager (ctrl + shift + esc) and kill all background processes of that browser
  7. Repeat for each browser shortcut you use.

Changing the Registry Launch settings

This is all well and good but what if you click on a link and the browser opens automatically? Now we aren’t using the custom launch option anymore. We can edit the registry to fix this.

This is the “I’m not responsible if you break your computer” warning: BE SURE TO ALWAYS MAKE A BACKUP OF THE REGISTRY BEFORE PERFORMING ANY CHANGES.

  1. Launch the Registry Editor (Win + R, regedit)
  2. Navigate to Computer\HKEY_CLASSES_ROOT\ChromeHTML\shell\open\command
  3. Change the (Default) value from "C:\Program Files\Google\Chrome\Application\chrome.exe" --single-argument %1 to "C:\Program Files\Google\Chrome\Application\chrome.exe" --disable-windows10-custom-titlebar --single-argument %1
    • We are essentially just adding that custom launch argument onto the default launch arguments Windows calls when it opens the program
  4. Starting the browser now use your themed settings. Older versions Chromium may have a different startup option. Try --disable-features=Windows10CustomTitlebar if it doesn’t work for you

Firefox

Firefox needs CSS and changing an internal flag in order to work.

  1. Open your Firefox profile. You can find it in %appdata%\Mozilla\Firefox\Profiles, try looking at the one most recently modified.
  2. If the folder doesn’t exist already, create a folder called ‘chrome‘. Yes, this is the tutorial for Firefox.
  3. Inside the ‘chrome‘ folder in your profile, create or edit the file ‘userChrome.css‘.
  4. Put these contents (or embedded down below) into the css file, either on its own or adding to what is already there. You can modify anything you want, such as the ‘titlebar-button:hover’ alpha value to your liking.
  5. Open a new Firefox window
  6. Enter about:config into the URL bar, and bypass the warning
  7. Change the toolkit. legacyUserProfileCustomizations. stylesheets option to true by double clicking.
  8. Restart Firefox
@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
.titlebar-button {
background-color: transparent !important;
transition: background-color 0.5s ease;
height: 0px;
}
.titlebar-button>.toolbarbutton-icon {
list-style-image: none;
}
.titlebar-button:hover {
background-color: rgba(121, 121, 121, 0.1) !important;
}
#titlebar-close:hover {
background-color: rgba(121, 121, 121, 0.1) !important;
}
#titlebar-close:hover>.toolbarbutton-icon {
list-style-image: url("chrome://browser/skin/caption-buttons.svg#close-white") !important;
}
#main-window {
-moz-appearance: -moz-win-glass !important;
background: transparent !important;
}
#navigator-toolbox {
background: transparent !important
}
view raw userChrome.css hosted with ❤ by GitHub

And finally, get my better dark theme for Firefox. 🙂


Now your browser buttons look sleek and uniform, just like the rest of your system.


It helps me if you share this post

Published 2022-07-31 09:03:00