Zap

Put the turret in game and nearly went deaf trying to make a good laser sound.

I was trying to use electric with operator to make a good “wirrr ZAP” sound. But I ended up just making garbage that made my head hurt (Lower your speakers before playing)

I also made a scorch mark decal in gimp:

I was doing all this work to get good edging effects but the “smoke” brush seemed to work fine!

I setup the explosion from the weapon library to have scorch marks on the ground:

My initial reasoning behind making this is that I wanted the tower I made earlier to make a line as it tracked you across the ground but I pivoted to make the tower just shoot a single explosion.

The tower now detects and tracks the player. I just setup a big collision trigger around the tower that grabs the player as a variable on overlap. Then I added in some laggy tracking code below.

I also added a plasma laser thing Niagara effect:

Surprisingly there wasn’t a torus primitive model in unreal so I had to whip one up really quick in blender (its like 3 buttons to make this in blender).

I went down a bunch of silly paths trying to make the rings conform to the beam including trying to custom HLSL (see: https://en.wikipedia.org/wiki/High-Level_Shader_Language) via a Niagara module (https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-custom-modules-in-niagara-effects-for-unreal-engine ):

This SHOULD HAVE worked not sure why but I gave up and used a “shape location” module which distribute the meshes along a cylinder of radius 1

Worked the same…

And the final result:

(Still working on the rotation logic which is why it randomly flips 360) No damage atm but I still dig the tower and the effect of a bright flash then an explosion. Kinda reminds me of Slave-1’s seismic charges:

Now my next plan is to:
1.) Hook these turrets up to terminals
2.) Add health to the turrets
3.) Add a “you win” screen
4.) Make this map:

5.) Make Menus
6.) Make demo
7.) Put on steam

Frightening Lamp Post

I got stuck on making the “boss” spawner thing so I decided to think more about what’s defending this boss area rather than the spawner itself. So I came up with this:

Which is just kinda a stick with a gun ontop….that being said I kinda dig it. I’m going to put some more time into this model and probably use a placeholder cube to get a simple level setup using the new game mode.

Honestly this might be one of the cleanest looking things I’ve ever made.

The thing I did this time around when modeling was to break apart models when I didn’t like the way the smoothing looked, not sure how that will translate to UE5 but I’m happy with the flat shading of this guy.

From a gameplay perspective I think I’ll have this thing shoot plasma or something that way I don’t have to have a loader mechanism for rockets (which I want to say is a pet peeve of mine but this). Which is why there’s little wires above the two tubes:

And the rear side looks like some kinda battery pack.

Also bots man:

Meh/Unreleased game

I have so much trouble trying to model the stock for a side by side. I had an old game concept that I had a single barrel which looks pretty sketch.

It kinda looked right but if you zoom in you see some issues:

The original game I was calling “shotty” and the idea was that you would have trick shells like hawkeye, here’s the gameplay of where I left the game:

I had a bunch of shells I made in substance painter which I thought were neat:

I think the animations were where I put in the most work:

But I never really had a good “game” for it, it was all mechanics. I might revisit this in the future but for now I want to focus on blacklace.

“Stay a while and Listen”

I added some visitor analytics to wordpress via a custom plugin

As a non-web programmer this was surprisingly easy…Only because I have AI.

Claude AI was able to throw everything into a script and threw it into a zip file and installed it into wordpress. A apart of me feels like I cheated but at the same time I don’t want to spend hours learning the wordpress database system, plugin api, and chart js just to see a few numbers and a line plot.

I’ve attached the code below that you can package into a .zip to upload to any wordpress site below.

Expand for plugin php code.
<?php
/**
 * Plugin Name: Visitor Counter Dashboard
 * Description: Displays visitor statistics in a graph on the admin dashboard
 * Version: 1.3.0
 * Author: Claude Ai (via Will Kolb)
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

class VisitorAnalytics {
    
    private $table_name;
    
    public function __construct() {
        global $wpdb;
        $this->table_name = $wpdb->prefix . 'visitor_analytics';
        
        // Hook into WordPress
        add_action('init', array($this, 'track_visitor'));
        add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
        add_action('wp_ajax_get_visitor_data', array($this, 'ajax_get_visitor_data'));
        
        // Activation hook
        register_activation_hook(__FILE__, array($this, 'create_table'));
    }
    
    /**
     * Create database table on plugin activation
     */
    public function create_table() {
        global $wpdb;
        
        $charset_collate = $wpdb->get_charset_collate();
        
        $sql = "CREATE TABLE {$this->table_name} (
            id mediumint(9) NOT NULL AUTO_INCREMENT,
            visit_date date NOT NULL,
            visit_count int(11) NOT NULL DEFAULT 1,
            ip_address varchar(45) NOT NULL,
            user_agent text,
            page_url varchar(255),
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY unique_daily_ip (visit_date, ip_address)
        ) $charset_collate;";
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }
    
    /**
     * Track visitor on each page load
     */
    public function track_visitor() {
        // Don't track admin users or admin pages
        if (is_admin() || current_user_can('manage_options')) {
            return;
        }
        
        global $wpdb;
        
        $ip_address = $this->get_user_ip();
        $today = current_time('Y-m-d');
        $user_agent = sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? '');
        $page_url = sanitize_text_field($_SERVER['REQUEST_URI'] ?? '');
        
        // Check if this IP has already been recorded today
        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT id FROM {$this->table_name} WHERE visit_date = %s AND ip_address = %s",
            $today, $ip_address
        ));
        
        if (!$existing) {
            // Insert new visitor record
            $wpdb->insert(
                $this->table_name,
                array(
                    'visit_date' => $today,
                    'ip_address' => $ip_address,
                    'user_agent' => $user_agent,
                    'page_url' => $page_url,
                    'visit_count' => 1
                ),
                array('%s', '%s', '%s', '%s', '%d')
            );
        }
    }
    
    /**
     * Get user's IP address
     */
    private function get_user_ip() {
        $ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
        
        foreach ($ip_keys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                $ip = sanitize_text_field($_SERVER[$key]);
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $ip;
                }
            }
        }
        
        return sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1');
    }
    
    /**
     * Add dashboard widget
     */
    public function add_dashboard_widget() {
        wp_add_dashboard_widget(
            'visitor_analytics_widget',
            'Visitor Analytics',
            array($this, 'display_dashboard_widget')
        );
    }
    
    /**
     * Display the dashboard widget content
     */
    public function display_dashboard_widget() {
        ?>
        <div id="visitor-analytics-container">
            <div style="margin-bottom: 15px;">
                <select id="analytics-period" style="margin-right: 10px;">
                    <option value="7">Last 7 days</option>
                    <option value="30">Last 30 days</option>
                    <option value="90">Last 90 days</option>
                </select>
                <button id="refresh-analytics" class="button button-secondary">Refresh</button>
            </div>
            
            <div id="analytics-summary" style="display: flex; gap: 20px; margin-bottom: 20px;">
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">Today</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #0073aa;" id="today-visitors">-</p>
                </div>
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">This Week</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #00a32a;" id="week-visitors">-</p>
                </div>
                <div style="text-align: center;">
                    <h3 style="margin: 0; color: #23282d;">This Month</h3>
                    <p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #d63638;" id="month-visitors">-</p>
                </div>
            </div>
            
            <canvas id="visitor-chart" width="400" height="200" style="max-height: 200px;"></canvas>
            <div id="analytics-loading" style="text-align: center; padding: 20px;">Loading...</div>
        </div>
        
        <script>
        jQuery(document).ready(function($) {
            let chart = null;
            
            // Cleanup function to properly destroy chart
            function destroyChart() {
                if (chart && typeof chart.destroy === 'function') {
                    chart.destroy();
                    chart = null;
                }
            }
            
            // Clean up when leaving the page
            $(window).on('beforeunload', destroyChart);
            
            function loadAnalytics() {
                const period = $('#analytics-period').val();
                $('#analytics-loading').show();
                
                $.ajax({
                    url: ajaxurl,
                    type: 'POST',
                    data: {
                        action: 'get_visitor_data',
                        period: period,
                        nonce: '<?php echo wp_create_nonce('visitor_analytics_nonce'); ?>'
                    },
                    success: function(response) {
                        if (response.success) {
                            updateSummary(response.data.summary);
                            updateChart(response.data.chart_data);
                        }
                        $('#analytics-loading').hide();
                    },
                    error: function() {
                        $('#analytics-loading').hide();
                        alert('Error loading analytics data');
                    }
                });
            }
            
            function updateSummary(summary) {
                $('#today-visitors').text(summary.today || 0);
                $('#week-visitors').text(summary.week || 0);
                $('#month-visitors').text(summary.month || 0);
            }
            
            function updateChart(chartData) {
                const canvas = document.getElementById('visitor-chart');
                const ctx = canvas.getContext('2d');
                
                // Properly destroy existing chart
                if (chart) {
                    chart.destroy();
                    chart = null;
                }
                
                // Clear the canvas
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // Reset canvas size
                canvas.style.width = '100%';
                canvas.style.height = '200px';
                
                chart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: chartData.labels,
                        datasets: [{
                            label: 'Daily Visitors',
                            data: chartData.data,
                            borderColor: '#0073aa',
                            backgroundColor: 'rgba(0, 115, 170, 0.1)',
                            borderWidth: 2,
                            fill: true,
                            tension: 0.4
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        interaction: {
                            intersect: false,
                            mode: 'index'
                        },
                        scales: {
                            y: {
                                beginAtZero: true,
                                ticks: {
                                    stepSize: 1,
                                    precision: 0
                                }
                            },
                            x: {
                                grid: {
                                    display: false
                                }
                            }
                        },
                        plugins: {
                            legend: {
                                display: false
                            },
                            tooltip: {
                                backgroundColor: 'rgba(0, 0, 0, 0.8)',
                                titleColor: '#fff',
                                bodyColor: '#fff',
                                cornerRadius: 4
                            }
                        },
                        elements: {
                            point: {
                                radius: 3,
                                hoverRadius: 6
                            }
                        }
                    }
                });
            }
            
            // Event listeners
            $('#analytics-period').change(loadAnalytics);
            $('#refresh-analytics').click(loadAnalytics);
            
            // Initial load
            loadAnalytics();
        });
        </script>
        <?php
    }
    
    /**
     * Enqueue admin scripts
     */
    public function enqueue_admin_scripts($hook) {
        if ($hook === 'index.php') {
            wp_enqueue_script('chart-js', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js', array(), '3.9.1', true);
        }
    }
    
    /**
     * AJAX handler for getting visitor data
     */
    public function ajax_get_visitor_data() {
        // Verify nonce
        if (!wp_verify_nonce($_POST['nonce'], 'visitor_analytics_nonce')) {
            wp_die('Security check failed');
        }
        
        // Check user permissions
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions');
        }
        
        global $wpdb;
        $period = intval($_POST['period']);
        
        // Get summary data
        $today = current_time('Y-m-d');
        $week_ago = date('Y-m-d', strtotime('-7 days', current_time('timestamp')));
        $month_ago = date('Y-m-d', strtotime('-30 days', current_time('timestamp')));
        
        $today_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date = %s", $today
        ));
        
        $week_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $week_ago
        ));
        
        $month_count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $month_ago
        ));
        
        // Get chart data
        $start_date = date('Y-m-d', strtotime("-{$period} days", current_time('timestamp')));
        
        $chart_data = $wpdb->get_results($wpdb->prepare(
            "SELECT visit_date, COUNT(*) as visitor_count 
             FROM {$this->table_name} 
             WHERE visit_date >= %s 
             GROUP BY visit_date 
             ORDER BY visit_date ASC",
            $start_date
        ));
        
        // Fill in missing dates with zero counts
        $labels = array();
        $data = array();
        
        for ($i = $period - 1; $i >= 0; $i--) {
            $date = date('Y-m-d', strtotime("-{$i} days", current_time('timestamp')));
            $labels[] = date('M j', strtotime($date));
            
            $count = 0;
            foreach ($chart_data as $row) {
                if ($row->visit_date === $date) {
                    $count = intval($row->visitor_count);
                    break;
                }
            }
            $data[] = $count;
        }
        
        wp_send_json_success(array(
            'summary' => array(
                'today' => intval($today_count),
                'week' => intval($week_count),
                'month' => intval($month_count)
            ),
            'chart_data' => array(
                'labels' => $labels,
                'data' => $data
            )
        ));
    }
}

// Initialize the plugin
new VisitorAnalytics();

In gamedev news I want to make a “boss” area thing that the player has to destroy

I think I want to incorporate that helix thingy as the “power core” (see: this post).

I’ve made a few iterations but they keep looking like the Red power ranger’s vape pen:

My hope was to make an equivalent to the Doom 2016 gore nest or the half life 2 energy pylon things, so that the player would essentially break the core causing some gameplay rush event where you gotta kill a bunch of bots and run.

From https://half-life.fandom.com/wiki/Combine_Power_Generator

I might go with some kind of oppressive robotic face instead? Like the Mussolini face from WWII.

from https://en.wikipedia.org/wiki/Palazzo_Braschi

“It’s not blurry its a design choice…”

This is the UI for the terminal. Next step is to hook up up to the game, I’m thinking the main difficulty there is that I’ll have to rework the assemblers to be controlled by a terminal rather than the game mode. In addition adding callbacks to throw what is build built back to the terminal, finally I’ll need to add a teamID to the player (which is a simple If statement so that shouldn’t be terrible). Also I kinda hate that the edge around the trackball is the same color as everything else…but game before polish needs to be my mantra for the next few weeks.

Terminal Time

A while back I made a terminal model that looked meh but was designed to serve as a stand in for some kind of computer interaction:

I started out wanted to make a command line interface, but then I changed my mind and wanted a Doom 3 style interface:

Example was pulled from here: https://www.reddit.com/r/Unity3D/comments/4nkvj9/ingame_gui_like_doom_3/

Setting up the actors

To accomplish this I found that unreal has a whole system for defining this procedure called a “widget interaction component” (see https://dev.epicgames.com/documentation/en-us/unreal-engine/umg-widget-interaction-components-in-unreal-engine) which is great! However if you’re not only trying to push buttons its a real pain to get everything setup to map properly.

The component works by doing a ray trace every frame so many meters in front of you, if that trace hits a widget component (the container class for UI components, see: https://dev.epicgames.com/documentation/en-us/unreal-engine/building-your-ui-in-unreal-engine) then the widget component will receive events as if the widget component was the 2d UI that is used for basic UI development. In other-words: Unreal makes a virtual screen, then I add the “virtual screen user” component to the player character and then it should be the same UI development process I would use for menus, huds etc.

the “virtual screen user class”

To show the UI in game I made a quick actor that held my terminal and my UI widget

My initial UI widget is just a white box cursor and a translucent green background

So ideally I would just use my Widget Interaction UI and do 100% of the development on the widget side. HOWEVER, I found out there isn’t a good “get What the Widget thinks the cursor position is” within the widget (The widget that holds the UI design also has its own execution path if you didn’t know that). But the issue came from that the only way to get the cursor position was the ACTUAL cursor position, which wont work here because it will always be centered on the screen during gameplay.

Sooooo, to fix this I had to make a “Set cursor position” public function that can update the location of the white square:

Then I had to add yet ANOTHER tick function to my main character to handle terminals:

So here’s the order of events:

1.) The widget interaction component detects its hitting a widget in the world here:

I then grab that widget and hold it in memory

2.) The tick function executes on the next tick

Which sets the position of the white square.

The result:

Seems decent enough, I just gotta mess with the sizing a bit more and disable debug to start really working at it.

Making a simple UI

To make a quick cursor (if you ever need this information)

1.) Select all

2.) Scale down to 1/4 of the screen

3) Fill black

4.) Rotate your selection 45 degrees

5.) Remove the corner

6.) undo that because that would never work

7.) Invert your selection and fill white

8.) Undo that because that makes no sense

9.) Select all

10.) Shrink by 50

11.) Rotate your selection 45

12.) Undo that and select all again

13.) Shrink by 25

14.) Rotate by 45

15.) Erase everything in your selection

16.) Select all

17.) Shrink by 50

18.) Scale horizontal by 50, increase vertical to 125

19.) Rotate by 45

20.) Undo and rotate by -45

21.) Be sad that you didn’t scale large enough on step 18

22.) Undo the last 4 steps and increase vertical size to 150

23.) Do steps 19-22 again and resize to 175

24.) Select all black, and realize you have weird artifacts from rotating

25.) Mess with the levels to remove all of the artifacts

25.) Make a boarder around the black selection and fill white

26.) Invert the colors because you realize most cursors are white not black

27.) Gaussian blur to make sure no-one can see your horrible brush strokes

28.) profit?

Now with that squared I can imported everything into unreal and made a quick ui with a few buttons:

Then I threw in some logic to make the weapon lower when you’re looking at a terminal, and that the “fire”event will left click the simulated UI:


The final result:

Still not sure why the cursor is so blurry when moving around a bunch, probably because of some of the motion blur I have on. But generally the goal is to have one of these on each assembler and I should be able to use it as a capture point kinda process to gain control over the assembler to create minions to go capture more assemblers etc. Which I think is now my basic “game loop”


So now I think I know what the game is, I’m thinking one map, 4-5 assemblers, one “Hub” boss area, I’ll probably package in my survival game mode because it’s kinda already done. Essentially this will be a single player MOBA against an over-world AI

So that means I need:

1.) An overview map UI element

2.) Region based spawning for the assemblers

3.) A “Boss hub” whatever that means

4.) Some kind of stationary turrets like which will essentially be a moba

5.) Some kind of countdown that adds an element of urgency to the player

So I added 5 tasks while removing one…not a great ratio (see https://trello.com/b/dmIooAod/blacklaceworking-board ) but knowledge is progress.

Oil and Strings take memory

Also for some reason my output log is taking up 8 gigs of memory.

I admit I had a divide by zero error but why did it fill up the output log to the point of nearly crashing my computer…

Other news, I reworked the bot deaths so they spray oil everywhere:

This is a oil material I made in gimp, then randomly subdivide and throw on walls. The oil material I made from an awesome blood splatter tutorial here: https://www.gimpusers.com/tutorials/blood-splatter-texture

Which long story short, it’s just the “sparks” brush grey-scaled. But still….

I also did another pass on the AI to 1.) stop it from following you while it was reloading and 2.) To keep it moving during a fight.

#2 was done by adding a list of positions where the bot received damage last

Then I made an eqs query context to basically make the bot shy away from where it was shot before.

To fix #1 it was just a weird quirk of ai services objects. The way I’m looking at the player is via a service (Called PatrolBot2FaceTargetService), which runs every 0.5 seconds that sets a “focus” variable for the ai controller. This essentially just turns the controller to the object it wants to “focus” on.

The issue was that that green guy was running after it stopped going down the “findFiringPosition” leg. So you never really cleared out the focus on the character. So a quick fix is when the bot reloads it drops all focus

Here’s the results of the two:

Honestly I think its already helping out with making fights a bit more dynamic.

I also added bot teams so I can start doing stuff like this:

I want to somehow get the eqs setup so it is a bit more random, you can see both of the top two bots moving upwards to find the next “good” position, which is scored more from where they’re at. I think I’ll need to do some kind of radius check? Not sure yet.

There still other issues to sort out also…

HUD Rework and Physics noodlin’

The old hud was starting to bother me so I leaned more into a simplier hud that I can expand upon later.

The original hud I was going for a kind of skewed look as if you were wearing a helmet and the information was displayed on these green dots on the screen. While a cool concept I think it was too janky and looked like mspaint levels of cleanliness. Also you can see here if I have a non-standar resolution the alignment would always be misaligned to the screen. The new hud (which I would say is still a developer hud) I went for a cleaner approach:

Nothing too crazy just a health bar and some text on the corners. But the best part:

The positioning stays consistent regardless of dpi/resolution.

I achieved this by avoiding the “scaleBoxes” I was using before and just used horizontal/vertical boxes with spacers in between:

I also adjusted my dpi scaling rules:

I also found out I had a bad strand in my blueprint spaghetti from months ago:

That circled red block is what determined the direction of damage when a bullet was fired. I was doing StartVector – EndVector instead of doing EndVector – StartVector which was inverting the direction of damage as the “direction Of Damage” was relative to the attacked instead of the attacker.

Why does this matter? Unreal has a damage type system which makes it easier to set impulses from different types of damage. I have a bullet type that I wanted to push objects, when the vectors are swapped:

When the math is correct:

(I realize the window is awkwardly small in these videos…)

I also wanted to touch up bot “death” (destruction? Disassembly?) and I saw I never made a physics asset for the Assault rifle with no stock, after that fix plus the impulse fix things start to look much nicer.

The legs spawning detached is still a bit annoying. That issues comes from the physics asset which I spent so much time with a month or two ago I eventually gave up and kept it a “this just works so don’t touch it” kinda stasis.

If you look at the tree to the left you can see there’s a “Body” bone which holds the main collision box then there’s “BackLegXX” bones which I have attached to the body bone and to the bottom legs. However, there’s nothing in between for the thighs of the bot, which is why in the video above you see the legs swinging free. I’d need to remake this guy by going through each leg and adding a box, connecting it to the body or previous leg and ensuring everything doesn’t collide with itself. There’s an “auto-generation” feature which I’ve had little success with so this will end up just being a big time sink.

Eventually I want bots to have a chance to detonate in a electrical fireshow which I think would add to the dynamic nature of fights. Also I need to do a re-write of the AI to add:
– AI Teams
– Team Positioning Coordination (so the bots don’t just all choose the same spot)
– Goal Zones (for some other ideas I’m working to add a meta level to gameplay)

Flute it up

I went into this then I found this bass sound and just kinda rolled with it I found a sound a liked and rolled with it (it a bit loud fyi)

Here’s the synths (where the concert flute is a sampler)

The flute is surprisingly nice is from the Abelton orchestral woodwinds pack: https://www.ableton.com/en/packs/orchestral-woodwinds/. That being said: a part of me thinks I shouldn’t have used the flutes, it kinda takes a dance thing and un-dances it.

I started working on the HUD also but I’m failing at keeping the scaling consistent:

I did put shells on the Assault Rifle though:

Not really a huge accomplishment but baby steps I guess?