├── exampleplace.rbxl ├── LICENSE ├── README.md └── wheel.lua /exampleplace.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeehamsonThe3rd/raycastsuspensionwheel/HEAD/exampleplace.rbxl -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Liam O'Keefe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raycast Suspension Wheel 2 | 3 | A (mostly) easy-to-use ROBLOX module that allows for wheels with raycast suspension instead of the standard physics-based wheels 4 | 5 | ## How to use 🏁 6 | 7 | ### Installation Guide 🔧 8 | 9 | To use this module you will need to copy the source code and place it into a module script. Then put this in your project, you will then need to create a chassis and some wheel parts. Once that is done, ensure that each wheel has its own `Attachment`, `VectorForce`, and `Weld` objects, name these `Attachment`, `VectorForce`, and `Weld` respectively. There is one more required step before using the module. Be sure that each wheel has these 5 attributes `SuspensionDamping : number`, `SuspensionHeight : number`, `SuspensionSpring : number`, `Debug : boolean`, and `RollingResistance : number` (please note the colon followed by the type is just so you know what type of attribute to make, it's means nothing more than that). Another thing to note is that the horizontal wheel friction is controlled by custom physical properties **ENABLE CUUSTOM PHYSICAL PROPERTIES OR YOU WILL GET CONSOLE ERRORS** (the perfered value is 1 so the wheel doesn't slip on small hills) Once done you may finally use the module! 10 | 11 | ### Scripting 💻 12 | 13 | This module is so easy to use it only requires that you call two functions from the class, those being `.new(VehicleModel : Model, Model : Part)`, to create the wheel, and `:Update()` to update the wheel physics. 14 | 15 | **If the wheels are tuned correctly everything should be working at this point** 16 | 17 | Example: 18 | ```lua 19 | local RunService = game:GetService("RunService") 20 | local WheelModule = require(script.Parent:WaitForChild("WheelModule")) 21 | 22 | local VehicleModule = {} 23 | VehicleModule.__index = VehicleModule 24 | 25 | function VehicleModule.new(Model : Model) 26 | local self = setmetatable({}, VehicleModule) 27 | 28 | self.Model = Model 29 | self.Wheels = {} 30 | 31 | self:GetWheels() 32 | 33 | self.Model.PrimaryPart:SetNetworkOwner(nil) 34 | RunService.Heartbeat:Connect(function() self:Update() end) 35 | 36 | return self 37 | end 38 | 39 | function VehicleModule:GetWheels() 40 | for _,Wheel in pairs(self.Model.Wheels:GetChildren()) do 41 | table.insert(self.Wheels, WheelModule.new(self.Model, Wheel)) 42 | end 43 | end 44 | 45 | function VehicleModule:UpdateWheels() 46 | for _,Wheel in pairs(self.Wheels) do 47 | Wheel:Update() 48 | end 49 | end 50 | 51 | function VehicleModule:Update() 52 | self:UpdateWheels() 53 | end 54 | 55 | return VehicleModule 56 | ``` 57 | 58 | ## Documentation 📚 59 | 60 | `Wheel.new(VehicleModel : Model, Model : Part)`: Instantiates the wheel class using the provided `VehicleModel` and `Model` parameters. 61 | 62 | `Wheel:Update()`: Recalculates the wheel's suspension and forces. 63 | -------------------------------------------------------------------------------- /wheel.lua: -------------------------------------------------------------------------------- 1 | local WheelModule = {} 2 | WheelModule.__index = WheelModule 3 | 4 | function WheelModule.new(VehicleModel : Model, Model : Model) 5 | local self = setmetatable({}, WheelModule) 6 | 7 | self.SuspensionHeight = Model:GetAttribute("SuspensionHeight") 8 | self.SuspensionDamping = Model:GetAttribute("SuspensionDamping") 9 | self.SuspensionSpring = Model:GetAttribute("SuspensionSpring") 10 | self.RollingResistance = Model:GetAttribute("RollingResistance") 11 | 12 | self.Debug = Model:GetAttribute("Debug") 13 | 14 | self.Vehicle = VehicleModel 15 | 16 | self.Model = Model 17 | self.Weld = self.Model.Weld 18 | self.Attachment = self.Model.Attachment 19 | self.VectorForce = self.Attachment.VectorForce 20 | self.RaycastParams = RaycastParams.new() 21 | self.Raycast = nil 22 | 23 | self.SuspensionForce = Vector3.zero 24 | self.SuspensionFrictionForce = Vector3.zero 25 | self.ResistanceForce = Vector3.zero 26 | self.FrictionForce = Vector3.zero 27 | 28 | self.Offset = self.Weld.C1 29 | 30 | self.RaycastParams.FilterDescendantsInstances = {VehicleModel} 31 | 32 | if self.Debug then 33 | self.UpIndicator = Instance.new("ConeHandleAdornment", workspace) 34 | self.UpIndicator.Adornee = workspace.Terrain 35 | end 36 | 37 | return self 38 | end 39 | 40 | function WheelModule:Update(...) 41 | self.SuspensionHeight = self.Model:GetAttribute("SuspensionHeight") 42 | self.SuspensionDamping = self.Model:GetAttribute("SuspensionDamping") 43 | self.SuspensionSpring = self.Model:GetAttribute("SuspensionSpring") 44 | self.RollingResistance = self.Model:GetAttribute("RollingResistance") 45 | 46 | self:CalculatePhysics(...) 47 | self:ApplyForce() 48 | self:UpdateModel() 49 | 50 | self.Debug = self.Model:GetAttribute("Debug") 51 | self:UpdateDebug() 52 | end 53 | 54 | local function AbsoluteVector(v : Vector3) 55 | return Vector3.new(math.abs(v.X),math.abs(v.Y),math.abs(v.Z)) 56 | end 57 | 58 | function WheelModule:CalculatePhysics(dt) 59 | self.Raycast = workspace:Raycast(self.Model.Position-(Vector3.yAxis*self.Weld.C1.Position), -self.Vehicle.PrimaryPart.CFrame.UpVector*self.SuspensionHeight, self.RaycastParams) 60 | 61 | if self.Raycast then 62 | local Extension = self.SuspensionHeight-self.Raycast.Distance 63 | local RelativeVelocity = self.Vehicle.PrimaryPart.CFrame:VectorToObjectSpace(self.Model.Velocity) 64 | 65 | self.SuspensionForce = self.Vehicle.PrimaryPart.CFrame.UpVector*(self.SuspensionSpring*Extension-self.SuspensionDamping*RelativeVelocity.Y) 66 | self.SuspensionFrictionForce = self.SuspensionForce*-AbsoluteVector(self.Vehicle.PrimaryPart.CFrame:VectorToObjectSpace(Vector3.new(self.Model.CustomPhysicalProperties.Friction,0,self.RollingResistance))) 67 | self.ResistanceForce = self.Vehicle.PrimaryPart.CFrame.LookVector*(((RelativeVelocity.Z*self.Model.AssemblyMass))*self.RollingResistance) 68 | self.FrictionForce = -self.Vehicle.PrimaryPart.CFrame.RightVector*(((RelativeVelocity.X*self.Model.AssemblyMass))*self.Model.CustomPhysicalProperties.Friction) 69 | end 70 | end 71 | 72 | function WheelModule:ApplyForce() 73 | if not self.Raycast then self.VectorForce.Force = Vector3.zero return end 74 | self.VectorForce.Force = self.SuspensionForce + self.SuspensionFrictionForce + self.ResistanceForce + self.FrictionForce 75 | end 76 | 77 | function WheelModule:UpdateModel() 78 | local RelativeVelocity = self.Vehicle.PrimaryPart.CFrame:VectorToObjectSpace(self.Model.Velocity) 79 | 80 | self.Weld.C1 = self.Offset * CFrame.new(0,-self.SuspensionHeight+(self.Model.Size.Y/2),0) 81 | if self.Raycast then self.Weld.C1 = self.Offset * CFrame.new(0,-self.Raycast.Distance+(self.Model.Size.Y/2),0) end 82 | self.Weld.C0 *= CFrame.Angles(math.rad(-RelativeVelocity.Z/(self.Model.Size.Y/2)),0,0) 83 | end 84 | 85 | function WheelModule:UpdateDebug() 86 | if not self.Debug then return end 87 | 88 | self.UpIndicator.CFrame = CFrame.new(self.Model.Position+(Vector3.yAxis*5)) * CFrame.Angles(math.rad(90),0,0) 89 | if self.Raycast then 90 | self.UpIndicator.CFrame = CFrame.new(self.Model.Position+(Vector3.yAxis*5)) * CFrame.lookAt(Vector3.zero, self.SuspensionForce.Unit) 91 | end 92 | 93 | print("\n [Suspension]: "..tostring(self.SuspensionForce), "\n [SuspensionFriction]: "..tostring(self.SuspensionFrictionForce), "\n [Resistance]: "..tostring(self.ResistanceForce), "\n [Friction]: "..tostring(self.FrictionForce), "\n [NET]: "..tostring(self.VectorForce.Force)) 94 | end 95 | 96 | return WheelModule 97 | --------------------------------------------------------------------------------