Flatfingers wrote:As your pseudo-official project manager, I have some relevant questions!
Thanks for the poignant questions as always Flat, since I'm working on all this code right now (especially ECS), I think answering in detail is actually as valuable to me as it is to you guys, so I will do so!
(EDIT: this post seriously got away from me....
)
Re: Physics/Collision Detection
That's a tough question. First, I'll disclaim that about 2 to 2.5 days were spent on that, so the week was pretty heavily-slanted towards ECS. Still, I will try to justify the "why now?" of it, as I can see how it might not seem that (1 - dot(physicsWork, practicalityDir)) < MIN_PRACTICALITY atm. I contend that it is close. Hats off to those who understand what I just said :V
So, to get broad-strokes of gameplay outlined, we can probably agree that we need at least
some collision. Lasers need to be able to hit other ships in order for combat to work at all. Scene raycasts need to be possible so that I can click on objects in-game. Real collisions (even coarse) would be nice but are not necessary for broad-strokes gameplay. Mesh raycasts would be really nice to have for shipgen algorithm, but again, not necessary for gameplay outlining.
So indeed, there aren't many cases where we really need colldet for gameplay prototyping. But there are, of course, a few (combat-related ones are the arguably the most important right now; combat simply can't be prototyped without some basic point test and raycast abilities). Now, the annoying and problematic thing about the physics engine: doing a highly-simplified version for the time being has serious drawbacks. Performance is probably the most important one, but there's also the way it affects gameplay. I can talk about this quite concretely due to the fact that I hacked together the simplest possible solutions for things that I required to make the LTDemo @ PAX and beyond work. Those hacks are still part of the demo. I see them every day, and every day it is reinforced to me that they're screwing with both the way the game works and the performance measurements thereof.
Take, for example, weapons & shooting. Lacking a broadphase engine, I can't actually detect projectiles hitting things without it becoming LTSlideshow. Even the cheapest possible test (point-sphere) will kill the perf in a battle of any real size, since we're talking O(n^2) for naive broadphase (or, to be more precise, O(n*m) where n = collidables and m = colliders, in this case n = numShips and m = numProjectiles). So how do I have combat working? By incredible hackery: when an AI shoots, the projectile knows the intended target. It will only check for intersection with that target (and by intersection, I mean the super-cheap point-sphere, not real intersection). So now it's just O(m). Ramifications: projectile hits are really reduced to 'did my projectile get within x meters of my specific target?', where x is an obscene over-estimation, because bounding spheres are bad, especially when the object is significantly larger along a certain spatial axis (like....most ships, which tend to be bigger on the Z (forward-backward) axis). Projectiles obviously pass through everything that's not their intended target, meaning combat AI doesn't have to be smart *at all*, because it can shoot right through all the asteroids it wants and still nail the poor guy on the other end. You see, the problem here is that, were I to start prototyping non-braindead combat AI, I would be prototyping under completely different game rules than will be reality. It wouldn't be throw-away code, but it'd be close...because, at least in LT, combat AI that doesn't consider the environment whatsoever isn't really worth a damn.
I haven't really put the nail in the coffin yet. One might still imagine prototyping gameplay under the restrictions I mentioned. But, now consider another demon: when I perf test battles, I'm not getting accurate readings. Even this hackery is terribly slow compared to a good solution, so it's hard for me to tell whether a big battle with lots of ships and projectiles in motion is lagging because of sim logic, rendering, or this placeholder physics. Sure, I can pull it apart piece-by-piece if I want to get a real measurement. But I can't just set up battles and get a good idea of how perf will
actually be (this is also true ATM because of the lack of an ECS in C; I've spoken at lengths about how per-frame object updates in Lua are killing me). So, the whole time I'm prototyping, I'm experiencing a dramatically less-performant version of LT. That's not fun for me; more importantly, it can cause me to spuriously believe that other systems are too slow when, in reality, everything would be silky-smooth with a decent physics engine and a performant ECS. That translates to higher-than-necessary anxiety and, possibly, to wasted time investigating bottlenecks that won't actually be bottlenecks. During a time when I'm trying so hard to put FPLT in the grave, it's quite detrimental to be working in the midst of placeholder systems in performance-critical areas.
Oh, and it's even worse for the player. I've seen several people get confused by this while playing the demo. Obviously, my hack doesn't work so well for the player because...unlike the AI, I can't read the player's mind and know what ship he wants to hit. So I do the next best thing and assume that he's trying to hit the thing closest to the reticle. There are a lot of situations where this works poorly (and I've seen 'em). So again, it's not just that we have a placeholder system, it's that the placeholder system is deeply affecting the gameplay in such a way that things are
fundamentally different from how they would be in LT. Not a great situation, even for broad-strokes prototyping. Of course, I've only talked about combat so far, but I'm sure I will run in to similar baddies if I were to start prototyping other types of gameplay.
Take all that, then take into account the fact that a 'good enough for now, easily-refinable later' solution is very nearly within reach, and sprinkle in how badly it breaks immersion for me when I zoom through asteroids, and you've got a clear answer:
yes, it makes sense to be working on a solution that is easy enough to get to quickly, but will be performant and robust enough to give me a much better approximation of gameplay when I do broad-strokes. Don't get me wrong, I am
not going to spend time making it 'UberPhysics 9000' right now. A simple broadphase and a good-but-not-microoptimized narrowphase are within reach and will make a big difference in prototypability!
The defense rests
(I didn't mean to write that much. Whoops.
)
Re: LT Scripting for the Non-Josh
Short answer: basically as easy as modding can be made to be, realistically. Which means easy, provided you can get a cursory understanding of Lua.
Long answer: easy to hard, depending on how 'deep' the mod is. Let me try to give some mock-real (as in real code, not necessarily exactly how it will be though) examples. Here's an example of a mod template that might be included with the game...'modtemplate.lua'
Code: Select all
-- This is a simple mod script template. It's for us simple folk.
-- Returns basic information about your mod: name, author, version string, description, dependencies (if applicable)
function getModInfo ()
return {
name = "My First Mod",
author = "John Smith",
version = "1.0.0",
description = "This is a Limit Theory mod!",
}
end
-- Called before scripts are loaded, during runtime compilation of game-related ECS code
-- Here you can change the low-level data and code used for implementing gameplay
-- WARNING: This is a low-level modding layer intended for mods that deeply change the game; most should not do anything here
-- You can, for example:
-- Add fields to datatypes (e.g. add a new property to items)
-- Define new C functions that can be called from your Lua code but have direct access to ECS data
-- Override the default ECS C functions
-- Write high-performance helper functions if your Lua scripts need more speed
function onCompile (ecs) end
-- Called during engine initialization, after the default LT scripts are loaded but before any game code runs.
-- Here you can change constant data, re-bind default functions to change scripted behavior, add new behaviors, etc.
function onLoadScripts (scripts) end
Notice that there are two fundamental levels at which you can mod the game. Most mods will require only the second level, which is purely dealing with changing and extending the default Lua scripts. Notice also that the architecture of a mod is fundamentally 'diff-like.' Instead of a mod being like a file-replacement, mods are just scripts that get called when they're loaded. Due to Lua's design, this gives you all the same power as file replacement, but this architecture makes mod conflicts far less likely and far more manageable. Even mods that require the first (low-level) modding layer can work together with no problem due to how the ECS is structured. You could have several mods active that add several new properties to the Item class in the game and they would work perfectly as long as no mods try to use the same name for a new property. The engine handles 'merging' in a very natural way at both modding layers. As a last-resort you
can still override files, but this will be discouraged for scripts (it makes sense for other asset types, though). In fact, the packaged mod file will know which files the mod alters, so LT can automatically notify of conflicts. It can also automatically show warnings for mods that override scripts. I plan to have a full set of mod 'warnings' that give players information about a mod (for example, a mod that accesses the networking API will be branded with a big fat "Warning: This mod is requesting access to networking functions. It may attempt to communicate over the network. Only enable the mod if it is clear why it should require such access.")
Without further adieu, a simple example mod that removes drag from the game:
Code: Select all
function getModInfo ()
return {
name = "Dragless",
author = "Flatfingers",
version = "1.0.0",
description = "Removes all artificial space-drag. Happy Newtonian piloting!",
}
end
function onLoadScripts (scripts)
scripts.constants.linearDragMult = 0
scripts.constants.angularDragMult = 0
end
Very simple since drag is a constant. A better version would probably also implement a velocity limit, although that's more involved.
Let's try something a bit more complex, adding a new behavior to NPCs that make them move out of the way of their owner:
Code: Select all
function getModInfo ()
return {
name = "Out of My Way!",
author = "Flatfingers",
version = "1.0.0",
description =
"You're a pilot of importance...at least to your subordinates. They should move the frak out of the way when you're trying to get by.\n" ..
"Especially useful when you're in formation with hundreds of fighters.",
}
end
-- Higher value means stronger repulsion field from the player's ship
local kRepelStrength = 1.0
-- Higher value means repulsion field is attenuated more strongly with distance
-- 2.0 is an inverse square field like gravity & electrostatics
local kRepelPower = 2.0
local function moveAway (asset)
local toPlayer = getDirection(asset, getPlayerShip())
local distance = getDistance(asset, getPlayerShip())
asset:applyForce(-toPlayer * (kRepelStrength / (distance ^ kRepelPower)))
end
local function attach (asset)
if asset:hasComponent(Component.Motion) then
asset:addListener(Event.InProximity(asset, getPlayerShip()), moveAway)
end
end
function onLoadScripts (scripts)
scripts.callbacks.onPlayerAcuireAsset:add(attach)
end
Note that I could have separated my script logic out into a new lua file and loaded it in my main script file instead. Note also that, again, this mod is virtually guaranteed to not conflict with any others. New functionality is (often) added via the event system. Old functionality can typically be modified in a similar manner. Finally, note that this is legitimate Lua code and is actually representative of what a real, working LT mod might look like. It's not even a trivial mod: it adds a handy repulsion field to the player's ship so that his own ships move out of the way. Sure, it's simple: the field is actually physical force; it would be more elegant to have the pilots reposition their ships instead. But this is a demo of a simple,
practical, quality-of-life mod
It could also be fun to make the constant very high and push big fleets of ships around like a ship-shepherd (that is...a...Commander Shepard
).
Finally, the
pièce de résistance, an ancestry mod, requiring use of the low-level api:
Code: Select all
function getModInfo ()
return {
name = "Ancient Ancestry",
author = "Talvieno",
version = "1.0.0",
description =
'"Josh Parnell? As in...THE Parnells? The same ones that own BumbleBoom Projectiles?" Now you can find out.\n' ..
"All NPC players keep track of their parents; their ancestry can be explored freely for the curious (or bored) pilot."
}
end
function onCompile (ecs)
local GenderT = ecs:addEnum('Gender')
GenderT:add('Male')
GenderT:add('Female')
-- Look, it's a game, for the purposes of AI reproduction I need gender to be either male or female, sorry :P
local PlayerT = ecs:getType('Player')
PlayerT:addField(GenderT, 'gender')
PlayerT:addField(CType.RefPointer(PlayerT), 'father')
PlayerT:addField(CType.RefPointer(PlayerT), 'mother')
end
local function recordAncestry (player, colony)
local colonists = colony:getSpawnedPlayers()
local father, mother
local rng = getRNG()
while not (father and mother) do
local parent = colonists:chooseRandom(rng)
if parent.gender == Gender.Male and not father then father = parent end
if parent.gender == Gender.Female and not mother then mother = parent end
end
player.father = father
player.mother = mother
if rng:chance(50) then player.gender = Gender.Male
else player.gender = Gender.Female
end
local function buildMenu (player, menu)
menu:getSection('BasicInfo'):addPropertyLabel('Gender', tostring(player.gender))
if player.father and player.mother then
local section = menu.addSection('Ancestry')
section:addLabeled('Father', section:addTextButton(player.father.name, function () createInfoMenu(player.father) end))
section:addLabeled('Mother', section:addTextButton(player.mother.name, function () createInfoMenu(player.mother) end))
-- TODO: Use NodeTree widget to show a graphical family tree!
end
end
function onLoadScripts (scripts)
scripts.callbacks.onAIPlayerCreate:add(recordAncestry)
scripts.menu.info.player.callbacks.onCreate:add(buildMenu)
end
Boom. There you have it. Legitimate example showing how low-level ECS data structures can be modified to painlessly and efficiently accommodate a mod that, for instance, needs to keep track of parent information (plus gender in order to generate that information). Also some mockup-real-code showing adding information to the UI. Wwhen the player pulls up the information UI for an AI player, they will now see a text label giving the player's gender, as well as a new sub-section that shows the mother and father, which are clickable links that go to their respective info menus...allowing a player to quickly browse back through time and look at the ancestry of LT AI players!
I hope this answers the sub-questions via example. To clarify further, answering your bullet points in-order:
- Modders will need at least a basic understanding of things like ECS, the event system, the built-in basic functions, etc. I will provide documentation on all of the above. Even better, I will provide as many mod examples as possible that demonstrate the 'right way' to do various things, straight from the horse's mouth.
- Functionality can be dramatically affected. I try to design the API so as to minimize unintentional effects. As usual, the architecture is designed to make performance something that most modders don't have to worry about if they're following the right way of doing this (i.e., using the event system rather than trying to run every frame). Mods that do things that require extreme performance will have to be more careful, as they will be defining new C code. Most mods won't need that (like, the vast vast vast majority; but it's there because it's how I will lay down a lot of the ECS fundamentals that don't belong in the LT Core yet still require max perf). Even then, I provide an API for adding new low-level functionality (i.e., compiled C) that handles as much of the 'nasty' stuff as possible (in other words, you're not writing full C files yourself). So even the most beastly of mods (I'm looking at you, LT: Multiplayer) will still only require minimal raw C in the perf-critical parts; I provide many mechanisms to avoid having to resort to this
- Some scripts will be considered 'not advisable to change,' but anything that isn't baked into the LT Core is, by definition, subject to change. Code that absolutely should never change is compiled into the library. Some low-level Lua support scripts may be tagged as 'non-overridable.' In general, though, I will rely on modders to be sensible: you don't really get much out of breaking everything. But, ideally, you'd need to be trying to break things to really break them. (I SAID IDEALLY )
- That kind of thing would likely be enforced by path. Off-limits scripts would be in a different folder that the engine internally considers non-overridable when it loads mods. Alternatively, I can bake those scripts as Lua bytecode and keep them in binary format, so that there would be no 'off-limits' lua files.
- Modders have access to virtually every function in the LT Core engine + everything in script so...yep, anything I use to debug / profile will be equally available to modders.
- Multiple scripts / mod assets are packaged in a folder that mirrors the folder structure of the LT assets. A simple mod like above would just be a folder with a single file, 'mod.lua' in it. A more complex mode might have 'myMod/mod.lua', 'myMod/tex2D/coollogo.png', 'myMod/shader/fragment/metal.glsl', 'myMod/bindlogo.lua' ...for example. The folder is then fed into a simple tool that I provide that turns it into a single, compressed .ltm file (or whatever extension) that includes the compressed directory structure as well as metadata about the mod. Simple, simple, simple.
Whew. Back to coding. FWIW that WAS development time. Especially those mod examples. They helped me think through a lot of things! So...thanks for the questions