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;