- Published on
Tarkov-like Gun Building PoC in UE5
Introduction
I play a lot of games... one of those games is tarkov, which I sunk over 400 hours into the 0.12.9 patch...
One of my favourite systems in Tarkov was the gun building (even though meta guns were just primarily used and no-one built cool guns anymore), it is so extensive and allows building a gun from all of their real life components, with the ability to swap out most meaningful parts for different stats/benefits. This in tandem with Tarkovs Flea Market system creates a really interesting experience where you're always building guns around cost/benefit decision making... at least until the end game, with the meta Colt M4A1 (in patch 0.12.9) costing around ₽400,000 in game currency, which is a fair amount for single gun! Enough about the game, let's get into my plan!
What I'm trying to replicate
This gif showcases some of the versatility of the Tarkov gun building system, this is supported by having over 70 guns and what seems like over a thousand attachments.
Here's a stock Colt M4A1 in Tarkov.
and here's what a modded one looks like! Crazy to think it's the same gun... or is it a Ship of Theseus scenario...
How are we gonna do it?
I focused mainly on the capability of attaching all the components as a first proof of concept, currently whatever you select on the ground is attempted to add to the gun if it can't be added, it will be ignored.
It does this through a mechanism similar to this psuedo-code
class Gun {
GunPart root;
void Attach(GunPart part) {
auto can, p = canAttach(attach_name);
if (can) {
part.attachToComponent(p);
}
}
(bool, *GunPart) canAttach(String attach_name) {
root.canAttach(attach_name);
}
}
class GunPart {
SkeletalMesh root;
map<string, GunPart> attached;
bool canAttach(String attach_name) {
if (attached.key(attach_name) && !attached[attach_name]) {
return true;
} else {
// Walk the attachment tree and check if they can support this attachment
for (attach : attached) {
if (attach.canAttach(attach_name)) {
return true
}
}
}
return false;
}
}
Once the skeletal mesh has bones defining the locations of the attach points of attachment locations. We can simply read those bones and generate a map of available sockets eg. mod_foregrip
, mod_handguard
, mod_barrel
.
Once knowing the map of available sockets we can also store what's currently set inside there, hence the map<string, GunPart> attached;
Which is also used in the GunPart::canAttach
function.
An implementation of an M4A1 could look like the following.
class M4A1 : Gun {
root: LowerReceiver,
attached: {
"mod_receiver": M4A1UpperReceiver(),
"mod_magazine": STANAG30(),
}
}
class M4A1UpperReceiver : GunPart {
attached = {
"mod_handguard": M4A1Handguard()
"mod_barrel": M4A1Barrel()
}
}
class M4A1Handguard : GunPart {
attached = {
"mod_foregrip": nullptr
"mod_laser": nullptr
// yes i know using a map here prevents us from having multiple laser
// slots, but there are different solutions we could apply
}
}
class M4A1Barrel : GunPart {
attached = {
"mod_muzzle": Silencer
}
}
class Magazine : GunPart {
TArray<UClass<Bullet>> bullets
// not gonna go into bullet, but an array let's us put specific ammunition
// types in and can mimic the FILO architecture of a standard magazine
int capacity
}
class STANAG30 : Magazine {
bullets = [
M855::Class(),
M995::Class(),
M856A1::Class()
],
capacity: 30,
}
This is all psuedocode so some values are added for example-sake, but the idea is there. It would be just as simple to incorporate the stats system by implementing a map<string, double> stats
on the Gun
class. Then adding a map<string, double> statModifiers
to modify it, so an updated attach function could look like this...
void GunPart::AttachPart(GunPart gunPart) {
// adding a variable called gunRef
gunPart.gunRef = this.gunRef;
root->AttachComponentToComponent(gunPart, gunPart.attach_name);
for (const auto& [key, value] : gunPart.statModifiers) {
if (this.gunRef.stats.contains(key)) {
// you could even go further here implementing a tuple or struct here to allow operator modification as well
this.gunRef.stats[key] += value;
}
}
}
Due to the nature of the parts being attached to other parts, it has an added nicety of being able to take of a part and retaining all it's attachments eg. a handguard and retaining the foregrip and lasers etc
Results
With a nice UI and more attachments, you could make a pretty interesting inventory system!
This system was built with ultimate flexibility in mind, but there's things you could do to get type safety on attachments natively with subclassing to prevent things like Kalishnakov barrels fitting onto 5.56 upper receivers etc