Spoiler:Project Absolam is no longer available on the ZDoom Forum. Thank you.
Project Absolam?
Project Absolam v0.1.1
I started this project about 4 years ago, so honestly I don't remember why I named it that, but it's a codename.
Project Absolam started out as a question: "can you make a game like Diablo in GZDoom?" At the time I said no because of the limited lighting system; I wanted to use models before dynamic lights understood vertices. So the project got shelved until I got laid off due to the slowdown the pandemic caused in manufacturing. The shop I worked at cut 50% of its 2nd shift in an afternoon; notice the past tense there. Anyway, bored, a few days into quarantine I started looking at old projects and thought I'd give Project Absolam a look. And my answer to the original question is now a resounding, yes!
Tools for the Crawling of Dungeons!
The circles...oh the circles!
The Camera
Besides Diablo, Dungeon Siege should also be mentioned. I haven't played it, I should, but my knowledge of it comes from Ross Scott. From what I have seen, the behavior of Project Absolam's camera is similar. While offering a birds-eye view, it remains attached to the player and will face the same direction as the player at all times. Or maybe it's a bit like a Bethesda game camera. What's important is that the behavior can be customized.
Features:The Crosshair
- Pitch control
- Height adjustment - can be keybound too
- This is sort of a gimbal adjustment. The camera is not actually attached to the player, instead the camera is attached to a secondary point that orients itself behind the player. This allows the camera to swing vertically 180 degrees along an arc to change the height of the camera while maintaining its pitch.
- Sync with players look
- Can be toggled between 1st and 3rd person
Also called the reticule, this glob of code and models was conceived of as a way to help target enemies from an unusual observation position.
Features:The Extension System
- Customizable search radius
- Lock until death behavior - the crosshair won't go idle until the current target is destroyed
- Customizable aim tolerance angle - how far from the edge of a target can the player aim before the crosshair goes idle
- Projectile leading - secondary reticules are created which indicate where to aim to hit a target with a projectile firing weapon. Does literal ballistic leading calculations.
- Interface for mod weapons and monsters - this thing only works with vanilla stuff right now but there is a way!
- Actually a functioning proof-of-concept of the extension system
Basically the player is responsible for spawning everything that this project does - or at least the parents of everything. Rather than hard code a bunch of A_SpawnItemEx calls into the player, Project Absolam uses the awesome power of event handlers to "register" modules with the core system through a net event. This is done for both spawning the object(s) and outputting debug info.
Plans for the ProjectDownloads
- One thing I skipped over is variable arguments for the extension system. Everything is there but nothing uses them, so I thought why not use them to control the A_SpawnItemEx arguments? The original intent was left undefined for modules to do with as needed, so likely I'll set up room for flexibility there.
- I still need to finish filling out the weapon and monster lists for the other vanilla games. Right now only Doom is done.
- Decide what to do about flying monsters - I haven't tested this at all yet.
- Some sort of yaw control for the camera
- Improved debug output, it's a functional mess
- Better color controls for debug messages - I gotta be missing an enum somewhere! Help?!
Download the full packages from my Google Drive. The Github repos only contain the source code.What in the World?
- Camera v0.1.1
- Crosshair v0.1.1
- World v0.1
Project Absolam Repo
Old Versions:- Camera v0.1
- Crosshair v0.1
The world package is the files for the map you see in the demo video. They aren't necessary but they're available if anyone is interested. Do as you please with the files, but credit please, I did make those models.
Load Order and Requirements
Project Absolam is also the origin of Tooltips. The camera has enough options that might be confusing enough that I wanted tooltips for them. So Tooltips is required otherwise Project Absolam will not load.
Load Order
Project Absolam packages need loaded in the correct order otherwise there will be problems. Use a mod loader such as ZDL to make this easy. Below is the correct order for Project Absolam Packages.Compatibility With Vanilla Games and Mods
- Tooltips
- the World
- the Camera
- the Crosshair
- any other extension module
- any other mod reliant on Project Absolam
Vanilla Games:
Right now only Doom is supported, but the framework is there to support the other games.
Mods:
Not without compatibility patches. Project Absolam creates its own player class, therefore any other mod doing the same will conflict. Also, the projectile leading system also makes some assumptions about both vanilla weapons and monsters, which any mod messing with will likely screw up the ballistics math. Finally, the same system does not have any way to gather the required information about weapons and monsters added by mods unless the provided interface is used.
Spoiler: Compatibility Info for ModsUse, Intent, Whatever
If you need to modify the player:Spoiler: Inherit from PA_PlayerWeapon and Monster Interfaces:
- First inherit your own player class from PA_Player.
- You'll need to call the Super for PostBeginPlay and Tick on your new player class so that the code defined in PA_Player is executed. All of the variables can be ignored.
Code: Select all
class PA_Player : DoomPlayer { float CamOriginZ; int iLoadedExtensionCount; bool bCanUseCamera, bCanLoadExtensions; Actor _PACam; PA_Camera_ControlHandler camHandler; // This method is a special addon for the crosshair, but it's useful anyway. bool PA_CheckSight(Actor a_target = null, int flags = 0) { if (a_target) return CheckSight(a_target, flags); else if (target) return CheckSight(target, flags); else return false; } // Imports all of the DoomPlayer class defaults override void PostBeginPlay() { Super.PostBeginPlay(); if (GetAge() < 1) { camHandler = PA_Camera_ControlHandler(EventHandler.Find("PA_Camera_ControlHandler")); iLoadedExtensionCount = 0; if (camHandler) bCanLoadExtensions = true; else bCanLoadExtensions = false; Console.Printf("PLAYER(#%d): - Initializing PA Camera. Hang on...", PlayerNumber()); CamOriginZ = 64; // Spawn Camera at the bottom of its scroll [bCanUseCamera, _PACam] = A_SpawnItemEx("PA_Camera", -Cvar.GetCVar('pavar_CamOriginX', players[PlayerNumber()]).GetFloat(), 0.0, CamOriginZ, 0.0, 0.0, 0.0, 0.0, SXF_SETMASTER, 0, 0); } } override void Tick() { Super.Tick(); if (bCanUseCamera) { if (!CVar.GetCVar('pavar_FrstCam', players[PlayerNumber()]).GetBool()) SetCamera(_PACam); else SetCamera(self); } // Extensions if (bCanLoadExtensions && iLoadedExtensionCount < camHandler.getTotalExtensions()) { for (; iLoadedExtensionCount < camHandler.getTotalExtensions(); iLoadedExtensionCount++) { class<Actor> cls = camHandler.getExtension(iLoadedExtensionCount); if (cls && PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_ExtPlayerToggles[PlayerNumber()]) { if (PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).bSpawnVargs) { float xofs = 0.0, yofs = 0.0, zofs = 0.0, xvel = 0.0, yvel = 0.0, zvel = 0.0, ang = 0.0; int fail = 0, sid = 0; for (int i = 0; i < PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs.Size(); i++) { switch (i) { case PA_Util.XOffset: xofs = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.YOffset: yofs = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.ZOffset: zofs = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.XVelocity: xvel = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.YVelocity: yvel = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.ZVelocity: zvel = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.Angle: ang = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToDouble(); break; case PA_Util.FailChance: fail = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToInt(); break; case PA_Util.ThingID: sid = PA_ExtData(camHandler.dar_ExtData[iLoadedExtensionCount]).dar_VArgs[i].ToInt(); break; default: console.printf("PROJECT ABSOLAM EXTENSION ERROR !! - Somehow got an unknown spawn argument!"); break; } } A_SpawnItemEx(cls, xofs, yofs, zofs, xvel, yvel, zvel, ang, SXF_SETMASTER, fail, sid); } else A_SpawnItemEx(cls, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SXF_SETMASTER, 0, 0); } } } } }
Spoiler: WeaponsKeep in mind this only needs done for weapons that fire actual projectile actors. Think rocket launcher or plasma gun.
- The interface is in the form of an abstract class that needs to be inherited from.
- Then set the following properties in the Default block:
- bool, ProjectileWeapon - must be true for weapon to be added to system
- string, ProjectileAmmoName - this is the ammo class name used in the Weapon.AmmoType(1/2) properties of the weapon.
- float, Projectile_Speed - this is the Speed property of the actual projectile class fired by the weapon.
Code: Select all
class PA_WeaponUtil : weapon abstract { bool ProjectileWeapon; property ProjectileWeapon: ProjectileWeapon; string ProjectileAmmoName; property ProjectileAmmoName: ProjectileAmmoName; float Projectile_Speed; property Projectile_Speed: Projectile_Speed; Default { PA_WeaponUtil.ProjectileWeapon false; PA_WeaponUtil.ProjectileAmmoName ""; PA_WeaponUtil.Projectile_Speed 0.0; } }
Spoiler: MonstersThe Extension System:
- Just like weapons, the interface is an abstract class that monsters need to inherit from.
- The TickSpeed property is an approximation of how many map units a monster will move per game tick.
- The math to calculate this is: speed / frame duration of A_Chase
- For example: the ZombieMan's Speed is 8 and the frame duration of A_Chase is 4, so 8 / 4 = 2. The TickSpeed property would be set to 2.
- The TickSpeed property is a float so that fractional values are accounted for, so include approximate fractions (.66, .33, etc.)
Code: Select all
class PA_MonsterUtil : actor abstract { float TickSpeed; property TickSpeed: TickSpeed; Default { PA_MonsterUtil.TickSpeed 0.0; } }
Spoiler: How to Load a Module into Project Absolam
What needs done is pretty simple. A module is at least two classes, the module actor(s) and an event handler. The event handler has one job, tell the Project Absolam Control Handler, the master event handler, about the module. This is called "registering" the module.This event handler does one thing and one thing only, when it is registered, it sends a network event. This event is in the form of a formatted string.Code: Select all
class PA_Crosshair_Registrar : EventHandler { override void OnRegister() { string eversion = "0.1"; //Send the event name, the extension's "nice name", the actor name, and the toggle CVar (must be boolean!!), each separated by a colon ( : ) SendNetworkEvent(string.Format("RegisterExtension:Project Absolam Crosshair Extension v%s:PA_Crosshair:pavar_Ext_UseCrosshair", eversion)); } }
The string format:Variable Arguments:
- command - this tells the system what to do; like a function call
- nice name - this is used in debug output, it's just what to call the extension
- actor name - the actual parent class name for the module
- CVar name - modules are expected to be toggled by the player therefore a boolean CVar name is expected here
- variable argument control boolean
- further vargs - variable arguments are designed to work in the same was a variable arguments in C. Unlike C where data type must be controlled for, here variable arguments are stored as strings. There are no restrictions on how many args you can supply.
Argument 5 - the variable argument control boolean - is a string literal. This means you write in the word "true" or "false" to assign a value to the actual boolean this argument corresponds to.You may also format the string as you are sending the net event, as shown in the example to make updating the version number quick and easy.
- If the boolean is set to true, any arguments supplied after will be used as arguments for the A_SpawnItemEx call in the player which spawns the extension actor. Up to 9 arguments are supported in this use case, all arguments except for flags, which currently preserves the setting of the master pointer.
- If the boolean is set to false, any arguments supplied after are not used by Project Absolam and are at the discretion of the extension to do with as needed.
- There are no limits on how many arguments can be supplied. There is potential for abuse of this, but I won't put in a limit unless I hear about it.
Spoiler: Adding a Module to the Control Handler for Debug Output
The PA_Extension_Base class offers the interface for debugging as well as other members that are useful.The real meat and potatoes of this is really only two things: find the Control Handler, and pushing the actor to the debug array - the actor does need to be a descendant of PA_Extension_Base.Code: Select all
class PA_Extension_Base : Actor abstract { Array<string> dar_DebugStrings; // Strings that need pushed to the screen, not the console Array<int> dar_DebugColors; bool bCanAddToDebug; Actor a_Player; // This needs to stay an Actor so that code casts to the right descendent PA_Camera_ControlHandler camHandler; }
Every tick the debug strings are pushed to the array, the array is also processed by the Control Handler, which pulls the strings into the string queue and empties the array. With a background in C and C#, ZScript's dynamic arrays are like candy. I just treat them like List<T>. The flipping of bCanAddToDebug is needed. The Control Handler looks at that to know an actor in it's debug list had added it's messages to its own pool.In this case, the strings are formatted based on the result of an inline conditional check of the CVar they represent. Also, _PA_Debug_Message, or some sort of equivalent is required for each actor, at the moment. It's on the feature list to internalize that to the base class instead.Code: Select all
// This is the actual class implementation class PA_Crosshair : PA_Extension_Base { // // Initialization // override void PostBeginPlay() { if (GetAge() < 1) { // Extensions can have debug output by registering with the Control Handler's dar_DebugExtensions array. // Note that calling PushNote is just for debugging. camHandler = PA_Camera_ControlHandler(EventHandler.Find("PA_Camera_ControlHandler")); if (camHandler) { uint crossIndex; crossIndex = camHandler.dar_DebugExtensions.Push(self); Console.Printf("CROSSHAIR - Registered with the Camera Control Handler for debug output. It should take note."); camHandler.PushNote(crossIndex, "Crosshair", false); } else Console.Printf("CROSSHAIR: - ERROR! - Crap...something went wrong. Unable to find Camera Control Handler!"); } } override void Tick() { Super.Tick(); _PA_Debug(); } private void _PA_Debug() { if (bCanAddToDebug) { _PA_Debug_Message(string.Format("DEBUG - Crosshair TID: %d\n- - -", self.tid), 0); _PA_Debug_Message(string.Format("Lock Target : %s", CVar.GetCVar('pavar_Ext_Crosshair_DestroyLock', players[a_Player.PlayerNumber()]).GetBool() ? "On" : "Off"), CVar.GetCVar('pavar_Ext_Crosshair_DestroyLock', players[a_Player.PlayerNumber()]).GetBool() ? 8 : 3); _PA_Debug_Message(string.Format("Projectile Leading : %s", CVar.GetCVar('pavar_Ext_Crosshair_ProjectileLeading', players[a_Player.PlayerNumber()]).GetBool() ? "On" : "Off"), CVar.GetCVar('pavar_Ext_Crosshair_ProjectileLeading', players[a_Player.PlayerNumber()]).GetBool() ? 3 : 8); bCanAddToDebug = false; } } // // -- This is how a debug message makes it onto the screen. // -- The string argument is the text, the int is the text color. // private void _PA_Debug_Message(string message, int color) { dar_DebugStrings.Push(message); dar_DebugColors.Push(color); } }
Project Absolam isn't done, as you can see by the feature list. I haven't even taken time to make a nice demo vid like I usually do, I just slapped up the quick capture I made for the Editing post I'd made about crazy angles and trigonometry I'm really bad at. Still pretty much shows what all this does.
So I'm not going anywhere fast with this project so I'm making it available to anyone else who might be interested in any of it. All of the plans for the project are on a "when I get to it" sort of timescale. That's it, hope you, yes you, like or hate or just passively grunt at Project Absolam, idk, it's a thing, here it is. Bye!
Spoiler: Changelog
- 23-07-2020 : Uploaded proper demo video.
- 04-08-2020 : Updated to v0.1.1, fixed the post to reflect the current project functionality. Variable args work, removed the camera TID, and a few minor fixes.
My projects can be found on my GitHub.