#Ingame Console (Pseudo-Shell?)

1 messages ยท Page 1 of 1 (latest)

pale gale
#

warning
very long text
crappy code snippets
asking for opinions/advice

Hey, in the past I've created an ingame console with commands, for learning purposes and personal use, even if the 2nd one did never happen because the 'architecture' I've chosen feels kinda crap and not fun to work with at all.

I'm happy with the 'grammar' of my commands, for example to find a GameObject I'd write findGameObject("name") or to move a Transform I'd write findGameObject("name").getComponent("transform").setPosition(0, 0, 0), that's only an example and the actual use cases can be endless. I like the 'grammar', to me it feels intuitive.

The way I actually structure my Commands and resolve those sequences is what makes it hard.
There are basically 3 different subclasses that inherit from a CommandBase, which looks like

abstract class CommandBase
{
    public abstract string Identifier { get; }

    public abstract bool AreArgumentsValid(string[] arguments);
}

in my opinion, the first issue already appeared here, EVERY concrete implementation of a CommandBase has the responsibility to validate and later resolve an array of strings into arguments, weird enough.

Ok, but, my architectural weirdness continues, the 3 subclasses of CommandBase are
ActionCommand, ValueCommand, ModifierCommand.
All of them can take arguments as they'd like to, defined in their actual implementations via AreArgumentsValid(...

#

ActionCommands have no return type and no input type, an example could be addItemToPlayer("item").

ValueCommands have no input type, but a return type which can be worked with by using ModifierCommands, an example could be findPlayer().someModifierCommand() where findPlayer() represents a ValueCommand.

ModifierCommands have an input type as well as a return type, hence they can only be used after a ValueCommand, because ActionCommand has no return type to receive as input. They can also be chained.
An example could be findGameObject("name").getComponent("name").doStuff(), findGameObject( is a ValueCommand, it's return type is GameObject, getComponent( is a ModifierCommand that takes in this GameObject and returns the actual Component, hence the name modifier command, it modified the value type across the span of the command sequence. doStuff( would then be any other ModifierCommand that takes that specific Component as an input type. ModifierCommands don't need to actually have different return types, it could very well have the same input and return types, let's say we have two ModifierCommands with names setActive and setDeactive, this snippet would be totally valid: findGameObject("name").setActive().setDeactive().setActive()

Ok, very long text for only the basic explanation of different command types. But it was kinda necessary.

#

The architectural fun behind these subclasses begins..

abstract class ValueCommand : CommandBase
{
    public abstract Type GetReturnType(string[] arguments);
    public abstract object Execute(string[] arguments);
}

Don't forget that the actual implementations will also have the AreValidArguments( method in it..
So, when resolving a sequence, it would check if the arguments are valid, probably parsing them to some type or similar. It would then check what return type we get when using this arguments (this is to avoid having commands like getTransform, getRigidbody, and have a single getComponent instead), this process will probably do the same work on the arguments as the previous validation.. it then executes with this arguments, usually doing the same work a THIRD time! Oh damn have i made a mess back then ๐Ÿคฃ

More fun to come, the ActionCommand is fine since there's nothing in it expect a void Execute(), but there's still the ModifierCommand...
Anybody, at this point of my story, can guess my approach?
Yeah you probably got it, copy paste and duplicate the same crap that i have in my ValueCommand, but for an input type on top... LMAO pls don't judge me

abstract class ModifierCommand : CommandBase
{
    public abstract Type GetReturnType(string[] arguments);
    public abstract Type GetInputType(string[] arguments);
    public abstract object Execute(object target, string[] arguments);
}

Still, implementations have AreArgumentsValid(, hence eventually parsing those arguments not 3, but possibly FOUR times when resolving..

#

So, the overall architecture definitely sucks. Better don't ask for the parsing.

I'd like to get some opinions/advice on what a more suitable architecture would've been.
My ideas for the next attempt are

  1. Ditching the arguments for input types and return types, better do implementations for single types if 10 of them take as long as a single command with multi type support.

  2. Eventually putting everything that separates the subclasses from each other into the CommandBase and use an enum to describe how the command works

  3. Find a way to parse those arguments only once into a data structure that the commands can easily work with

If anybody read and understood all of that and would like to give me some input, I appreciate it!

#

Dear you who is reading this, please forgive me ๐Ÿ™
Formatting advice for readability is appreciated

#

I already looked up Lexers, Parsers etc, but I think it's kinda overkill because I don't have any statements, conditionals etc

half ice
#

Well, you're essentially making a programming language right now :)
Formatting tip, inline code, enclose the text between one set of backticks to produce inline code.

Here's some `inline` code
Here's some inline code

Formatting aside, that's pretty impressive. If I had to do this I would keep the same command system, but have your evaluations return a concrete type deriving from an abstract one, just like commands, but for "return values".
For example findObjectByName(s) would return a ObjectReturn (you name it), deriving from an abstract ReturnType class.
And for chaining commands I would store a list of them, well, a Dictionary<string, CommandCallDelegate> (stored on the abstract class) to be able to run any command by name.
Execution workflow incoming...

#

For example, a function that finds an object by name findByName. Returns a value of type ObjectReturn.

class FindByName : Command {
  
  public override ReturnType Evaluate(string[] args) {
    // ...
    return new ObjectReturn(...);
  }
}
pale gale
#

Aaaah, using something like a CommandReturnType class would definitely make it easier, I could then just check if blabla is TYPE

#

And yes, it's kinda like a programming language, but it'll only ever be a single line of expressions without any conditionals, statements etc

half ice
#

Even more automated, ReturnType would have a Dictionary that stores all functions that can be called on it, and that Dictionary is populated by the classes that inherit ReturnType

pale gale
#

For reusing values for example, i plan on having a static class acting as a cache, where I could do addToCache(findGameObject("name"), "key") and getFromCache("key").doStuff()

pale gale
#

In my current implementation i also had commands to just get properties by name, but it's kinda error-prone obviously XD

half ice
#

Yes, using your example calling getValue would return something inheriting from the abstract class (so with the Dictionary), and the interpreter will try to fetch and call someMethod by looking into the Dictionary

pale gale
#

Nice, I feel like I understand what we're talking about ๐Ÿคฃ ๐Ÿ˜Ž
I had another approach back then, where I had a ArgumentValidator class with an enum, that would receive a string and then confirm if it matches what the enum expects

#

The enum would be like int float ValueCommand etc and the Validator would then check if the string is parsable to any of them, in the case of ValueCommand for example it'd store a Sequence class for later use

#

Sequence is basically the first command, a list of it's arguments, a list of more Sequences that represent modifier and it's arguments, my execution syntax is as simple as

    sequence.Resolve();```
half ice
#

And of course, you need some way to pass yourself to the next call when you chain commands like these, so

override ReturnValue Call(ReturnValue callerReturnValue, object[] args)
{
  // the interpreter passes the function we just called as an argument, so you can access its Dictionary of available methods
}
#

Or that might better be done before the actual call

#

TryCall? Or Validate

#

Yeah that needs to be done before the call is made, because you don't have the type at this point, and it allows you to throw exceptions in case the function with a specific name isn't defined (present in the dictionary) for that type

pale gale
#

I'll definitely try an approach with ReturnTypes as their own class, that seems to be soooo much more scalable

#

They could just contain everything they want lol, a single class reference, primitive types, or even a whole reference to a system that's made out of several class references

#

Like a ReturnType GameplaySystem which could hold the UI, a Spawner, Player and so on, that would be kinda crappy of course, but it's a possibility

#

A method StartGameplay, contained in that GameplaySystem ReturnType may then be subscribed to the Dictionary and be called, resulting in interaction with the individual parts that make it up, showing UI, start spawning and so on, abstracted away into a single call that could be made by different commands as long as they can work with the ReturnType provided

half ice
#

That looks good to me, more scalable than before

pale gale
#

Definitely, and much more straight forward to define the types then parsing an array of strings for any dumb reason ๐Ÿคฃ
TYSM!

half ice
#

๐Ÿ‘

pale gale
#

Small Update, I've built a more manageable architecture for this now.

Pseudo Code

-abstract class ArgsParser
  --abstract bool CanParse(string)

-abstract class ArgsParser<T> : ArgsParser
  --abstract T Value

concrete implementations will assign Value when running CanParse, so the arguments parsing needs to be done only once and the value is accessible for later use, there could be implementations for parsing Commands with specific Input/Output types and so much more

-class CommandSignature
  --ArgsParser[] Arguments
  --bool CanParseAllArguments(string[])
-abstract class CommandValue
  --empty
-abstract class Command
  --abstract string Identifier
  --abstract CommandSignature[] Signatures
  --abstract Type OutputValueType
  --abstract Type InputValueType
  --abstract CommandValue Execute(int signatureIndex, CommandValue inputValue)
  --bool HasSignatureThatCanParseAllArguments(string[], out int index)
#

It's so much easier to work with, I could and probably will constrain my Commands to have <TInput, TOutput> where T : CommandValue instead of Type, but I'm already happy with the direction this is going
Also, by receiving the signature index at execution, it's really easy to work with the arguments, since we set them up by ourselves, we know which signatures has which arguments in which order, the only thing left to do is grabbing those values and work with them

half ice
#

Again, to avoid this thing from happening

instead of

Type

, but I'm already happy [...]
(notice how the code block takes the whole line)

You can inline code by enclosing in one set of backticks.
Raw

instead of `Type`, but I'm already happy [...]
Output
instead of Type, but I'm already happy [...]

This reduces vertical space by quite a lot actually, and makes sentences look more like sentences.

#

Apart from that, the code structure looks good