I remember @oprypin helped me a while back with game commands using Enums instead of strings.
I’m at the point where there are quite a bit of game commands. Not just 20, but far, far more. I’ve gotten to the point that a command has its own sub-commands. This got me thinking why am I sending over the command as a string, when it could be an int?
Mock-up without Enums:
require "socket"
require "json"
class Client
property socket : TCPSocket?
def send(msg, cmd = "SERV")
new_message = {cmd: cmd, message: msg}.to_json
#socket.write_bytes(new_message.bytesize)
#socket.send new_message
pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd} to the client..."
end
end
p = Client.new
p.send ({portal_idx: 1}), "UPDATE_ENTITY"
Mock-up using Enums (if I am doing it correctly?):
require "socket"
require "json"
enum Command
XFER
UPDATE_ENTITY
TRAVEL_TO
end
class Client
property socket : TCPSocket?
def send(msg, cmd = "SERV")
new_message = {cmd: cmd, message: msg}.to_json
#socket.write_bytes(new_message.bytesize)
#socket.send new_message
pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd} to the client..."
end
end
p = Client.new
p.send ({portal_idx: 1}), Command::UPDATE_ENTITY
Positives of using Enums:
Eases the mind of a developer (worry less about all the added string commands, and possible performance degradation). This will make the developer feel more free, and NOT stunt their thoughts about new features that could be added to the game.
Less bandwidth will be used since you are only sending an int value, not an entire string.
Faster when reading game commands on the client/server, because the if conditions only check int values, not a 13-character string.
Negatives of using Enums:
The client (Godot in this case), has to have the same Enums as well. So when you add a game command on the server, you need to update the Enums on the client. Now, this is not really a big issue for me, since it’s a client->server relationship. However, it is an added step a developer must do.
Using Command::ENTITY_UPDATE is more verbose syntactically than "ENTITY_UPDATE". However, at the same time… it’s really not because it maps to an integer value, which is far lessverbose than a 13-character string. So… it’s actually a positive?
Am I thinking correctly about Enums? Or am I delusional?
I have no one to talk to about it, so I don’t really know if it makes sense. What are your thoughts?
I think there is obviously not a difference in performance on a small scale. However, if your commands are running in a 20-30hz game loop, and more players are online, this will increase the times a command is executed (more comparison executions). I’m sure there comes a point where comparing a 13-character string is slower than comparing just an int value.
I’m not sure on how to test this though, @Blacksmoke16 might be able to help with a nice benchmark script. I’ll try to create one, but I usually do them wrong
Enums are essentially int values mapped to a name. They are best used to represent specific constant values, like the status of something (Active, Inactive, Pending, etc). So yes using an enum for this makes sense, it also gives you more type safety as you can add type restrictions based on your Command enum, which would prevent you from using an invalid one.
Couldn’t you make some ECR file that would generate the enum for your godot code? IDK what it actually looks like, but i imagine it wouldn’t be that hard to just generate some JSON or python or whatever.
There is also the symbol casting feature, which would allow you to do like
enum Nums
One
Two
end
def foo(val : Nums)
puts val.value
end
foo :two # => 1
Vs having to type out the full name of the enum member.
I don’t know either, haha. Probably. I only have to alt-tab to Godot, press enter and type in the same command again. (GDScript’s Enums are quite simple as well). Doesn’t really bother me, but just had to think of something.
I’m going to switch to Enums ASAP!
edit: I’m going to be using
enum CMD
XFER
UPDATE_ENTITY
TRAVEL_TO
end
Syntaxically speaking, CMD::UPDATE_ENTITY is actually really nice looking IMO compared to Command:UPDATE_ENTITY. And the enum name is shorter, I like that!
Oh wow, I see what you mean now.
require "socket"
require "json"
enum Command
XFER
UPDATE_ENTITY
TRAVEL_TO
end
class Client
property socket : TCPSocket?
def send(msg, cmd : Command)
new_message = {cmd: cmd.value, message: msg}.to_json
#socket.write_bytes(new_message.bytesize)
#socket.send new_message
pp "Sending #{msg} (#{new_message.bytesize} bytes) with the command #{cmd.value} to the client..."
end
end
p = Client.new
p.send ({portal_idx: 1}), :UPDATE_ENTITY
I don’t need to use the enum name at all… wtf. That’s even better!
So far so good. Any thoughts / improvements? (outside of I need to use from_json).
require "socket"
require "json"
enum CMD
XFER
UPDATE_ENTITY
TRAVEL_TO
end
class Client
property socket : TCPSocket?
end
p = Client.new
def game_handler(msg, p)
cmd = msg["cmd"].as_i
_enum = CMD.from_value(cmd)
case _enum
when CMD::XFER
pp "hi 1"
end
rescue e
pp "Invalid Data Received:"
pp e
end
incoming_message = JSON.parse (%({"cmd": 0, "message": "hello"}))
game_handler(incoming_message, p)
Because it’s a method, and methods can’t start with an uppercase letter.
Enums automatically define methods that check if the current instance is that member. All the methods are down snakecase, as is the standard naming practice for methods in Crystal.
There should be a shorthand notation for CMD::XFER I think. That’s not fair that lower case .xfer? works, but an uppercase variant doesn’t, especially considering the enum values must start with a capital letter. IMO, this promotes consistency and brevity. @asterite what are your thoughts?
I wonder if there are any github issues related to this, I want to read them ;D
I just want a way of doing it properly. I feel like there is too many choices. I’ll probably just do CMD::XFER, so I can keep the consistent casing
Now that I think of it, that’s really weird because first, Enum values must start with a Capital letter. Second, having lowercase methods to check enums… like .xfer? in this case, is the opposite of brevity, it’s more confusing, because it doesn’t even match the Enum value’s casing. A method is not an Enum’s value LOL
Yeah, I really don’t mind using CMD::XFER, however, I feel CMD::XFER is far more explicit, more consistent with how Enums function (values must start with a capital letter), and offers more brevity. I don’t understand why the implicit-object syntax was added for Enums when it’s the total opposite of all the positives of using CMD::XFER.
Probably because Enums can have methods.
However, if used in a case when statement, it’s weird because those methods don’t match the values of the Enum values
Problem I have is, why is the language being prejudiced against developers who prefer to check their Enum values with the actual value of the Enum, but on the other hand, offers short-hand notation through implicit-object syntax that does not even correspond to the actual Enum value itself (XFER → xfer)?
Sorry, but that just doesn’t make sense to me. It should work both ways!
I’d argue the implicit-object syntax for Enums is hard to read code and honestly, shouldn’t even work for Enums because it’s confusing af.
Consistency and less ambiguity is very important.
This also is not a personal opinion, I’ve stated objective facts in my GitHub issue btw… I tried, oh well
If you’re so adamant on having the casing match you are free to define your own way of doing things for your projects, no matter what anyone else thinks. Something like:
struct Enum
def is?(member : self)
self == member
end
end
enum Color
Red, Green, Blue
end
c = Color::Green
case c
when .is?(:Red)
puts "It's red"
when .is?(:Green)
puts "It's green"
when .is?(:Blue)
puts "It's blue"
end
Or crazier still:
struct Enum
def []?(member : self)
self == member
end
end
enum Color
Red, Green, Blue
end
c = Color::Green
p c[:Green]?
Thank you @Exilor for the examples. Honestly, I like being more explicit and using CMD::XFER instead of using implicit-object syntax, because the case when is simply matching the value of an Enum. I don’t feel like it needs to do anything more than that.
Just to add: it’s not about me. This is objectively about good programming practices. It’s consistent, offers more brevity, and matches the actual Enum value itself (not lowercase!). IMO, these 3 things offer a clear advantage over an implicit-object syntax for Enum value checking. This is objectively true, not a personal statement.