using SanAndreasUnity.Importing.Conversion; using SanAndreasUnity.Importing.Items; using SanAndreasUnity.Importing.Items.Definitions; using SanAndreasUnity.Importing.Vehicles; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using VehicleDef = SanAndreasUnity.Importing.Items.Definitions.VehicleDef; namespace SanAndreasUnity.Behaviours.Vehicles { public partial class Vehicle { [Flags] public enum WheelAlignment { None = 0, Front = 1, Mid = 2, Rear = 4, Left = 8, Right = 16, LeftRightMask = Left | Right, FrontMidRearMask = Front | Mid | Rear, RightFront = Right | Front, LeftFront = Left | Front, RightMid = Right | Mid, LeftMid = Left | Mid, RightRear = Right | Rear, LeftRear = Left | Rear, } [Flags] public enum SeatAlignment { None = 0, Front = 1, Back = 2, Left = 4, Right = 8, FrontBackMask = Front | Back, LeftRightMask = Left | Right, FrontRight = Front | Right, FrontLeft = Front | Left, BackRight = Back | Right, BackLeft = Back | Left, } public enum DoorAlignment { None, RightFront, LeftFront, RightRear, LeftRear, } private static VehicleDef[] _sRandomSpawnable; private static int _sMaxSpawnableIndex; private static VehicleDef[] GetRandomSpawnableDefs(out int maxIndex) { var all = Item.GetDefinitions().ToArray(); var defs = all .Where(x => x.Frequency > 0 && x.VehicleType == VehicleType.Car) .ToArray(); maxIndex = defs.Sum(x => x.Frequency); return defs; } private static VehicleDef GetRandomDef() { if (_sRandomSpawnable == null) { _sRandomSpawnable = GetRandomSpawnableDefs(out _sMaxSpawnableIndex); } var index = UnityEngine.Random.Range(0, _sMaxSpawnableIndex); foreach (var def in _sRandomSpawnable) { index -= def.Frequency; if (index < 0) return def; } throw new Exception("Unable to find cars to spawn"); } public static void GetPositionForSpawning(Transform inFrontOfTransform, out Vector3 pos, out Quaternion rot) { pos = Vector3.zero; rot = Quaternion.identity; Vector3 spawnOffset = new Vector3 (0, 2, 5); pos = inFrontOfTransform.position + inFrontOfTransform.forward * spawnOffset.z + inFrontOfTransform.up * spawnOffset.y + inFrontOfTransform.right * spawnOffset.x; rot = Quaternion.LookRotation(-inFrontOfTransform.right, Vector3.up); } public static Vehicle Create(VehicleSpawner spawner) { return Create(spawner.Info.CarId, spawner.Info.Colors, spawner.transform.position, spawner.transform.rotation); } public static Vehicle Create(int carId, Vector3 position, Quaternion rotation) { return Create (carId, null, position, rotation); } public static Vehicle CreateInFrontOf(int carId, Transform inFrontOfTransform) { Vector3 pos; Quaternion rot; GetPositionForSpawning (inFrontOfTransform, out pos, out rot); return Create (carId, pos, rot); } public static Vehicle CreateRandomInFrontOf(Transform inFrontOfTransform) { return CreateInFrontOf(-1, inFrontOfTransform); } public static Vehicle Create(int carId, int[] colors, Vector3 position, Quaternion rotation) { // this probably should not be done like this var go = Instantiate(VehicleManager.Instance.vehiclePrefab); var v = Create(go, carId, colors, position, rotation); if (Net.NetStatus.IsServer) { v.GetComponent().OnAfterCreateVehicle(); Net.NetManager.Spawn(go); } return v; } public static Vehicle Create(GameObject vehicleGameObject, int carId, int[] colors, Vector3 position, Quaternion rotation) { var inst = vehicleGameObject.AddComponent(); VehicleDef def; if (carId == -1) { def = GetRandomDef(); } else { def = Item.GetDefinition(carId); } inst.Initialize(def, colors); inst.transform.position = position - Vector3.up * inst.AverageWheelHeight; inst.transform.localRotation = rotation; OutOfRangeDestroyer destroyer = Utilities.F.GetOrAddComponent(inst.gameObject); destroyer.timeUntilDestroyed = 5; destroyer.range = 300; return inst; } private Geometry.GeometryParts _geometryParts; public class Wheel { public WheelAlignment Alignment { get; set; } public bool IsLeftHand { get { return (Alignment & WheelAlignment.Left) == WheelAlignment.Left; } } public bool IsRightHand { get { return (Alignment & WheelAlignment.Right) == WheelAlignment.Right; } } public bool IsFront { get { return (Alignment & WheelAlignment.Front) == WheelAlignment.Front; } } public bool IsMid { get { return (Alignment & WheelAlignment.Mid) == WheelAlignment.Mid; } } public bool IsRear { get { return (Alignment & WheelAlignment.Rear) == WheelAlignment.Rear; } } public Transform Parent { get; set; } public Transform Child { get; set; } public WheelCollider Collider { get; set; } public Wheel Complement { get; set; } public float Travel { get; private set; } public void UpdateTravel() { Travel = 1f; WheelHit hit; if (Collider.GetGroundHit(out hit)) { Travel = (-Parent.transform.InverseTransformPoint(hit.point).y - Collider.radius) / Collider.suspensionDistance; } } public Quaternion Roll { get; set; } } public class Seat { public SeatAlignment Alignment { get; internal set; } public Transform Parent { get; internal set; } /// Ped that is occupying this seat. public Ped OccupyingPed { get; internal set; } public float TimeWhenPedChanged { get; internal set; } = float.NegativeInfinity; public float TimeSincePedChanged => Time.time - this.TimeWhenPedChanged; public bool IsTaken { get { return this.OccupyingPed != null; } } public bool IsLeftHand { get { return (Alignment & SeatAlignment.Left) == SeatAlignment.Left; } } public bool IsRightHand { get { return (Alignment & SeatAlignment.Right) == SeatAlignment.Right; } } public bool IsFront { get { return (Alignment & SeatAlignment.Front) == SeatAlignment.Front; } } public bool IsBack { get { return (Alignment & SeatAlignment.Back) == SeatAlignment.Back; } } public bool IsDriver { get { return Alignment == SeatAlignment.FrontLeft; } } } private FrameContainer _frames; public FrameContainer Frames => _frames; public Transform EngineTransform { get; private set; } public Transform PetrolcapTransform { get; private set; } private static GameObject s_highDetailMeshesContainer; public Transform HighDetailMeshesParent { get; private set; } private List> m_highDetailMeshObjectsToUpdate = new List>(); private readonly List _wheels = new List(); private readonly List _seats = new List(); public List Wheels { get { return _wheels; } } public List Seats { get { return _seats; } } private WheelAlignment GetWheelAlignment(string frameName) { switch (frameName) { case "wheel_rf_dummy": return WheelAlignment.RightFront; case "wheel_lf_dummy": return WheelAlignment.LeftFront; case "wheel_rm_dummy": return WheelAlignment.RightMid; case "wheel_lm_dummy": return WheelAlignment.LeftMid; case "wheel_rb_dummy": return WheelAlignment.RightRear; case "wheel_lb_dummy": return WheelAlignment.LeftRear; default: return WheelAlignment.None; } } private DoorAlignment GetDoorAlignment(string frameName) { switch (frameName) { case "door_rf_dummy": return DoorAlignment.RightFront; case "door_lf_dummy": return DoorAlignment.LeftFront; case "door_rr_dummy": return DoorAlignment.RightRear; case "door_lr_dummy": return DoorAlignment.LeftRear; default: return DoorAlignment.None; } } public Transform GetPart(string name) { var frame = _frames.GetByName(name); return frame != null ? frame.transform : null; } private void AttachSeat(Transform parent, SeatAlignment alignment) { _seats.Add(new Seat { Parent = parent, Alignment = alignment }); } public static Geometry.GeometryParts LoadGeometryParts(VehicleDef vehicleDef) { return Geometry.Load(vehicleDef.ModelName, TextureDictionary.Load(vehicleDef.TextureDictionaryName), TextureDictionary.Load("vehicle"), TextureDictionary.Load("misc")); } private void Initialize(VehicleDef def, int[] colors = null) { Definition = def; if (colors != null && colors[0] != -1) { SetColors(colors); } else { var defaultClrs = CarColors.GetCarDefaults(Definition.ModelName); if (defaultClrs != null) { SetColors(defaultClrs[UnityEngine.Random.Range(0, defaultClrs.Count)]); } else { Debug.LogWarningFormat("No colours defined for {0}!", def.GameName); } } name = Definition.GameName; _geometryParts = LoadGeometryParts(Definition); _frames = _geometryParts.AttachFrames(transform, MaterialFlags.Vehicle); var wheelFrame = _frames.FirstOrDefault(x => x.Name == "wheel"); if (wheelFrame == null) { Debug.LogWarningFormat("No wheels defined for {0}!", def.GameName); Destroy(gameObject); return; } var engineFrame = _frames.FirstOrDefault(x => x.Name == "engine"); if (engineFrame != null) this.EngineTransform = engineFrame.transform; var petrolcapFrame = _frames.FirstOrDefault(x => x.Name == "petrolcap"); if (petrolcapFrame != null) this.PetrolcapTransform = petrolcapFrame.transform; foreach (var frame in _frames) { if (!frame.Name.StartsWith("wheel_")) continue; if (!frame.Name.EndsWith("_dummy")) continue; var childFrames = _frames.Where(x => x.ParentIndex == frame.Index); // disable all children of wheel dummies foreach (var childFrame in childFrames) { childFrame.gameObject.SetActive(false); } var wheelAlignment = GetWheelAlignment(frame.Name); Wheel inst; // see if this wheel dummy has a wheel child var wheel = childFrames.FirstOrDefault(x => x.Name == "wheel"); if (wheel == null) { var copy = Instantiate(wheelFrame.transform); copy.SetParent(frame.transform, false); _wheels.Add(inst = new Wheel { Alignment = wheelAlignment, Parent = frame.transform, Child = copy, }); } else { // all children of wheel dummies get set to inactive so activate this one wheel.gameObject.SetActive(true); _wheels.Add(inst = new Wheel { Alignment = wheelAlignment, Parent = frame.transform, Child = wheel.transform, }); } if (inst.IsLeftHand) { frame.transform.Rotate(Vector3.up, 180.0f); } inst.Complement = _wheels.FirstOrDefault(x => (x.Alignment & WheelAlignment.LeftRightMask) != (inst.Alignment & WheelAlignment.LeftRightMask) && (x.Alignment & WheelAlignment.FrontMidRearMask) == (inst.Alignment & WheelAlignment.FrontMidRearMask)); if (inst.Complement != null) { inst.Complement.Complement = inst; } } InitializePhysics(); this.Health = this.MaxHealth = Mathf.Pow(this.HandlingData.Mass, VehicleManager.Instance.massToHealthExponent); this.SetupDoorsHingeJoints(); var frontSeat = GetPart("ped_frontseat"); var backSeat = GetPart("ped_backseat"); if (frontSeat != null) { var frontSeatMirror = new GameObject("ped_frontseat").transform; frontSeatMirror.SetParent(frontSeat.parent, false); frontSeatMirror.localPosition = Vector3.Scale(frontSeat.localPosition, new Vector3(-1f, 1f, 1f)); if (frontSeat.localPosition.x > 0f) { AttachSeat(frontSeat, SeatAlignment.FrontRight); AttachSeat(frontSeatMirror, SeatAlignment.FrontLeft); } else { AttachSeat(frontSeatMirror, SeatAlignment.FrontRight); AttachSeat(frontSeat, SeatAlignment.FrontLeft); } DriverTransform = GetSeat(SeatAlignment.FrontLeft).Parent; } if (backSeat != null) { var backSeatMirror = new GameObject("ped_backseat").transform; backSeatMirror.SetParent(backSeat.parent, false); backSeatMirror.localPosition = Vector3.Scale(backSeat.localPosition, new Vector3(-1f, 1f, 1f)); if (backSeat.localPosition.x > 0f) { AttachSeat(backSeat, SeatAlignment.BackRight); AttachSeat(backSeatMirror, SeatAlignment.BackLeft); } else { AttachSeat(backSeatMirror, SeatAlignment.BackRight); AttachSeat(backSeat, SeatAlignment.BackLeft); } } // Add vehicle damage /* var dam = gameObject.AddComponent(); dam.damageParts = new Transform[] { transform.GetChild(0).Find("engine") }; dam.deformMeshes = gameObject.GetComponentsInChildren(); dam.displaceParts = gameObject.GetComponentsInChildren().Where(x => x.GetComponent() != null || x.GetComponent() != null).ToArray(); dam.damageFactor = VehicleAPI.constDamageFactor; dam.collisionIgnoreHeight = -.4f; dam.collisionTimeGap = .1f; //OptimizeVehicle(); dam.deformColliders = gameObject.GetComponentsInChildren(); */ gameObject.SetLayerRecursive(Layer); SetupHighDetailMesh(); } void SetupHighDetailMesh() { // We need to add mesh colliders with high detail vehicle's mesh. // These colliders will be used, among other things, when raycasting with weapons. // This is a problem because Unity does not support concave (non-convex) mesh colliders attached to rigid body. // Tried adding a separate kinematic rigid body (kinematic ones work with concave mesh colliders) to each object with a mesh filter, but without success. // So, we are left with the following options: // - somehow generate multiple convex meshes from a concave mesh // - create a separate game object with mesh colliders, and update his position/rotation every frame to be the same as vehicle's // Option with a separate game object is chosen. if (null == s_highDetailMeshesContainer) { s_highDetailMeshesContainer = new GameObject("Vehicle high detail meshes container"); } GameObject parent = new GameObject(this.gameObject.name); this.HighDetailMeshesParent = parent.transform; parent.transform.parent = s_highDetailMeshesContainer.transform; parent.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation); this.SetupDamagable(); // for each mesh filter, create child game object with mesh collider foreach (var meshFilter in this.gameObject.GetComponentsInChildren()) { GameObject child = new GameObject(meshFilter.gameObject.name, typeof(MeshCollider)); child.layer = Vehicle.MeshLayer; child.transform.parent = parent.transform; child.transform.SetPositionAndRotation(meshFilter.transform.position, meshFilter.transform.rotation); var meshCollider = child.GetComponent(); meshCollider.convex = false; meshCollider.sharedMesh = meshFilter.sharedMesh; if (null != meshFilter.gameObject.GetComponent() || null != meshFilter.transform.parent.GetComponent() || null != meshFilter.transform.parent.GetComponent()) { // this object has a dedicated rigid body or is a wheel, so it will move // make sure that we update transform of this object m_highDetailMeshObjectsToUpdate.Add(new KeyValuePair(meshFilter.transform, child.transform)); } } // add petrolcap /* if (this.PetrolcapTransform != null) { GameObject petrolcapGo = new GameObject(this.PetrolcapTransform.name, typeof(BoxCollider)); petrolcapGo.layer = Vehicle.MeshLayer; petrolcapGo.transform.parent = parent.transform; petrolcapGo.transform.SetPositionAndRotation(this.PetrolcapTransform.position, this.PetrolcapTransform.rotation); var boxCollider = petrolcapGo.GetComponent(); boxCollider.center = VehicleManager.Instance.petrolcapBoxColliderCenter; boxCollider.size = VehicleManager.Instance.petrolcapBoxColliderSize; this.PetrolcapUnderHighDetailMeshTransform = petrolcapGo.transform; } */ } void SetupDoorsHingeJoints() { foreach (var pair in _frames.Where(x => x.Name.StartsWith("door_"))) { var doorAlignment = GetDoorAlignment(pair.Name); if (doorAlignment == DoorAlignment.None) continue; var hinge = pair.gameObject.AddComponent(); hinge.axis = Vector3.up; hinge.useLimits = true; var limit = 90.0f * ((doorAlignment == DoorAlignment.LeftFront || doorAlignment == DoorAlignment.LeftRear) ? 1.0f : -1.0f); hinge.limits = new JointLimits { min = Mathf.Min(0, limit), max = Mathf.Max(0, limit), }; hinge.connectedBody = gameObject.GetComponent(); } } } }