Using a set of optional messages is a reasonable way to solve this (the optional messages that are not filled in will not take any space on the wire). You might also want to include an enum which identifies which of the messages is filled in, so that you can switch on it.
message GameProtocolMessage {
enum Type { ... }
required Type type = 1;
optional LoginInfo login_info = 2;
optional Entity entity = 3;
...
}
Another possibility is to do something like this:
message GameProtocolMessage {
enum Type { ... }
required Type type = 1;
required bytes message = 2;
}
Here, "message" itself contains an encoded protocol message of the type identified by Type.
However, I think that using a set of optional fields is a better choice. This is what we usually do at Google. Note that if the number of optional fields becomes unwieldy you may want to switch to using extensions. You can convert fields to extensions without breaking wire-compatibility.
If the input or output should be empty, just define an empty message type and use that. If you decide later that you want it to be non-empty, you can easily add new fields. If there were some generic "void" type you would not be able to change it later without updating all the code that uses your service.