
I’m digging this kinda vibe for the boss thing. Probably going to work on the game part a bit more Friday.
I’m digging this kinda vibe for the boss thing. Probably going to work on the game part a bit more Friday.
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
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:
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.
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.
<?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.
I might go with some kind of oppressive robotic face instead? Like the Mussolini face from WWII.
I wanted to mesh paint (https://dev.epicgames.com/documentation/en-us/unreal-engine/getting-started-with-mesh-texture-color-painting-in-unreal-engine) a bit in unreal and thought “Oh I’ll just flip this virutal texture box”
I have a pretty good pc (4090 24gb) I’m surprised its taking so 5+ minutes (but then again I’ve made NO effort to optimize any of my materials…
That being said…
I guess I made things low poly enough that maybe this is inherently optimized….
But thats a detail thing that I can work on later, For the outside I want to:
Also I need to make a better road texture, at far distances it looks fine but then you zoom in…
Every-time I look at it my graphics card fan spins up…probably a bad thing.
I also added a tunnel model
The UV’s and normals are REALLY messed up atm but I dont want to dig in yet. I fixed it by making the underlying material double sided but generally if you’re a single sided material and your model goes from looking like this in blender:
To this…
You should probably do it again….
But in the meantime there’s the magic “Make the material two sided” button which is normally used for meshes that have both an interior and exterior (which is not this….at all)
Still has issues. The darkness underneath comes from the unreal seeing that the normal of the face is facing in a direction that is into the block’s origin rather than away from it. This makes the lighting calculations get all wonky.
I’ve been disorganized when working this (also I stopped working it for like 2 weeks and now I’m lost) So I made a trello board here:
https://trello.com/b/dmIooAod/blacklaceworking-board
Broken into:
“Longer term goals”- Things I probably wont look into until I have something on steam
“MVP Make/Design”- Things I want to add to the game before I submit a demo/game to steamworks
“Fix/Improve” – Things that are in the game but need to be re-worked.
I’ll start chipping away at these and my personal goal is to get something ready to throw on steam by June.
I started making a car
I was trying to base it off of https://www.classic.com/m/bmw/3-series/e36/m3/ and I think the vibe is kinda there? I think the wheel wells are kinda in the right place:
My goal is to have a driving sequence at some point. But honestly this might just be a thing I hold off on attempting until I finish up the whole “game” part of the game…Plus the doors seem like they would be hard to get right.
Churning through this first semi-finalized level (which will probably be the demo level). The idea here is that the warehouse will open up to an underground section.
I re-wrote a bunch of the patrol bot AI to use EQS (Environmental query system: see https://dev.epicgames.com/documentation/en-us/unreal-engine/environment-query-system-quick-start-in-unreal-engine#2-environmentquerycontext ) , which should greatly simplify the the bots and give them better criteria to find spots that see the player, but are also kinda close.
Here’s the EQS Tree:
Essentially EQS is a way to break up the navigation grid into nodes, each has a score and the EQS tree determines the score of each node. (Lower the better). In this case I have two tests for each node: Can the node see the current location of the player? Am I close to the player? Here’s another video of things kinda working but not fully:
I also fixed up the rocket so now it wont fly through walls
Here’s some quick gameplay of what I have atm:
It’s very silent right now…Also the stabby bots are quick as hell and I want some kinda warning for that. The gameplay right now is very serious sam and I want to tone it back speed wise. I think I might just slow down all the characters and add it some head bob.
Longer gameplay (with sound) below.
The map I was playing on isn’t great but I figure it might be better than that dev arena that I had before. I’m in the process of re-making the warehouse as a big static mesh so it’s easier on lower performance machines (right now it’s like 200+ meshes).
My 4090 fan is starting to spin up more often so I gotta feeling I might be hitting the limit of my “optimize later” strategy.
I made a real spawn location for the patrol bots:
Isn’t too much to it, just a bunch of boxes and a conveyor that spits out the bots.
I reworked some of the deployment logic so that bots are physics objects before they get fully deployed. That way I can do stuff like drop them off cliffs after being built.
The deployment logic now hardcodes the mesh’s position and rotation before playing the deploy animation (which is why you see them jump up in the second video). A way I can fix/clean this up is by adding a rollover animation which will play if the mesh is disoriented.
Right now the assemblers are attached to timers, when I get this hooked up to the survival game mode I’ll add control back to the game mode to dump out bots as needed.
I also added this guy in game
Doesn’t look great but I want to have one of these at the side of each assembler and allow some kind of “override” command or mini-game to stop the flow of bots or to flip them to your side.