Ok, will try and explain how this works... (check's coffee supply)
So, what this example does, is keeps track of several objects and records their state for each cycle of the main loop. The system relies on an integer 'game time' variable to hold the actual time index, and this is used to rewind and record.
Ok, so lets step through the code...
sync on
sync rate 60
type entitytype
x as float y as float z as float
xs as float ys as float zs as float
xa as float ya as float za as float
xas as float yas as float zas as float
mode as integer
stamp as integer
id as integer
endtype
So, that creates a type called entitytype, which is your standard entity properties, position, velocity, angle, angular velocity, mode - the only thing that these entities do right now is move in a set direction until they hit the wall, then they bounce back. There's no gravity or anything, but adding gravity would be easy, just decrement the YS variable, that's the vertical speed. This type is used to record the data, and control it, so it is used in the array Entity() to store those cubes, and also Timeline() which records it. The Stamp variable holds the integer game time, which simply increments each loop while recording, or decrements if rewinding.
In this example, 100,000 indices have been set for recording, at 60fps with 100 objects, that gives about 100000/(60*100) seconds, or 167 seconds, with 1000 objects you get 16.7 seconds. However, you only have to record active objects, any object that is not static should be recorded, so you might even just be recording the player position each loop, in which case it'll record for almost half an hour.
Anyhoo, timelines global holds the number of entries, and timelinecur holds the current timeline number. So when recording timelinecur will increment as entities are recorded, if it hits 100,000 then it has to reset to 0.
global timelines=100000
global timelinecur=0
So then we dimension those arrays, both of them as entitytype, for consistancy more than anything - for example, you might have an established entity movement system, you might be using a physics plugin - in which case you might not need entity().
dim entity(256) as entitytype
dim timeline(timelines) as entitytype
Now this just adds 100 objects that are updated and tracked in the array entity() - so this little bit will create those objects and spawn them.
`Make some spheres to avoid
for e=1 to 100
entity(e).x=rnd(20)-10
entity(e).y=rnd(20)-10
entity(e).z=rnd(20)-10
entity(e).xa=rnd(360)
entity(e).ya=rnd(360)
entity(e).za=rnd(360)
entity(e).xs=sin(rnd(360))/10.0
entity(e).ys=sin(rnd(360))/10.0
entity(e).zs=sin(rnd(360))/10.0
entity(e).xas=sin(rnd(360))
entity(e).yas=sin(rnd(360))
entity(e).zas=sin(rnd(360))
entity(e).mode=1
entity(e).stamp=0
if e=1
`Player object
entity(e).x=-95
entity(e).y=-95
entity(e).z=-95
make object sphere e,0.3
color object e,rgb(255,0,0)
ghost object on e
set object collision to spheres e
else
make object cube e,15+rnd(30)
color object e,rgb(55,0,45)
set object collision on e
endif
next e
Notice that entity 1 is flagged and creates a sphere instead of a cube, as that's the player object - the entity is basically an object, and there are no stuck rules for what the entity can be. I mean, the entity array is just an array, the relationship with actual object is tenuous to say the least - in fact it assumes that entity 1 is object 1, and that's a pretty neat way to work with this technique. But, as I was typing, the entity could be a 2D sprite, it could be a 3D sound - for more complex tracking, allowing tracking of sounds and stuff you would need to add to the type, maybe a 'typeof' field to track what the entity actually is.
This just makes a border, so we know where the barriers are - the example is locked into a 200x200x200 area. Also some visual stuff goes on, like adding fog.
`Make outside border
make object cube 500,200
scale object 500,-100,-100,-100
color object 500,rgb(100,50,0)
fog on
fog color 0
fog distance 30.0,250.0
Now actually recording the data needs a couple more variables to keep track, Record is the state of the recorder, 0 is paused, 1 is recording, and 2 is rewinding. As a default the record will be active. Gametime will store the frame number of the main loop, this is rewound downto a minimum of 0. Recordtime is a vital variable, without it things would loose track really quickly. Every time the loop is recorded, this is incremented upto the maximum number of recordings (100,000, stored in timelines), so if it exceeds 100,000 it stays at 100,000 - because that's how many recordings you have and the maximum is 100,000. When rewinding, the recordtime is decremented downto 0, or a safe value (might not be a good idea to go all the way back to 0). If it hits 0, then either the recorder is right at the start, or it's looping on itself - and going beyond these limits would give weird results.
global record=1
global gametime=0
global recordtime=0
Now the main loop, pretty straightforward as the complex stuff is done in the function.
do
if mouseclick()=1 then record=2 else record=1
if record=1 and gametime>100
if object collision(1,0)>0 then record=0 : center text screen width()/2,300,"HIT - Click mouse to rewind"
endif
if record=1
`cheap movement speed calc
move object 1,1.0
entity(1).xs=(object position x(1)-entity(1).x)/3.0
entity(1).ys=(object position y(1)-entity(1).y)/3.0
entity(1).zs=(object position z(1)-entity(1).z)/3.0
move object 1,-1.0
`Rotation according to mouse moves
entity(1).xas=curvevalue(mousemovey(),entity(1).xas,10.0)
entity(1).yas=curvevalue(mousemovex(),entity(1).yas,10.0)
entity(1).zas=0
entity(1).za=0
update()
record()
endif
`Rewind with left mouseclick
if record=2 then playback()
position camera entity(1).x,entity(1).y,entity(1).z
rotate camera entity(1).xa,entity(1).ya,entity(1).za
move camera -2.0
sync
loop
Remember that entity #1 is the player, so the values for this entity are forced, otherwise the update function would interfere - it is a sloppy way of doing it, but for the example it's fine.
So if you click the mouse then RECORD=2, and the game time is decreased and the objects revert to their previous stored state. If you don't click then RECORD=1, so the game time will increment and the objects are stored.
The mechanics of the player and camera are irrelevant, it's not usually the way I would handle a player model, so it's there to get the tutorial working, hopefully though that stuff is quite straightforward.
Now that update() function, it'll step through all the entities and update their position, rotation, velocity, as long as the mode>0 - if an object dies, becomes static, then setting the mode to 0 will remove it from entity recording, however this is not covered in this example, and it should probably only be done after one last recording - otherwise it might get a bit flaky - so record the state when an object stops moving, then set it's mode, and then you'll still be able to rewind etc. It'll look after itself I think as long as your sensible with the MODE variable, maybe use a flag to signify a dying entity - set the mode to -1 if it's dying, record it's dead position, then set it's mode to 0 in the next loop if mode=-1.
So, steps through the entities, if mode>0 then increment X Y and Z according to the speeds stored in XS YS and ZS. Also increment the X Y and Z angles based on the angle speeds XAS YAS and ZAS. This function also increases the speed, which is really just an artifact of the example, trying to make it more and more challenging to avoid the cubes, so they speed up - this should probably be disabled if using this code in a proper game. Any cubes that hit the walls bounce back, and all cubes start in the centre and spread out, so there is no chance of being owned by cubes before you can do anything when starting up.
function update()
for e=1 to 100
m=entity(e).mode
if m>0
`Affect position and angle based on speed
inc entity(e).x,entity(e).xs
inc entity(e).y,entity(e).ys
inc entity(e).z,entity(e).zs
inc entity(e).xa,entity(e).xas
inc entity(e).ya,entity(e).yas
inc entity(e).za,entity(e).zas
`Increase speed slowly
entity(e).xs=entity(e).xs*1.0001
entity(e).ys=entity(e).ys*1.0001
entity(e).zs=entity(e).zs*1.0001
`Bounce against walls
if entity(e).x<-100 then entity(e).xs=abs(entity(e).xs)
if entity(e).x>100 then entity(e).xs=-abs(entity(e).xs)
if entity(e).y<-100 then entity(e).ys=abs(entity(e).ys)
if entity(e).y>100 then entity(e).ys=-abs(entity(e).ys)
if entity(e).z<-100 then entity(e).zs=abs(entity(e).zs)
if entity(e).z>100 then entity(e).zs=-abs(entity(e).zs)
`Update and show object
position object e,entity(e).x,entity(e).y,entity(e).z
rotate object e,entity(e).xa,entity(e).ya,entity(e).za
show object e
else
hide object e
endif
next e
endfunction
Now the record and playback functions, these are actually quite straightforward I think. When recording, the timelinecur variable is incremented, and if it exceeds the limit of recordings it resets to 0. It records every active entity's state, really it's the same as saving the game data to a file, except this is the equivalent of having maybe 1000 save games running in sequence, so you could step back to any point in those 1000 recordings. The entity ID relates to the object in this example, needed so that the system knows which item in the entities() array to use. So STAMP and ID are like an index of this data, there will be several ID's per STAMP, and if you step back through the recording, then setting each entity until you hit the required stamp ID will 'rewind'.
`Increases gametime and sets timeline to objects state
function record()
inc gametime,1
for e=1 to 100
m=entity(e).mode
if m>0
timeline(timelinecur).x=entity(e).x
timeline(timelinecur).y=entity(e).y
timeline(timelinecur).z=entity(e).z
timeline(timelinecur).xa=entity(e).xa
timeline(timelinecur).ya=entity(e).ya
timeline(timelinecur).za=entity(e).za
timeline(timelinecur).xs=entity(e).xs
timeline(timelinecur).ys=entity(e).ys
timeline(timelinecur).zs=entity(e).zs
timeline(timelinecur).xas=entity(e).xas
timeline(timelinecur).yas=entity(e).yas
timeline(timelinecur).zas=entity(e).zas
timeline(timelinecur).mode=entity(e).mode
timeline(timelinecur).stamp=gametime
timeline(timelinecur).id=e
inc timelinecur,1
inc recordtime,1
if recordtime>timelines then recordtime=timelines
if timelinecur>timelines then timelinecur=0
endif
next e
endfunction
`Play back, rewinds gametime and sets objects to previous state
function playback()
if gametime<1 or recordtime<1 then exitfunction
dec gametime,1
dec timelinecur,1
dec recordtime,1
while timeline(timelinecur).stamp>=gametime
e=timeline(timelinecur).id
entity(e).x=timeline(timelinecur).x
entity(e).y=timeline(timelinecur).y
entity(e).z=timeline(timelinecur).z
entity(e).xa=timeline(timelinecur).xa
entity(e).ya=timeline(timelinecur).ya
entity(e).za=timeline(timelinecur).za
entity(e).xs=timeline(timelinecur).xs
entity(e).ys=timeline(timelinecur).ys
entity(e).zs=timeline(timelinecur).zs
entity(e).xas=timeline(timelinecur).xas
entity(e).yas=timeline(timelinecur).yas
entity(e).zas=timeline(timelinecur).zas
entity(e).mode=timeline(timelinecur).mode
position object e,entity(e).x,entity(e).y,entity(e).z
rotate object e,entity(e).xa,entity(e).ya,entity(e).za
if entity(e).mode>0
show object e
else
hide object e
endif
dec timelinecur,1
if timelinecur<0 then timelinecur=timelines
dec recordtime,1
endwhile
endfunction
It's just like a time machine, it's as fragile as the worst 60's sci-fi time machine as well. If you rewind, then most likely anything significant that you do will result in a different outcome, even without random elements or physics. The example is sorta like a challenge really, see if you can record a path without getting stuck - using the rewind if things go hairy, sometimes you have to make drastic changes, sometimes tiny tweaks are enough to get through.
Ok, will leave it there and see if you have any queries once you've read it.

Health, Ammo, and bacon and eggs!
