Gameplay Gameplan (From a Gamefan during a self-made gamejam)

So I was looking back at the last thing that I made for planning



and I already see the inherent problem is that I never made a “Game” sequence in addition to the boss building. In that vein what I was thinking is:

  1. OnStart
    • Spawn Player (give only the pistol)
    • Spawn “Protection” bots (i.e. a number of bots around each assembler)
  2. Every X Seconds
    • Spawn a bot (after checking if we have too many bots) and set the patrol location to the last known player location
  3. On Destroying An Assembler
    • Give the player a new weapon
    • Increase the difficulty of the other assemblers
    • Alert the Other Patrol Bots to your location from all assemblers (minus the patrol bots)

This seems simple enough but there’s a few mechanics I do not have written yet.

  1. Bot Patrol locations/target locations
  2. Player Pickups
    • Both Health and Weapons (Weapons can be magically granted I figure)
  3. Patrol bot synchronization for alerts etc.

Bot Patrol Locations

This was pretty easy, I just updated the EQS query to be set around a specific location rather than the location of the bot. This changes the scores so the bots wander around a fixed point and don’t leave its radius until its engaged.

I also had to update the Blackboard to hold this value, so I can swap it on the fly

Now to update the guard point for a non-deployed bot I just set the “IsGuarding” flag to true and the “GuardPoint” to wherever.

Player Pickups

I think I want the health system to be a set of stims rather than recovering health or anything else. So I started modeling up a health station in blender

and syrettes/injectors/needles/syringes are kinda easy (I did this in 5ish minutes, still need a touch up).

My hope is to play an animation before giving health, essentially I want the player to show the syrette then pull below the frame and inject. This should save me animation work (hopefully).

Game Mode Work

I got the initial game mode made and the spawn but I also made a data table which holds difficulty settings

The idea is that each assembler will have a number of randomly generated guard bots which will patrol around the assembler and a set list of “wandering” bots that will be assembled and run towards the last place the player has been seen.

Right now I have the gamemode start working I just need to get the player alerts working. Which In my head I think I want each assembler to control their fleet of bots so the chain will be:
A bot sees the player and alerts their assembler with a location > The Assembler Then alerts the game mode > The game mode gets the new position and sends it to the other assemblers.

This way I can also setup alert radius’s, a limit of reporting etc.

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

“Building in progress”

(Ignore the messed up ragdoll)

Sidebar:
If you attempt to pull a widget ui on construction rather than beginplay you’ll get a pointer that is valid but isn’t aligned with the actual game pointer. My guess is that unreal setups UI placeholders in memory before begin play (Probably why they mark most UI elements as “DONT USE THIS FOR GAMEPLAY”)

Posted for the cool, cool header image