Car Crash System

Which part are you most struggling with so I can help you understand?

2 Likes

The main part is all the functions, they confuse me a bit.

2 Likes

Here’s a commented version for you which should help:

-- helpers
local function create(obj) -- this is a function that just instances an object, and returns a function which allows you to change the object's properties by a table
	obj = Instance.new(obj)
	return function (props)
		for prop, val in next, props do
			obj[prop] = val
		end
		return obj
	end
end

-- init
local car = { }
function car.new(model)
	local this = { }
	this.car             = nil
	this.mass            = nil
	this.bounds          = nil
	this.currentVelocity = nil
	this.lastPosition    = nil
	this.update          = Instance.new 'BindableEvent' --> Set up a public bindable event so we can tell the other scripts when we crash
	this.crashed         = this.update.Event
	this.connections     = { }
	this.massThreshold   = 0 --> Determines the minimum relative mass our object needs to be to damage our car
	this.veloThreshold   = 0 --> Determines the minimum velocity we need to be travelling to cause damage => You could alter this to be partly determined by the mass of the object instead
	
	-- local
	local function getMass(part)
		-- this function calculates the mass of a singular part given its volume and the density as defined by the PhysicalProperties class
		local density = PhysicalProperties.new(part.Material).Density
		local size    = part.Size
		local volume  = size.x * size.y * size.x
		return density * volume
	end
	
	local function recurseMass(obj)
		-- this function recursively calculates a model's mass by finding all the parts and calling the 'getMass' function
		local mass = 0
		local recurse do
			function recurse(item)
				for i, v in next, item:GetChildren() do
					if v:IsA 'BasePart' then
						mass = mass + getMass(v)
					end
					recurse(v)
				end
			end
		end
		return mass
	end
	
	local function normalToSurface(cf, normal)
		-- this function takes the normal vector e.g. Vec3(1, 0, 0) and determines the surface of the object from the normal vector
		normal = cf:vectorToObjectSpace(normal)
		local x, y, z = normal.x, normal.y, normal.z
		if x ~= 0 and (y == 0 and z == 0) then
			return x > 0 and 'Right' or 'Left'
		elseif y ~= 0 and (x == 0 and z == 0) then
			return y > 0 and 'Top' or 'Bottom'
		elseif z ~= 0 and (x == 0 and y == 0) then
			return z > 0 and 'Back' or 'Front'
		end
		return 'Back'
	end
	
	local function carCollision(hit)
		if not this.currentVelocity then
			return
		end
		if hit and hit.Parent then
			-- Let's find the mass of the part we touched
			local mass = getMass(hit)
			-- Is it at least the same or more mass than ourselves?
			if mass - this.mass > this.massThreshold then
				-- Are we travelling faster than our threshold velocity for damage to occur?
				if this.currentVelocity.magnitude > this.veloThreshold then
					-- Find the position, normal and surface of the hit to pass back for some kind of impact on that part etc
					local _, collision, normal = game.Workspace:FindPartOnRayWithWhitelist(
						Ray.new(this.bounds.Position, (hit.Position - this.bounds.Position).unit * (hit.Position - this.bounds.Position).magnitude),
						{hit}
					)
					local surface = normalToSurface(hit.CFrame, normal)
					
					-- Perform our crash event and pass back the current info
					this:crash(hit, collision, normal, surface, {
						relativeMass   = mass - this.mass,
						impactVelocity = this.currentVelocity.magnitude
					})
				end
			end
		end
	end
	
	-- private
	function this._construct(obj)
		-- This is the constructor e.g. every time someone calls car.new() we'll make a new car class and set it up for use
		if typeof(obj) ~= 'Instance' then
			return warn 'Unable to instantiate, provide an instance'
		end
		
		-- construct our basic variables and our hit box
		-- we're creating a hitbox so we can ensure we can detect collisions from all axes without having to set up a touch event for each part of the car
		-- we've also ensured that we're checking to see if it's a model or not, to determine how to create the collision box
		-- similarly, we've made sure to instantiate a weld if the car is unanchored so we don't have to recursively set the CFrame
		if obj:IsA 'Part' then
			this.mass   = getMass(obj)
			this.bounds = obj
		else
			this.mass = recurseMass(obj)
		
			local cf, size = obj:GetBoundingBox()
			this.bounds = create 'Part' {
				Name         = 'BOUNDING_BOX';
				CanCollide   = false;
				Anchored     = obj.PrimaryPart.Anchored;
				Size         = size;
				Transparency = 1;
				Parent       = obj;
			}
			-- this is called a ternary operator, but i've sunk the return value
			-- basically we've done:
			--[[
				if not this.bounds.Anchored then
					-->> CREATE WELD
				else
					-- do nothing
				end
			]]
			_ = not this.bounds.Anchored and create 'Weld' {
				Parent = this.bounds;
				Part0  = obj.PrimaryPart;
				Part1  = this.bounds;
				C0     = obj.PrimaryPart.CFrame:toObjectSpace(cf);
			}
		end
		this.car = obj:IsA "Model" and obj.PrimaryPart or obj --> this.car is always the primarypart of the car, or it's the part that's acting as a car
		
		if not this.lastPosition then
			this.lastPosition = this.bounds ~= obj and (function () local cf = obj:GetBoundingBox() return cf.p end)() or obj.CFrame.p
		end
		this.connections['joint'] = game:GetService('RunService').Heartbeat:connect(function (delta)
			if not this.car or not this.car.Parent then
				return this:destroy()
			end
			
			-- move our bounding box along with our car if it's anchored (because the weld we tried to create won't work if it's anchored)
			local cf = this.bounds ~= obj and obj:GetBoundingBox() or obj.CFrame
			if this.bounds ~= obj then
				this.bounds.CFrame = cf
			end
			
			-- set up our velocity determinants
			local pos = cf.p
			this.currentVelocity = (pos - this.lastPosition) / delta --> this is just the equation v = d/t where v = velocity, d = distance, and t = time
			this.lastPosition    = pos
		end)
		this.connections['collision'] = this.bounds.Touched:connect(carCollision) --> Set up touch event with the bounding box
		
		return this
	end
	
	-- public
	function this:getMass() -- function for other scripts to get our mass
		return this.mass
	end
	
	function this:getCar() -- function for other scripts to get the car's primary part
		return this.car
	end
	
	function this:setMassThreshold(val) -- function for other scripts to set the mass threshold
		this.massThreshold = val
		return this -- this is called 'chaining' we're passing back the current instance of our class so they can do further changes
	end
	
	function this:setVelocityThreshold(val) -- function for other scripts to set the velocity threshold
		this.veloThreshold = val
		return this -- this is called 'chaining' we're passing back the current instance of our class so they can do further changes
	end
	
	function this:crash(hit, position, normal, surface, impact) -- this is our crash event which fires the .crashed event we created earlier
		-- Remove our listeners
		for i, v in next, this.connections do
			if v then
				v:disconnect()
			end
		end
		if this.bounds ~= this.car then
			this.bounds:Destroy()
		end
		-- Remove our body objects if we're not anchored
		if not this.car.Anchored then
			for i, v in next, this.car:GetChildren() do
				if v:IsA "BodyVelocity" or v:IsA "BodyPosition" or v:IsA "BodyGyro" then
					v:Destroy()
				end
			end
		end
		-- Fire this event to our .crashed event, so any listeners will be told of our crash
		this.update:Fire(hit, position, normal, surface, impact)
	end
	
	function this:destroy()
		-- Remove all listeners and delete our car
		for i, v in next, this.connections do
			if v then
				v:disconnect()
			end
		end
		if this.car and this.car.Parent then
			this.car:Destroy()
		end
	end
	
	-- construct
	return this._construct(model)
end

return car

The other script commented:

-- local
local function create(obj) -- same as in the other script
	obj = Instance.new(obj)
	return function (props)
		for prop, val in next, props do
			obj[prop] = val
		end
		return obj
	end
end

-- init
local car = require(script.car_class).new(script.Parent) -- require the module with our fancy car class

-- fires when the car crashes as defined by our MassThreshold and VelocityThreshold properties as I mentioned in the other script
car.crashed:connect(function (hit, position, normal, surface, impact)
	-- debug to show the hit information
	print(
		('Our car instance collided with %s\'s %s surface (relative mass %d) at a velocity magnitude of %d studs/second'):format(hit.Name, surface, impact.relativeMass, impact.impactVelocity)
	)
	-- Instantiate our smoke / do any other animations
	create "Smoke" {
		Parent = car:getCar()
	}
end)

-- this is an example of the 'chaining' i mentioned above
car:setMassThreshold(10):setVelocityThreshold(30):getCar().BodyVelocity.Velocity = Vector3.new(0, 0, -50)

-- this wasn't necessary though, you could have also done this:
car:setMassThreshold(10)
car:setVelocityThreshold(30)

local car_primary_part = car:getCar()
car_primary_part.BodyVelocity.Velocity = Vector3.new(0, 0, -50)

Check out the placefile.rbxl file I sent above to see the architecture of the set up e.g. a model with a primarypart, and inside the primarypart is a BodyVelo + BodyPos. You don’t need to use those BodyMovers though, your car can be anchored and you can do whatever you like with it.

Similarly, as mentioned in other posts, you don’t need to use this car module - it’s just because I got bored and wanted to try it and thought it may be helpful. You’re more than welcome to rip out parts from the crashing parts and use it as you wish

5 Likes

Hey there,
Are you able to explain how to implement your plugin into A-Chassis system?
As well, doing the repair tool.

2 Likes

I have a issue with this script when I add BodyVelocity to the car it messes the cars up by making it not able to drive. Any tips?

1 Like