diff --git a/Assets/Scripts/Game/Entity.cs b/Assets/Scripts/Game/Entity.cs index 40e5201..d8dfde5 100644 --- a/Assets/Scripts/Game/Entity.cs +++ b/Assets/Scripts/Game/Entity.cs @@ -12,6 +12,7 @@ public class Entity private SkinnedMeshRenderer skinnedMeshRenderer; private AliasModel aliasModel; + private GameObject brushModel; private GameObject worldModel; public Entity(int entityNum) @@ -33,6 +34,8 @@ public class Entity public void ClearModel() { aliasModel = null; + brushModel = null; + worldModel = null; Object.Destroy(meshFilter); Object.Destroy(meshRenderer); @@ -52,6 +55,13 @@ public class Entity UpdateAnimation(0); } + public void SetBrushModel(GameObject brushModelGO) + { + ClearModel(); + brushModel = brushModelGO; + brushModel.transform.SetParent(gameObject.transform); + } + public void SetWorldModel(GameObject worldModelGO) { ClearModel(); diff --git a/Assets/Scripts/Game/GameAssets.cs b/Assets/Scripts/Game/GameAssets.cs index 713c5f0..015dd7d 100644 --- a/Assets/Scripts/Game/GameAssets.cs +++ b/Assets/Scripts/Game/GameAssets.cs @@ -65,6 +65,21 @@ public class GameAssets brushModels.Add(brushModel); } + public bool TryGetBrushModel(string name, out BrushModel brushModel) + { + foreach (var model in brushModels) + { + if (model.Name == name) + { + brushModel = model; + return true; + } + } + + brushModel = null; + return false; + } + public void SetWorldModel(BrushModel brushModel) { worldModel?.Dispose(); diff --git a/Assets/Scripts/Game/GameState.cs b/Assets/Scripts/Game/GameState.cs index e426f93..b1a0016 100644 --- a/Assets/Scripts/Game/GameState.cs +++ b/Assets/Scripts/Game/GameState.cs @@ -30,11 +30,11 @@ public class GameState // The first sub-model contains all of the static geometry var subModel = worldModel.GetSubModel(0); - var subModelGO = CreateWorldGameObject(subModel); + var subModelGO = CreateBrushGameObject(subModel); subModelGO.transform.SetParent(worldGameObject.transform); } - private GameObject CreateWorldGameObject(BrushModel.SubModel subModel) + private GameObject CreateBrushGameObject(BrushModel.SubModel subModel) { var subModelGO = new GameObject(subModel.Name); @@ -104,6 +104,26 @@ public class GameState entity.SetAliasModel(aliasModel); } + public void SetEntityBrushModel(int entityNum, string modelName) + { + var entity = GetOrCreateEntity(entityNum); + + if (!uq.GameAssets.TryGetBrushModel(modelName, out var brushModel)) + { + Debug.LogWarning($"Unknown brush model name: {modelName}"); + return; + } + + var brushModelGO = new GameObject(brushModel.Name); + for (int i = 0; i < brushModel.SubModelCount; ++i) + { + var subModelGO = CreateBrushGameObject(brushModel.GetSubModel(i)); + subModelGO.transform.SetParent(brushModelGO.transform); + } + + entity.SetBrushModel(brushModelGO); + } + public void SetEntityWorldModel(int entityNum, int subModelNum) { var entity = GetOrCreateEntity(entityNum); @@ -117,7 +137,7 @@ public class GameState // TODO: these relatively complex world game objects are going to get destroyed and re-created all the time // as the player moves through the map and moves in and out of range of these entities. This can and should // be done more efficiently by creating the game objects only once and enabling/disabling them on demand. - var worldModelGO = CreateWorldGameObject(subModel); + var worldModelGO = CreateBrushGameObject(subModel); entity.SetWorldModel(worldModelGO); } diff --git a/Assets/Scripts/Modules/GameModule.cs b/Assets/Scripts/Modules/GameModule.cs index 64ba3ea..72b0720 100644 --- a/Assets/Scripts/Modules/GameModule.cs +++ b/Assets/Scripts/Modules/GameModule.cs @@ -34,8 +34,7 @@ public partial class GameModule if (modelName.EndsWith(".bsp")) { - // TODO: non-world brush model - uq.GameState.GetEntity(entityNum)?.ClearModel(); + uq.GameState.SetEntityBrushModel(entityNum, modelName); return; } diff --git a/Assets/Scripts/Support/AliasModel.cs b/Assets/Scripts/Support/AliasModel.cs index c6b285c..c6a0c1f 100644 --- a/Assets/Scripts/Support/AliasModel.cs +++ b/Assets/Scripts/Support/AliasModel.cs @@ -91,6 +91,10 @@ public class AliasModel ImportGroupFrameAnimations(poseVertices, indices, uvs); } + /// + /// Single frame animations are used for dynamically animated entities. The game logic determines which frame is displayed at which time. + /// In Unity we still need to identify groups of frames that form a single animation sequence, so that we can smoothly loop animations when interpolation is enabled. + /// private void ImportSingleFrameAnimations(QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs) { Mesh mesh; @@ -122,6 +126,10 @@ public class AliasModel animationMeshes.Add((startFrame, mesh)); } + /// + /// Grouped frame animations are used for entities that animate automatically in an endless cycle, e.g. flames. + /// They are set up once and then the renderer ensures they are updated at a steady rate. + /// private void ImportGroupFrameAnimations(QTriVertex[][] poseVertices, ushort[] indices, Vector2[] uvs) { for (int frameIdx = 0; frameIdx < header.numFrames; ++frameIdx) diff --git a/engine/Quake/cl_parse.c b/engine/Quake/cl_parse.c index 1577c13..1bcc2f1 100644 --- a/engine/Quake/cl_parse.c +++ b/engine/Quake/cl_parse.c @@ -393,6 +393,15 @@ void CL_ParseServerInfo (void) R_NewMap (); + for (i = 1; i < nummodels; i++) + { + if (cl.model_precache[i] == NULL || cl.model_precache[i] == cl.worldmodel || cl.model_precache[i]->type != mod_brush || cl.model_precache[i]->name[0] == '*') + continue; + + UQ_GL_UploadBrushModel(cl.model_precache[i]); + CL_KeepaliveMessage(); + } + //johnfitz -- clear out string; we don't consider identical //messages to be duplicates if the map has changed in between con_lastcenterstring[0] = 0;