diff --git a/Assets/Scripts/Data/QExtensions.cs b/Assets/Scripts/Data/QExtensions.cs index 584a6c9..b5b0807 100644 --- a/Assets/Scripts/Data/QExtensions.cs +++ b/Assets/Scripts/Data/QExtensions.cs @@ -51,6 +51,11 @@ public static class QExtensions return new Vector3(vec.x, vec.z, vec.y); } + public static Vector3 ToUnityPosition(this QVec3 origin) + { + return new Vector3(origin.x, origin.z, origin.y); + } + public static Quaternion ToUnityRotation(this QVec3 angles) { return Quaternion.Euler(angles.x, 90 - angles.y, -angles.z); diff --git a/Assets/Scripts/Game/Entity.cs b/Assets/Scripts/Game/Entity.cs new file mode 100644 index 0000000..1cf1878 --- /dev/null +++ b/Assets/Scripts/Game/Entity.cs @@ -0,0 +1,62 @@ +using UnityEngine; +using UnityEngine.Rendering; + +public class Entity +{ + private readonly int entityNum; + + private GameObject gameObject; + private SkinnedMeshRenderer meshRenderer; + + private AliasModel aliasModel; + + public Entity(int entityNum) + { + this.entityNum = entityNum; + CreateGameObject(); + } + + private void CreateGameObject() + { + gameObject = new GameObject($"Entity_{entityNum}"); + + // DEBUG - we'll want to instantiate a prefab and assign materials from a preset collection + meshRenderer = gameObject.AddComponent(); + meshRenderer.material = new Material(Shader.Find("Universal Render Pipeline/Simple Lit")); + meshRenderer.shadowCastingMode = ShadowCastingMode.Off; + meshRenderer.receiveShadows = false; + meshRenderer.lightProbeUsage = LightProbeUsage.Off; + meshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off; + } + + public void Destroy() + { + Object.Destroy(gameObject); + } + + public void SetAliasModel(AliasModel model) + { + aliasModel = model; + + // Set a default pose based on the first animation frame + UpdateAnimation(0); + } + + public void UpdateAnimation(int frameNum) + { + if (aliasModel != null) + { + aliasModel.Animate(frameNum, out Mesh mesh, out float blendWeight); + meshRenderer.sharedMesh = mesh; + + if (mesh.blendShapeCount > 0) + meshRenderer.SetBlendShapeWeight(0, blendWeight); + } + } + + public void SetTransform(Vector3 position, Quaternion rotation) + { + gameObject.transform.position = position; + gameObject.transform.rotation = rotation; + } +} diff --git a/Assets/Scripts/Game/Entity.cs.meta b/Assets/Scripts/Game/Entity.cs.meta new file mode 100644 index 0000000..74a62a1 --- /dev/null +++ b/Assets/Scripts/Game/Entity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e876769ea29c4d10bfa6b7eea48009a3 +timeCreated: 1625824889 \ No newline at end of file diff --git a/Assets/Scripts/Game/GameAssets.cs b/Assets/Scripts/Game/GameAssets.cs index d365889..37e13c3 100644 --- a/Assets/Scripts/Game/GameAssets.cs +++ b/Assets/Scripts/Game/GameAssets.cs @@ -45,6 +45,21 @@ public class GameAssets aliasModels.Add(aliasModel); } + public bool TryGetAliasModel(string name, out AliasModel aliasModel) + { + foreach (var model in aliasModels) + { + if (model.Name == name) + { + aliasModel = model; + return true; + } + } + + aliasModel = null; + return false; + } + public void AddBrushModel(BrushModel brushModel) { brushModels.Add(brushModel); diff --git a/Assets/Scripts/Game/GameState.cs b/Assets/Scripts/Game/GameState.cs index 9380756..c3044d5 100644 --- a/Assets/Scripts/Game/GameState.cs +++ b/Assets/Scripts/Game/GameState.cs @@ -9,6 +9,8 @@ public class GameState private GameObject worldGameObject; + private readonly Dictionary entities = new Dictionary(); + public GameState(UniQuake uniQuake) { uq = uniQuake; @@ -16,16 +18,18 @@ public class GameState public void Destroy() { + DestroyEntities(); DestroyWorld(); } public void NewMap(BrushModel worldModel) { - DestroyWorld(); + Destroy(); // DEBUG - we'll want to instantiate prefabs for each submodel and assign materials from a preset collection worldGameObject = new GameObject(worldModel.Name); - for (int i = 0; i < worldModel.SubModelCount; ++i) + //for (int i = 0; i < worldModel.SubModelCount; ++i) + for (int i = 0; i < 1; ++i) { var subModel = worldModel.GetSubModel(i); var subModelGO = new GameObject($"SubModel_{i}"); @@ -64,4 +68,61 @@ public class GameState worldGameObject = null; } } + + public void SetEntityAliasModel(int entityNum, string modelName) + { + if (!entities.TryGetValue(entityNum, out var entity)) + { + entity = new Entity(entityNum); + entities.Add(entityNum, entity); + } + + if (!uq.GameAssets.TryGetAliasModel(modelName, out var aliasModel)) + { + Debug.LogWarning($"Unknown alias model name: {aliasModel}"); + return; + } + + entity.SetAliasModel(aliasModel); + } + + public void SetEntityWorldModel(int entityNum, int subModelNum) + { + // TODO: obtain Mesh from brush submodel and assign it to MeshRenderer + } + + public void UpdateEntityAnimation(int entityNum, int frameNum) + { + if (entities.TryGetValue(entityNum, out var entity)) + { + entity.UpdateAnimation(frameNum); + } + } + + public void SetEntityTransform(int entityNum, Vector3 position, Quaternion rotation) + { + if (entities.TryGetValue(entityNum, out var entity)) + { + entity.SetTransform(position, rotation); + } + } + + public void RemoveEntity(int entityNum) + { + if (entities.TryGetValue(entityNum, out var entity)) + { + entity.Destroy(); + entities.Remove(entityNum); + } + } + + private void DestroyEntities() + { + foreach (var entity in entities.Values) + { + entity.Destroy(); + } + + entities.Clear(); + } } diff --git a/Assets/Scripts/Modules/GameModule.Interop.cs b/Assets/Scripts/Modules/GameModule.Interop.cs index ce5ff45..181552d 100644 --- a/Assets/Scripts/Modules/GameModule.Interop.cs +++ b/Assets/Scripts/Modules/GameModule.Interop.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; +using AOT; using UnityEngine; public partial class GameModule : CallbackHandler @@ -11,6 +12,10 @@ public partial class GameModule : CallbackHandler var callbacks = new Callbacks { target = TargetPtr, + + SetEntityModel = CreateCallback(Callback_GameSetEntityModel), + SetEntityTransform = CreateCallback(Callback_GameSetEntityTransform), + RemoveEntity = CreateCallback(Callback_GameRemoveEntity), }; RegisterCallbacks(callbacks); @@ -23,5 +28,36 @@ public partial class GameModule : CallbackHandler private class Callbacks { public IntPtr target; + + public IntPtr SetEntityModel; + public IntPtr SetEntityTransform; + public IntPtr RemoveEntity; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GameSetEntityModelCallback(IntPtr target, int entityNum, [MarshalAs(UnmanagedType.LPStr)] string modelName, int frame); + + [MonoPInvokeCallback(typeof(GameSetEntityModelCallback))] + private static void Callback_GameSetEntityModel(IntPtr target, int entityNum, string modelName, int frame) + { + GetSelf(target).SetEntityModel(entityNum, modelName, frame); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GameSetEntityTransformCallback(IntPtr target, int entityNum, QVec3 origin, QVec3 angles); + + [MonoPInvokeCallback(typeof(GameSetEntityTransformCallback))] + private static void Callback_GameSetEntityTransform(IntPtr target, int entityNum, QVec3 origin, QVec3 angles) + { + GetSelf(target).SetEntityTransform(entityNum, origin, angles); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GameRemoveEntityCallback(IntPtr target, int entityNum); + + [MonoPInvokeCallback(typeof(GameRemoveEntityCallback))] + private static void Callback_GameRemoveEntity(IntPtr target, int entityNum) + { + GetSelf(target).RemoveEntity(entityNum); } } diff --git a/Assets/Scripts/Modules/GameModule.cs b/Assets/Scripts/Modules/GameModule.cs index 120870f..bfb3cd5 100644 --- a/Assets/Scripts/Modules/GameModule.cs +++ b/Assets/Scripts/Modules/GameModule.cs @@ -11,4 +11,37 @@ public partial class GameModule uq = uniQuake; BuildCallbacks(); } + + private void SetEntityModel(int entityNum, string modelName, int frame) + { + if (modelName != null) + { + if (modelName.StartsWith("*")) + { + if (!int.TryParse(modelName.Substring(1), out int subModelNum)) + { + Debug.LogWarning($"Invalid world submodel index: {modelName}"); + return; + } + + uq.GameState.SetEntityWorldModel(entityNum, subModelNum); + return; + } + + // TODO: identify non-world brush model names + uq.GameState.SetEntityAliasModel(entityNum, modelName); + } + + uq.GameState.UpdateEntityAnimation(entityNum, frame); + } + + private void SetEntityTransform(int entityNum, QVec3 origin, QVec3 angles) + { + uq.GameState.SetEntityTransform(entityNum, origin.ToUnityPosition(), angles.ToUnityRotation()); + } + + private void RemoveEntity(int entityNum) + { + uq.GameState.RemoveEntity(entityNum); + } } diff --git a/Assets/Scripts/Modules/RenderModule.cs b/Assets/Scripts/Modules/RenderModule.cs index b0123eb..b1e36b0 100644 --- a/Assets/Scripts/Modules/RenderModule.cs +++ b/Assets/Scripts/Modules/RenderModule.cs @@ -31,12 +31,12 @@ public partial class RenderModule Debug.Log($"Alias model '{name}' (frame type {frameType}) with {header.numVerts} vertices, {header.numTriangles} triangles, {header.numFrames} frame(s), {header.numPoses} pose(s):\n{sb}"); - string modelName = System.IO.Path.GetFileNameWithoutExtension(name); - AliasModel aliasModel = new AliasModel(modelName, frameType); + AliasModel aliasModel = new AliasModel(name, frameType); aliasModel.ImportMeshData(header, poseVertices, triangles, stVertices); uq.GameAssets.AddAliasModel(aliasModel); + string modelName = System.IO.Path.GetFileNameWithoutExtension(name); var go = new GameObject(modelName); go.transform.SetPositionAndRotation(new Vector3(xPos, 0, zPos), Quaternion.identity); @@ -137,8 +137,8 @@ public partial class RenderModule var cam = uq.Camera; if (cam == null) return; - - cam.transform.position = origin.ToVector3().ToUnity(); + + cam.transform.position = origin.ToUnityPosition(); cam.transform.rotation = angles.ToUnityRotation(); } } diff --git a/Assets/Scripts/Support/AliasModel.cs b/Assets/Scripts/Support/AliasModel.cs index 49958f7..c083f44 100644 --- a/Assets/Scripts/Support/AliasModel.cs +++ b/Assets/Scripts/Support/AliasModel.cs @@ -11,6 +11,8 @@ public class AliasModel private readonly QAliasFrameType frameType; private readonly List<(int, Mesh)> animationMeshes = new List<(int, Mesh)>(); + public string Name => name; + public AliasModel(string name, QAliasFrameType frameType) { this.name = name; diff --git a/Assets/Scripts/UniQuake.Interop.cs b/Assets/Scripts/UniQuake.Interop.cs index fe03e8d..a504867 100644 --- a/Assets/Scripts/UniQuake.Interop.cs +++ b/Assets/Scripts/UniQuake.Interop.cs @@ -18,7 +18,7 @@ public partial class UniQuake private IntPtr libraryHandle; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void UniQuake_InitFunc(IntPtr parms, IntPtr sysCalls, IntPtr glCalls); + private delegate void UniQuake_InitFunc(IntPtr parms, IntPtr sysCalls, IntPtr glCalls, IntPtr gameCalls); private UniQuake_InitFunc UniQuake_Init; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] diff --git a/Assets/Scripts/UniQuake.cs b/Assets/Scripts/UniQuake.cs index bc04b8c..e4c32d7 100644 --- a/Assets/Scripts/UniQuake.cs +++ b/Assets/Scripts/UniQuake.cs @@ -96,7 +96,9 @@ public partial class UniQuake: MonoBehaviour try { UniQuake_SetFmodSystem(AudioManager.Instance.FmodSystem.handle); - UniQuake_Init(quakeParms.ToNativePtr(), systemModule.CallbacksPtr, renderModule.CallbacksPtr); + UniQuake_Init(quakeParms.ToNativePtr(), + systemModule.CallbacksPtr, renderModule.CallbacksPtr, gameModule.CallbacksPtr); + initialized = true; } catch (QuakeException ex) diff --git a/engine/Quake/cl_main.c b/engine/Quake/cl_main.c index 5d870bd..94521d6 100644 --- a/engine/Quake/cl_main.c +++ b/engine/Quake/cl_main.c @@ -459,6 +459,7 @@ void CL_RelinkEntities (void) { ent->model = NULL; ent->lerpflags |= LERP_RESETMOVE|LERP_RESETANIM; //johnfitz -- next time this entity slot is reused, the lerp will need to be reset + UQ_Game_RemoveEntity(i); continue; } @@ -580,6 +581,8 @@ void CL_RelinkEntities (void) cl_visedicts[cl_numvisedicts] = ent; cl_numvisedicts++; } + + UQ_Game_SetEntityTransform(i, ent->origin, ent->angles); } } diff --git a/engine/Quake/cl_parse.c b/engine/Quake/cl_parse.c index 75bacda..3f021c9 100644 --- a/engine/Quake/cl_parse.c +++ b/engine/Quake/cl_parse.c @@ -617,6 +617,13 @@ void CL_ParseUpdate (int bits) R_TranslateNewPlayerSkin (num - 1); //johnfitz -- was R_TranslatePlayerSkin ent->lerpflags |= LERP_RESETANIM; //johnfitz -- don't lerp animation across model changes + + UQ_Game_SetEntityModel(num, model->name, ent->frame); + } + else + { + // Model hasn't changed, just update the animation info + UQ_Game_SetEntityModel(num, NULL, ent->frame); } //johnfitz diff --git a/engine/Quake/client.h b/engine/Quake/client.h index 1c7db18..b2ccb0c 100644 --- a/engine/Quake/client.h +++ b/engine/Quake/client.h @@ -372,5 +372,9 @@ void TraceLine (vec3_t start, vec3_t end, vec3_t impact); void Chase_UpdateForClient (void); //johnfitz void Chase_UpdateForDrawing (void); //johnfitz +void UQ_Game_SetEntityModel(int entityNum, const char *modelName, int frame); +void UQ_Game_SetEntityTransform(int entityNum, vec3_t origin, vec3_t angles); +void UQ_Game_RemoveEntity(int entityNum); + #endif /* _CLIENT_H_ */ diff --git a/engine/UniQuake/game_uniquake.c b/engine/UniQuake/game_uniquake.c new file mode 100644 index 0000000..3f3c814 --- /dev/null +++ b/engine/UniQuake/game_uniquake.c @@ -0,0 +1,29 @@ +#include "uniquake.h" + +#include "../Quake/quakedef.h" + +typedef struct unity_gamecalls_s +{ + void *target; + + void(*SetEntityModel)(void *target, int entityNum, const char *modelName, int frame); + void(*SetEntityTransform)(void *target, int entityNum, vec3_t origin, vec3_t angles); + void(*RemoveEntity)(void *target, int entityNum); +} unity_gamecalls_t; + +const unity_gamecalls_t *unity_gamecalls; + +void UQ_Game_SetEntityModel(int entityNum, const char *modelName, int frame) +{ + unity_gamecalls->SetEntityModel(unity_gamecalls->target, entityNum, modelName, frame); +} + +void UQ_Game_SetEntityTransform(int entityNum, vec3_t origin, vec3_t angles) +{ + unity_gamecalls->SetEntityTransform(unity_gamecalls->target, entityNum, origin, angles); +} + +void UQ_Game_RemoveEntity(int entityNum) +{ + unity_gamecalls->RemoveEntity(unity_gamecalls->target, entityNum); +} diff --git a/engine/UniQuake/uniquake.c b/engine/UniQuake/uniquake.c index ed0d402..b69f71c 100644 --- a/engine/UniQuake/uniquake.c +++ b/engine/UniQuake/uniquake.c @@ -14,12 +14,13 @@ UNIQUAKE_API void UniQuake_SetFmodSystem(FMOD_SYSTEM *system, int playernumber) } #endif -UNIQUAKE_API void UniQuake_Init(quakeparms_t *parms, const unity_syscalls_t *syscalls, const unity_glcalls_t *glcalls) +UNIQUAKE_API void UniQuake_Init(quakeparms_t *parms, const unity_syscalls_t *syscalls, const unity_glcalls_t *glcalls, const unity_gamecalls_t *gamecalls) { host_parms = parms; unity_syscalls = syscalls; unity_glcalls = glcalls; + unity_gamecalls = gamecalls; COM_InitArgv(parms->argc, parms->argv); parms->argc = com_argc; diff --git a/engine/UniQuake/uniquake.h b/engine/UniQuake/uniquake.h index 9024056..28a1929 100644 --- a/engine/UniQuake/uniquake.h +++ b/engine/UniQuake/uniquake.h @@ -14,3 +14,6 @@ extern const unity_syscalls_t *unity_syscalls; typedef struct unity_glcalls_s unity_glcalls_t; extern const unity_glcalls_t *unity_glcalls; + +typedef struct unity_gamecalls_s unity_gamecalls_t; +extern const unity_gamecalls_t *unity_gamecalls; diff --git a/engine/Windows/VisualStudio/uniquake.vcxproj b/engine/Windows/VisualStudio/uniquake.vcxproj index f2d1136..4c5a31a 100644 --- a/engine/Windows/VisualStudio/uniquake.vcxproj +++ b/engine/Windows/VisualStudio/uniquake.vcxproj @@ -359,6 +359,7 @@ copy "$(SolutionDir)\..\SDL2\lib64\*.dll" "$(TargetDir)" + diff --git a/engine/Windows/VisualStudio/uniquake.vcxproj.filters b/engine/Windows/VisualStudio/uniquake.vcxproj.filters index 0552751..a989e19 100644 --- a/engine/Windows/VisualStudio/uniquake.vcxproj.filters +++ b/engine/Windows/VisualStudio/uniquake.vcxproj.filters @@ -241,6 +241,9 @@ uniquake + + uniquake +