String length? For use in Record Gameplay feature

BlitzPlus Forums/BlitzPlus Beginners Area/String length? For use in Record Gameplay feature

David Boudreau(Posted 2008) [#1]
I'm considering the best way to allow the player to record playing the game (for playback).

There was an article on blitzcoder that described one way to do it by writing all the inputs to a string each frame (and if there are no inputs during a particular frame, to have a special character for "nothing" written to keep it in all in sync). Is that the best way and standard? What about string length limitations? When you have a long game session, isn't that like some huge string that will take a while to write to a file, and also later take a while to read from?

btw what happens if MilliSecs() reaches its upper (finite state) bounds? Can we assume some internals kick in seamlessly and it won't affect our programs? (...I mean someday the sun will burn out and are we really sure we'll have spaceships to travel safely to another galaxy by that time? These things have been keeping me up at night lately.)


LineOf7s(Posted 2008) [#2]
I can't speak for the string-size limit, but if Millisecs() reaches its upper limit, if I recall correctly it wraps around to a very large negative number. Someone clever will tell us what it is, I'm sure, and I know there are (somewhere) equally clever workarounds for it.

[edit] Noticed this was in the BlitzPlus forum. If a knowledgable BlitzPlus user says it's been addressed in the BlitzPlus internals, then ignore what I just said and consider me educated.


Zethrax(Posted 2008) [#3]
The info below relates to Blitz3D, but should also apply to BlitzPlus.

MilliSecs() uses a signed 32 bit integer value, so it will flip to the highest negative value and start counting down to zero after around 24 days from the time your OS was last started. Once it hits zero it will start counting up again.

Check this post for more info: http://www.blitzbasic.com/Community/posts.php?topic=78458#880544

Strings use a 32 bit unsigned integer value for the length header, as far as I'm aware; so you can have strings up to 2^32 characters in length. I'd recommend you look at using custom types to work with more managable chunks of data, however.


David Boudreau(Posted 2008) [#4]
Thanks for the info on MilliSecs guys.

Bill, as far as Types vs. strings for Record/Playback of a game session, can you shed a little more light on how to go about that? I don't yet see where the savings would be by going with types over strings... I mean I bet strings are slower but for this particular purpose, is there a faster way to do it? (I also like the simplicity with strings as the concept seems pretty easy to implement but I don't want to slow things down if I don't have to.)

There is probably a better term for this than record/playback of a game session but I can't think of it right now... you know like a ghost in a driving game that can be saved, and you can race against yourself etc. (like my ghost this week of Ford SVT Lightning winning the weekly cone challenge in pgr2 against Porches).


Zethrax(Posted 2008) [#5]
Bear in mind that, in some cases, there are simple ways to do things, and there are smart ways to do things. The two aren't always happy bedfellows.

If you store all your data in a single string, then towards the end of your play session you will end up working with some huge string values. This would likely cause a variety of issues with memory and cpu usage, as well as memory fragmentation.

You can mitigate this by using custom type objects; with a string field for storing generic data, and some more specific fields for data that is more predictably structured. You would create a new custom type object for each game update, to store the data for that update. This would mean that you would be working with shorter and more managable strings.


David Boudreau(Posted 2008) [#6]
Thanks Bill... Would you read over my concerns below please and let me know if I'm on the right track:

Are you saying that storing the data inside a Type object's Field of type string is more efficient than just a string variable? (If so I wonder why that is the case.)

Or are you saying it is just that you should chunk it all up by dividing one big string of all the inputs into smaller instances of types with fields of much shorter strings (like one character long each)? So after like 30 seconds of recording the game session, that is potentially a whole bunch of instances of this type we're talking about, right? This is the most efficient way to go about it?

So if the user rapidly presses and releases many buttons it won't stress the system so much if I chunk it up into many type instances, instead of one big string? (I need to keep track of each input on/off, and I also want precise recording of which millisecond each input was pressed/released).

I was thinking about this structure for the type, is it enough?

Type WhichInput
Field InputID ; joystick left/up, button1 etc.
Field TimePressedDown ; use MilliSecs() command to get this
Field TimeReleased;
End Type

and then write the values out to a text file later (creating the big hairy string later, only in the output file, after I've stopped recording the game session). Should I just leave everything in the type objects' memory until the player stops recording the game session? Leaving it all to the end won't bloat it up as a cumulative string would?

btw if my Type structure above is ok, will I still need to flag something in the text output file like 0 (zero) to indicate "no input made during this frame" and place 0's in between inputs in the text file to keep it all in sync?


epiblitikos(Posted 2009) [#7]
Call me crazy, but I think I have got something that will interest you.
You want to record the state of inputs per frame of your game, if I am not mistaken. Depending on the detail in which you wish to record them, a base-2 flag system would work--particuarly, if you are recording key-presses/mouse-clicks.

Suppose you have 4 inputs: the arrow keys. Set some constants that represent their input IDs, and a variable that will hold the total input during each frame:
const UP_id = 1 ; 2^0 = 1
const DN_id = 2 ; 2^1 = 2
const LF_id = 4 ; 2^2 = 4
const RT_id = 8 ; 2^3 = 8

global inputState = 0 ; global just for fun, and set to zero to denote no input yet
                      ; this would mean yes, do flag an input-less frame, give it the value 0

We'll assume that diagonal moves are possible, since most games do involve more than one input at a time. And, therefore, at each frame of the game, the 4 inputs can be represented as the sum of these ids, e.g. if UP and LF are pressed then inputState = UP_id + LF_id = 5. This means you can save the byte of value 5 to your record (a bank, I guess) to denote the current input state. (Actually, in this case, since the binary number 1111 = the decimal number 15 = the sum of the 4 ids, the input can be saved in a half of a byte per frame! I'll just use/say byte for simplicity)

A stream of bytes in this case represents an input record from your game and can easily be written to file using BlitzPlus's standard commands. Thus, your record is saved...

Now, how to read it might not be entirely obvious, but it's pretty simple.
Consider out highest id value, RT_id = 8. Each other id is less than it, and in fact, even their sum is less than it! (1 + 2 + 4 = 7 < 8) This is an important property of the system.

Suppose we have a record consisting of input states from our four-input system. Reading the bytes from file we can figure out which inputs are engaged by checking to see which ids the state is less than. e.g.(in pseudocode)

(to transcribe the record into your program)
file = the record file
get the file size
bank = a bank of the file's size
read the bytes into the bank
.
.   (between the record transcription and decoding process)
.
(to translate your bank of bytes into an input sequence
for q = each byte of the bank
    if q > 8 then
        right was pressed
        q = q - 8
    endif
    if q > 4 then
        left was pressed
        q = q - 4
    endif
    if q > 2 then
        down was pressed
        q = q - 2
    endif
    if q > 1 then
        up was pressed
        don't need to subtract 1 from q since it is last check
    endif
next q


It is absolutely necessary to decode from highest to lowest id. This process works for I/O sorts of input--is a key up or down, is a button pressed or not. For higher resolution inputs (joystick directional controls), just record the value(s) per frame.

If you have both I/O inputs and hi-res inputs then you can still use a bank, you just have to make sure you account for the hi-res values in your save/load sequences. This way, no types or strings unless you want them in the output/input file.

Hope this is helpful to you!

PS - You probably know this, but in this system the relationship of the number of inputs to the size of the data is linear in terms of bits: 1 input needs 1 bit of data. So if you have 8, each frame is a byte.


xlsior(Posted 2009) [#8]
There was an article on blitzcoder that described one way to do it by writing all the inputs to a string each frame (and if there are no inputs during a particular frame, to have a special character for "nothing" written to keep it in all in sync). Is that the best way and standard? What about string length limitations?

When you have a long game session, isn't that like some huge string that will take a while to write to a file, and also later take a while to read from?



That last part pretty much goes without saying: the more information you gather, the more you need to write to disk, the longer it will take.

Anyway, some alternatives: If you have logic that is independent of the actual framerate (or have a fixed framerate), there is an alternative approach:

Rather than storing every single step of an event, you can simple record the behavior (e.g. unit a starts moving north, starting at x,y at speed z)
and record it when the event ends (e.g. unit A stopped moving 300 frames later, and is now at position x,y)

The downside is that you may see some graphical glitches along the way if the playback system can't keep up the same framerate as the original system, but you can account for that a little bit by recording the end result positions and such as well.

It's a bit more work to analyze what should be happening, but on the plus side there is likely to be a lot less data to be recorded.

And another thought: Rather than a single string, you could look into an array or shorter strings. That way it's easier to implement some kind of time-tracking system, e.g. each array slot contains one second/minute/whatever worth of information, so can be easier to match with the passage of time.

Another potential benefit is that it's easier to write the data to disk without freezing the program if you have a lot of data: instead of a single writestring that can take a long time to complete, you can iterate through the array, write some members to disk, update a progress bar, write some more members to disk, etc. until completed.


xlsior(Posted 2009) [#9]
Oh, and one final thought: From a technical point of view, there really isn't that much difference in between a record gameplay feature vs. implementing multi-player network routines.

In a network game, the individual clients are essentially replaying what happened (a fraction of a second earlier) on the other computer(s), and consolidating that information with its own world view.

When recording your local gameplay, the only difference is that you record your gameplay information instead of sending it real-time over a network... but the required information is pretty much the same.

Perhaps it's easier to wrap your brain around the concept when you think about it from that angle?


David Boudreau(Posted 2009) [#10]
Thanks Deedub and xlisor. Interesting... and neat way to take advantage of binary counting, not sure it would help for my particular code structure but good to keep in mind for another use.

I recently finished the latest version of the program I was working on. I just wound up storing the inputs into types, one instance for each input "pressing" (StartMS, EndMS, ID (up/down/button3 etc). That works but I had to really fatten up that input detection code... ("first time left being pressed? record StartMS to a new type instance", etc).

btw I found a nasty bug on Friday with the implementation I had because during playback, I made a really wide, wide image with createImage (totalRecordedTime*14 wide... ouch), which worked on my dev machine but tried it out on another machine and realized/remembered the BlitzPlus or 3D thread on how Windows does not like wide images and corrupts them, found out very... intermitently at first. I switched to displaying playback of inputs using an array and that method of storage for the playback data works pretty well now.


epiblitikos(Posted 2009) [#11]
Glad to hear things are working out for you. I'm excited to see how you've used the playback in your game. Good luck with the rest of it!


David Boudreau(Posted 2009) [#12]
btw if you'd like to see what it's like here's a video I made for it:
http://www.youtube.com/watch?v=xi_BYZM1LsA

and you can download it here (if you can recommend a better free download site let me know):
http://www.filefactory.com/file/a016ea3/n/ExecutionAidv2XPw2kJan10_rar

It was primarily intended as a tool for fighting game players, but any game on the PC might make use of it. No keyboard support yet, just joystick (and only those joysticks that have -1, +1 for Axis X/Y and -1 neutral, 0 for up, 45 for up-right, 90 for right etc. for POVhat... a few users say it doesn't work, even when I've heard from others with the exact same joystick that it works fine for, can't figure that one out yet.)


epiblitikos(Posted 2009) [#13]
Looks pretty sweet! I would have d/l'd, but I have no js.