UNet + Blackthorn

5 views
Skip to first unread message

Patrick Stein

unread,
Jun 23, 2011, 7:06:21 PM6/23/11
to Blackthorn Engine 3D
Looking at the networking in Blackthorn, I'm still interested in
pursuing some portions of my unet library [1], but I'm not as married
to the UDP-only model that I had started for it.

[1]: http://nklein.com/software/unet/

Below is a brief overview/description of how I would envision the unet
library working. A good portion of something similar is already done
with UDP sockets. After a few days thought, I'm seeing how TCP could
work, too. I'd much appreciate hearing what you folks think about it
conceptually and whether it's the sort of network plumbing that
Blackthorn would want.

Create a unet instance to track the communications.

(make-instance 'unet :port port-number :protocol protocol :connection-
policy connection-policy)

If a `port-number` is specified, then incoming connections will be
allowed. The `protocol` would be either `:reliable` or `:datagram`.
I'm not too fond of calling this class `unet`. Maybe `session`? The
`connection-policy` would be a function which takes two arguments (the
unet instance and a peer (described later)). The `connection-policy`
function is called when a new peer tries to connect. The function
could then add the peer to various channels (described next) or close
the connection or what-have-you. The default `connection-policy`
would add the peer to all channels on the unet instance.

Next, create some number of channels to add to unet with the add-
channel method.

(add-channel unet (make-instance 'channel :channel-id channel-
id :peer-policy peer-policy))
(list-channels unet) => (channel1 channel2 ...)
(remove-channel unet channel)

The `peer-policy` would specify what to do when someone who is not
already part of a given channel sends a message on the channel. It
would be a function that takes two arguments (the channel and the peer
(described next)) and decides what to do with messages from that peer
on this channel. The default would be to add that peer to the
channel, but one could put them on an ignore list where any messages
they send for this channel are discarded, close the connection with
that peer altogether, what-have-you.... (actually, maybe the default
should be to add him to the ignore list since the connection policy
coulda added him to this channel if it had so desired).

Then, make peers, connect to them, and add them to channels.

(let ((pp (make-instance 'peer :hostname hostname :port port)))
(add-to-all-channels unet pp)
(add-to-channel channel pp)
(list-all-peers unet pp)
(list-channel-peers channel pp)
(remove-from-all-channels unet pp)
(remove-from-channel channel pp)
(close-peer-connection unet pp))

Peer might not seem like the right word if you are used to thinking as
client/server. I'm open to a better word. But, I definitely want to
preserve the ability to have multiple different recipients for
messages on a channel... a possibility of a many-to-many configuration
rather than just a star topology.

If one desires, one can start a separate thread to handle incoming
messages from the socket(s) and have it call this:

(incoming-message-loop unet)

If that function isn't called, unet seamlessly does things in the
current thread. Actually, the above may have to know more about the
particular threading library to get enough mutex and semaphore stuff
set up.... in which case, a better API would be just to do something
like:

(start-message-thread unet)

And just key off of the bordeaux-threads :features to tell whether
that can really be done or not.

Regardless of whether there is a separate message thread or not, one
can check for incoming messages:

(check-for-messages unet :check check-mode :timeout timeout)

;; or for multiple unet instances???
(check-for-messages '(unet1 unet2 ...) :check check-mode :timeout
timeout)

The possible values for `check-mode` are: `:always`, `:never`, and
`:if-empty`. The `timeout` is either a number of milliseconds, the
keyword `:immediately`, or the keyword `:never`. The default is
`:immediately`. The timeout is only used if the `check-mode` is
`:always` or the `check-mode` is `:if-empty` and there are no messages
already waiting on any channels.

And, one can see which channels have messages:

(list-channels-with-messages unet :check check-mode :timeout
timeout)

One can retrieve a single message from a channel or retrieve all of
the messages from a given channel:

(get-message channel :check check-mode :timeout timeout)
=> (values message peer-address)

(get-all-messages channel :check check-mode :timeout timeout)
=> ((message1 peer-address1) (message2 peer-address2) ...)

The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`. Should there be a message struct/class that contains the
message data and the peer address?

I would expect the most common sort of loop would be something like:

(check-for-messages unet :check :if-empty :timeout :immediately)
... do some stuff ...
(mapc #'process-chat-message (get-all-messages chat-channel))
... do some other stuff ...
(mapc #'process-map-message (get-all-messages map-channel))
... do some other stuff ...
(bwhen (mp (get-message game-phase-channel))
(process-game-phase-message (first mp)))

Thoughts/complaints/suggestions/SEGA?

Thanks,
Patrick

Elliott Slaughter

unread,
Jun 24, 2011, 1:18:18 AM6/24/11
to blackthorn-eng...@googlegroups.com
On Thu, Jun 23, 2011 at 4:06 PM, Patrick Stein <p...@nklein.com> wrote:
Looking at the networking in Blackthorn, I'm still interested in
pursuing some portions of my unet library [1], but I'm not as married
to the UDP-only model that I had started for it.

 [1]: http://nklein.com/software/unet/

Below is a brief overview/description of how I would envision the unet
library working.  A good portion of something similar is already done
with UDP sockets.  After a few days thought, I'm seeing how TCP could
work, too.  I'd much appreciate hearing what you folks think about it
conceptually and whether it's the sort of network plumbing that
Blackthorn would want.

Create a unet instance to track the communications.

       (make-instance 'unet :port port-number :protocol protocol :connection-
policy connection-policy)

If a `port-number` is specified, then incoming connections will be
allowed.

If you don't specify port-number, then presumably you want to do outgoing connections? But if so then how do you connect?

The `protocol` would be either `:reliable` or `:datagram`.
I'm not too fond of calling this class `unet`.  Maybe `session`?

"Session" makes me think that a socket is already listening and you're establishing and actual connection. "Connection" has similar issues. I agree though that "unet" sounds too generic to me. Maybe "listen-X" or "X-listener" might make it more obvious, but that fails to capture the outgoing connections only case.
 
The
`connection-policy` would be a function which takes two arguments (the
unet instance and a peer (described later)).  The `connection-policy`
function is called when a new peer tries to connect.  The function
could then add the peer to various channels (described next) or close
the connection or what-have-you.  The default `connection-policy`
would add the peer to all channels on the unet instance.

Next, create some number of channels to add to unet with the add-
channel method.

   (add-channel unet (make-instance 'channel :channel-id channel-
id :peer-policy peer-policy))
   (list-channels unet) => (channel1 channel2 ...)
   (remove-channel unet channel)

The `peer-policy` would specify what to do when someone who is not
already part of a given channel sends a message on the channel.  It
would be a function that takes two arguments (the channel and the peer
(described next)) and decides what to do with messages from that peer
on this channel.  The default would be to add that peer to the
channel, but one could put them on an ignore list where any messages
they send for this channel are discarded, close the connection with
that peer altogether, what-have-you.... (actually, maybe the default
should be to add him to the ignore list since the connection policy
coulda added him to this channel if it had so desired).

I'm a little confused by the concept of channels. I think in the typical server-client case, the server will either want to send a message to a specific client, or will broadcast something to all clients. But I can see some other use cases, e.g. a multiplayer strategy team where you have one channel per team.

But I still find the concept confusing. Where do channels live? You talk above about a peer sending a message to a channel it isn't a member of, implying that that peer can somehow know about, and send messages to channels on remote machines. But from the API examples below, it looks to me like each node sets up the channels that a remote peer, so it isn't obvious that this is two-way relationship.

In Blackthorn, each machine keeps a symbolic mapping from node IDs to sockets. In a way, my node IDs are similar to channels where each channel has one peer. I even create a "broadcast" pseudo ID for sending to all hosts. But the key difference is that the mapping lives completely locally, and thus there is no way a remote host could send to (or even know about) any of the "channels" a local host is connected to. Maybe setting up many-to-many connections like this is more work, as each client needs it's own set of mappings. But the one-to-many case is entirely manageable.
I don't understand how this one is different from check-for-messages.

One can retrieve a single message from a channel or retrieve all of
the messages from a given channel:

   (get-message channel :check check-mode :timeout timeout)
       => (values message peer-address)

   (get-all-messages channel :check check-mode :timeout timeout)
       => ((message1 peer-address1) (message2 peer-address2) ...)

The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.

What exactly does ":never" mean? The timeout parameter is critical, but I can't think of a use case for check-mode.

Should there be a message struct/class that contains the
message data and the peer address?

Or multiple return values.

Should a physical address be abstracted behind a symbolic identifier? Vectors and strings are not very nice as hash keys, which is a common enough operation to do with a remote address.

I would expect the most common sort of loop would be something like:

   (check-for-messages unet :check :if-empty :timeout :immediately)
   ... do some stuff ...
   (mapc #'process-chat-message (get-all-messages chat-channel))
   ... do some other stuff ...
   (mapc #'process-map-message (get-all-messages map-channel))
   ... do some other stuff ...
   (bwhen (mp (get-message game-phase-channel))
       (process-game-phase-message (first mp)))

Thoughts/complaints/suggestions/SEGA?

I think the API is missing some sort of method for handling disconnects. Any network operation could potentially result in a disconnect, and games usually have game-specific cleanup that needs to be done when this happens.

Other than that and some confusion about channels and check-mode, looks good to me.

Thanks,
Patrick



--
Elliott Slaughter

"Don't worry about what anybody else is going to do. The best way to predict the future is to invent it." - Alan Kay

Patrick Stein

unread,
Jun 24, 2011, 2:41:20 AM6/24/11
to blackthorn-eng...@googlegroups.com
On Jun 24, 2011, at 12:18 AM, Elliott Slaughter wrote:

On Thu, Jun 23, 2011 at 4:06 PM, Patrick Stein <p...@nklein.com> wrote:
Create a unet instance to track the communications.

       (make-instance 'unet :port port-number :protocol protocol :connection-
policy connection-policy)

If a `port-number` is specified, then incoming connections will be
allowed.

If you don't specify port-number, then presumably you want to do outgoing connections? But if so then how do you connect?

When you call `add-to-channel channel pp`, the first time address `pp` is added to a channel on a given `unet` instance, it establishes a connection.  So, assuming you didn't see a need for channels (and I'll explain why I think they'd be useful more again below) and you were doing something client-server, you could do this on the server side:

    (defparameter *s* (make-instance 'unet :port +server-port+))
    (defparameter *chan* (make-instance 'channel :channel-id 1))
    (add-channel  *s* *chan*)

On the client side, you'd do:

    (defparameter *s* (make-instance 'unet :connection-policy #'deny-all-connections))
    (defparameter *chan* (make-instance 'channel :channel-id 1))
    (add-channel *s* *chan*)
    (add-to-channel *chan* (make-instance 'peer :hostname +server-hostname+ :port +server-port+))

In a peer to peer situation, both sides would use the :port option to the 'unet instance and both sides would call add-to-channel with the other's address.  Peer A wouldn't try connecting to Peer B if Peer B had already succeeded in connecting to Peer A and if Peer B had tried and failed to connect to Peer A, it would still record Peer A as part of that channel so that when it gets that connection, it can complete the handshake.

I'm a little confused by the concept of channels. I think in the typical server-client case, the server will either want to send a message to a specific client, or will broadcast something to all clients. But I can see some other use cases, e.g. a multiplayer strategy team where you have one channel per team.

The idea of channels for me was a way to multiplex the messages coming over the socket so that on the receiving end, they get put into different queues where I can process the messages from one queue before moving on to a different queue.  From the examples that I used in the original email, there is no reason that I would have to deal with all of the messages on the chat-channel every time through the main loop.

Contrast this with a typical sort of event loop where you'd have a big case block keying off of the message type and dealing with each message in the order it was received.

The idea would be to have a set of inbox-folders rather than one monolithic inbox.  In a client/server mode, the clients would have only the server in any channel.

I forgot to mention too that I have a way to send to only a subset of the clients on a channel.  So, the server could have everyone all in one channel and then send to only individuals if it so desired or broadcast to all members of the channel.

In a peer-to-peer configuration, some channels might not have everyone in them.  You might have a 'team' channel in which you carefully decide who gets to send messages to you on that channel and who messages go to when you send on that channel.

It is expected that for a particular game, all participants would have the same set of channel-ids.... the channel-id would be part of the header on the message so that the receiving end knows which queue it goes to.

In Blackthorn, each machine keeps a symbolic mapping from node IDs to sockets. In a way, my node IDs are similar to channels where each channel has one peer. I even create a "broadcast" pseudo ID for sending to all hosts. But the key difference is that the mapping lives completely locally, and thus there is no way a remote host could send to (or even know about) any of the "channels" a local host is connected to. Maybe setting up many-to-many connections like this is more work, as each client needs it's own set of mappings. But the one-to-many case is entirely manageable.

So, in the current Blackthorn model, you can decide whether or not to ever read from the socket for node ID XX and decide not to write to it.  But, if you read from it, then you've got a message in your hand that you either have to stow somewhere or deal with.    

Your main-loop logic would be set up to know... here are the people that I know about... go through each one (or some subset of them) and deal with whatever it sent me.

With the channels mentality, your main loop would be set  up to know... here are the categories of messages that I expect to get... go through each one (or some subset of them) and deal with each message+WhoSentIt for that category (and you could have decided ahead of time whether you'd ever read messages for that category from that dude).

I suppose that I could see it debatable either way.  At the very least, I'd think that under the hood there would be sort-of two channels... one for any communication that has to happen under-the-hood/behind-the-scenes/entirely-in-the-network-library and one for messages that the application wants to send to the application on the other side.


   (check-for-messages unet :check check-mode :timeout timeout)

   (list-channels-with-messages unet :check check-mode :timeout
timeout)

I don't understand how this one is different from check-for-messages.

Ah... I was assuming 'check-for-messages' would just return a boolean as to whether there are some messages available on some channels.  The `list-channels-with-messages` would return a list of all of the channels that have messages.  Neither of these would actually return a message.  They'd just let you know that there are messages available on some channel or which channels there are messages available on.

   (get-message channel :check check-mode :timeout timeout)
       => (values message peer-address)

   (get-all-messages channel :check check-mode :timeout timeout)
       => ((message1 peer-address1) (message2 peer-address2) ...)

The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.

What exactly does ":never" mean? The timeout parameter is critical, but I can't think of a use case for check-mode.

Suppose that I just started up and I called:

    (check-for-messages unet :check :always :timeout 0.1)

This would wait 100ms for data to come in on any of the sockets that the unet instance knows about.  It would return 't' if it had gotten some messages and put them on the channel queues.

Now, suppose a bit later, we again call:

    (check-for-messages unet :check :always :timeout 0.1)

This would again wait up to 100ms trying to read data from some socket that the unet instance knows about.  It would return 't' if it got some new messages *or* if there were still some messages that weren't retrieved by `get-message` left over from the previous call.

Now, suppose a bit later, we call with :never...

    (check-for-messages unet :check :never)

This would return 't' if there are some messages still waiting in some channel queues.

Now, suppose a bit later, we call with :if-empty

    (check-for-messages unet :check :if-empty :timeout 0.1)

If there are still some messages waiting in some channel queues, this would return 't' immediately.  Otherwise, it does an :always.


Should there be a message struct/class that contains the
message data and the peer address?

Or multiple return values.

And, multiple-return values is what I have as the return for `get-message`, but it is trickier with `get-all-messages`.  I suppose it could return two lists.... one of messages and one of who sent it.

Should a physical address be abstracted behind a symbolic identifier? Vectors and strings are not very nice as hash keys, which is a common enough operation to do with a remote address.

The peer struct that I have reduces the hostname + port to a 32-bit IP address and a 16-bit IP address stored together in a 48-bit integer effectively.  It makes a decent hash key.   And, you can construct it once and tuck it into a variable if you want to refer to it later.

I think the API is missing some sort of method for handling disconnects. Any network operation could potentially result in a disconnect, and games usually have game-specific cleanup that needs to be done when this happens.

True, that.  There certainly should be a disconnect policy, too.

Hope this cleared up some stuff,
Patrick

Elliott Slaughter

unread,
Jun 24, 2011, 12:05:07 PM6/24/11
to blackthorn-eng...@googlegroups.com
It still seems kind of magical, e.g. in the current system it seems like the channels all live locally, which adds some possibility of error if the server and client have different names for things.

For example, take the team chat example. The server creates two channels, :team-chat-A and :team-chat-B. A given client is on team A, so it needs to be on :team-chat-A. But the client can't even know the name of the channel until the server tells it what that is. I think this is where things get confusing (e.g. "why isn't the client automatically in the channel after the server adds it?") and a potential place where new users could make mistakes with the API.

I think the idea of inboxes is good, and the idea of tagging messages for which inbox they should go into is good. I'm not sure that the idea of "only some clients are allowed to use certain tags" is good, because it starts sounding more like IRC where "channels" exist in some global context and joining is a one-step operation (and e.g. other clients don't need to do anything when a sibling joins, even though the server has some work to do). The IRC analogy was actually the first thing I thought of when I saw channels, which is obviously not a very good match for what they actually do.
   (check-for-messages unet :check check-mode :timeout timeout)

   (list-channels-with-messages unet :check check-mode :timeout
timeout)

I don't understand how this one is different from check-for-messages.

Ah... I was assuming 'check-for-messages' would just return a boolean as to whether there are some messages available on some channels.  The `list-channels-with-messages` would return a list of all of the channels that have messages.  Neither of these would actually return a message.  They'd just let you know that there are messages available on some channel or which channels there are messages available on.

I guess the advantage is that check-for-messages doesn't have to cons a return value? Otherwise I'd just capture and test the result of (list-channels-with-messages ...) and just do nothing when nil was returned.
   (get-message channel :check check-mode :timeout timeout)
       => (values message peer-address)

   (get-all-messages channel :check check-mode :timeout timeout)
       => ((message1 peer-address1) (message2 peer-address2) ...)

The default `check-mode` for `get-message` and `get-all-messages` is
`:never` while the default `check-mode` for `check-for-messages` is
`:always`.

What exactly does ":never" mean? The timeout parameter is critical, but I can't think of a use case for check-mode.

Suppose that I just started up and I called:

    (check-for-messages unet :check :always :timeout 0.1)

This would wait 100ms for data to come in on any of the sockets that the unet instance knows about.  It would return 't' if it had gotten some messages and put them on the channel queues.

Now, suppose a bit later, we again call:

    (check-for-messages unet :check :always :timeout 0.1)

This would again wait up to 100ms trying to read data from some socket that the unet instance knows about.  It would return 't' if it got some new messages *or* if there were still some messages that weren't retrieved by `get-message` left over from the previous call.

Now, suppose a bit later, we call with :never...

    (check-for-messages unet :check :never)

This would return 't' if there are some messages still waiting in some channel queues.

How is this different from

(check-for-messages unet :check :always :timeout :immediately)

it seems to me that this captures the exact meaning of

(check-for-messages unet :check :never :timeout :immediately)

because either way no waiting can occur. And the other options for timeout, e.g.

(check-for-messages unet :check :never :timeout 1.0)

are completely bogus because the call above should never wait. It doesn't make sense to specify both :check :never and a :timeout at the same time. I think the API might be less confusing without the :check parameter. As a side effect that gets rid of :if-empty; if that's a problem then we could just get rid of the :never value for :check.

Now, suppose a bit later, we call with :if-empty

    (check-for-messages unet :check :if-empty :timeout 0.1)

If there are still some messages waiting in some channel queues, this would return 't' immediately.  Otherwise, it does an :always.

What is this use case for this?

In single threaded mode, I don't think I want the network code waiting, ever. My main loop needs precise control over the timings of the loop iterations.

In multi threaded mode, then I'd use your wrapper around this and never even see this function call.

Should there be a message struct/class that contains the
message data and the peer address?

Or multiple return values.

And, multiple-return values is what I have as the return for `get-message`, but it is trickier with `get-all-messages`.  I suppose it could return two lists.... one of messages and one of who sent it.

Or a list of lists where each list contains <message> <sender>? That has the advantage that you can do something like

(dolist (msg-and-stuff (get-all-messages ...))
  (apply #'my-handler-fn msg-and-stuff))

without having to unpack a struct or map over a set of lists.

Should a physical address be abstracted behind a symbolic identifier? Vectors and strings are not very nice as hash keys, which is a common enough operation to do with a remote address.

The peer struct that I have reduces the hostname + port to a 32-bit IP address and a 16-bit IP address stored together in a 48-bit integer effectively.  It makes a decent hash key.   And, you can construct it once and tuck it into a variable if you want to refer to it later.

I think the API is missing some sort of method for handling disconnects. Any network operation could potentially result in a disconnect, and games usually have game-specific cleanup that needs to be done when this happens.

True, that.  There certainly should be a disconnect policy, too.

Hope this cleared up some stuff,
Patrick


Thanks for taking the time to work on this.

Patrick Stein

unread,
Jun 24, 2011, 12:59:09 PM6/24/11
to blackthorn-eng...@googlegroups.com
On Jun 24, 2011, at 11:05 AM, Elliott Slaughter wrote:

I suppose that I could see it debatable either way.  At the very least, I'd think that under the hood there would be sort-of two channels... one for any communication that has to happen under-the-hood/behind-the-scenes/entirely-in-the-network-library and one for messages that the application wants to send to the application on the other side.

It still seems kind of magical, e.g. in the current system it seems like the channels all live locally, which adds some possibility of error if the server and client have different names for things.

For example, take the team chat example. The server creates two channels, :team-chat-A and :team-chat-B. A given client is on team A, so it needs to be on :team-chat-A. But the client can't even know the name of the channel until the server tells it what that is. I think this is where things get confusing (e.g. "why isn't the client automatically in the channel after the server adds it?") and a potential place where new users could make mistakes with the API.

Indeed... in a peer-to-peer system, there would not be a :team-chat-A or a :team-chat-B channel, just a :team-chat channel and you carefully decide who gets to be in your channel (who receives messages when you send to the channel and whose messages you pay attention to when they arrive on the channel).

There are two ways to approach it in a client-server system:

You could still have a single :team-chat channel and that the idea of their being multiple teams is kept in the game logic so the game logic knows that if it got a team chat message from user X then it needs to repeat that to all users on team A (except user X) on the team chat channel.

Alternately, you could have both a :team-chat-A channel and a :team-chat-B channel.  The server sends clients on team A a message (on :team-chat-A channel or on some other :server-directive channel or something) telling them to use :team-chat-A instead of :team-chat-B.  The server doesn't add any of the users on team A to the :team-chat-B channel and thus implicitly ignores anything they send on that channel and never sends them something when it sends stuff to that channel (because they aren't a part of the channel).

I was thinking of channels as fairly static, game-specific things.  You establish them and then use them as you like.

I think the idea of inboxes is good, and the idea of tagging messages for which inbox they should go into is good. I'm not sure that the idea of "only some clients are allowed to use certain tags" is good, because it starts sounding more like IRC where "channels" exist in some global context and joining is a one-step operation (and e.g. other clients don't need to do anything when a sibling joins, even though the server has some work to do). The IRC analogy was actually the first thing I thought of when I saw channels, which is obviously not a very good match for what they actually do

Well, then you still need some logic somewhere in the game to ignore messages coming from player Y to your team-chat-B channel.  I was just trying to get that little bit of access control taken care of as early as possible with the least amount of extra game logic.

.
   (check-for-messages unet :check check-mode :timeout timeout)

   (list-channels-with-messages unet :check check-mode :timeout
timeout)

I don't understand how this one is different from check-for-messages.

Ah... I was assuming 'check-for-messages' would just return a boolean as to whether there are some messages available on some channels.  The `list-channels-with-messages` would return a list of all of the channels that have messages.  Neither of these would actually return a message.  They'd just let you know that there are messages available on some channel or which channels there are messages available on.

I guess the advantage is that check-for-messages doesn't have to cons a return value? Otherwise I'd just capture and test the result of (list-channels-with-messages ...) and just do nothing when nil was returned.

Yes... whereas if you just always wanted to service all of the messages all at once, you could #'mapc on the results of list-channels-with-message.



Now, suppose a bit later, we call with :never...

    (check-for-messages unet :check :never)

This would return 't' if there are some messages still waiting in some channel queues.

How is this different from

(check-for-messages unet :check :always :timeout :immediately)

The difference is that :check :never does not read anything from the socket even if there is stuff waiting to be read.  The only advantage of the :never is if you want to carefully control when you pluck bytes off of the socket.... or when you interact with the thread which is pulling bytes off of the other socket.

it seems to me that this captures the exact meaning of

(check-for-messages unet :check :never :timeout :immediately)

The end-result is the same if there are no messages on any of the sockets, it takes zero time to check that there aren't any messages on the sockets, and (if the socket reader is in a separate thread) that there was no time required to grab the appropriate mutexes to check whether there is data that needs to be handed from one thread to the other.

The concept for me was that you might like to call it once with a timeout of immediately or a few milliseconds to gather up messsages and then call it at various other points in the same iteration of the loop to see if you're done handling all of the messages that you got when you actually gathered them.

because either way no waiting can occur. And the other options for timeout, e.g.

(check-for-messages unet :check :never :timeout 1.0)

Yes... the timeout option is unused with the :never.

Now, suppose a bit later, we call with :if-empty

    (check-for-messages unet :check :if-empty :timeout 0.1)

If there are still some messages waiting in some channel queues, this would return 't' immediately.  Otherwise, it does an :always.

What is this use case for this?

The use case is one where I have 100ms that I could spare, but if there are messages that I haven't dealt with yet or a message comes in during that 100ms, I want to go ahead and start dealing with it.  100ms is probably not game-compatible here at all... make it 10ms.  Somehow, at the moment, you've only used up 6ms in this pass through the game loop and you don't need to update the screen for another 10ms to keep up with your 60fps target.

I admit, most games would opt for :immediately.  But, if you're just in the lobby portion of your game, you might be a great deal less pressed for time.

In multi threaded mode, then I'd use your wrapper around this and never even see this function call.

In threaded mode, you'd still need to call check-for-messages to transfer control of messages from the other thread to this thread.  True, the timeout makes much less sense in that use-case.  It could probably still be worked out with condition-waits with timeouts, but it would complicated without much benefit.


Should there be a message struct/class that contains the
message data and the peer address?

Or multiple return values.

And, multiple-return values is what I have as the return for `get-message`, but it is trickier with `get-all-messages`.  I suppose it could return two lists.... one of messages and one of who sent it.

Or a list of lists where each list contains <message> <sender>? That has the advantage that you can do something like

And, a list-of-lists was what I had in the original email, but I found it annoying that each function to deal with messages had to destructure that list.  Returning two lists gave the possiblity of:

    (mapcar #'my-handler-that-ignores-who-sent-it msgs (get-all-messages ...))
or:
    (multiple-value-bind (msgs peers) (get-all-messages ...)
      (mapcar #'my-handler-that-cares-who-sent-it msgs peers)

(dolist (msg-and-stuff (get-all-messages ...))
  (apply #'my-handler-fn msg-and-stuff))

Hmmm.... I suppose that's not bad... especially with a macro mapapply

   (defmacro mapapply (func list-of-lists)
      (let ((var (gensym)))
         `(mapcar #'(lambda (,var) (apply ,func ,var)) ,list-of-lists))


Anyhow, more later,
Patrick

Elliott Slaughter

unread,
Jun 26, 2011, 1:21:23 PM6/26/11
to blackthorn-eng...@googlegroups.com
I think that's all of my questions for now. I think anything else can wait for the library to actually materialize before continuing the discussion. :-)

Patrick Stein

unread,
Jun 26, 2011, 1:33:49 PM6/26/11
to blackthorn-eng...@googlegroups.com
All of this said, I hadn't realized Bill Robinson's library had lag compensation now.  IIRC, it's all UDP based, but certainly, it could make a good base.

-- Patrick <p...@nklein.com>
Reply all
Reply to author
Forward
0 new messages