## slow motion / bullet time concepts

Started by RemiD, April 06, 2023, 11:05:57

#### RemiD

#15
i have translated the code example by Midimaster, into bb :

`Graphics( 800, 600, 32, 2 )Global SloMo% = 1Global X#, Y#Global Time%Time = MyMilliSecs()Repeat     If( KeyHit(57)=1 )  If( SloMo = 1 )  SloMo = 4          Else If( SloMo = 4 )  SloMo = 1  EndIf  Time = MyMilliSecs() EndIf   If( Time < MyMilliSecs() )  Time = MyMilliSecs()+1  Y = 600/2-10/2  X = X +1 : If( X > 800-1 ) : X = 0 : EndIf EndIf     SetBuffer(BackBuffer()) ClsColor(000,000,000) : Cls() Color( 240, 240, 240 ) : Rect( X, Y, 10, 10, 1 )     Color( 240, 240, 240 ) : Text( 0, 0, "/"+SloMo ) Flip(0)Until( KeyDown(1)=1 ) Function MyMillisecs%()    Return MilliSecs() / SloMoEnd Function `

@Midimaster >> i hope this is what you wanted to demonstrate ?

so if i understand correctly, the idea is to update the transforms (moves / turns / positions / rotations) only if enough millis have elapsed, but the slomo value can slow down when the next update is done, correct ?

#### Derron

if you did it that way "slow motion" would look "non smooth".

In slow-motion you just move in "more little" steps.
If you only updated less often, then during rendering you need to "interpolate" between "last pos" and "next pos".

Think of a "flip-book" - if you flip "slower", the animation will become ... less smooth. You need more drawings (intermediate animation poses) instead of flipping less often. And so the same for physics: you need "more steps" (albeit they are "smaller" then).

bye
Ron

#### Midimaster

As long as the refreshing of the values is more often than 60Hz you cannot see any "non smooth" effect.

In his example he calls the regular timing 1000 times per second and in slow motion "only" 500 times with a factor of SloMo-factor of 2.

This system is much easyer to implement in an given code, which was based on millisecs, than new inplement a complete delta timing system.

You only have to care about motions which you call less often than every 16msec, 16 corresponds to Screen FLIP 60Hz.

CASES:

When you move clouds  on the sky every 100msec for 1 pixel... In SloMo they would move every 200msec for 1 pixel. That's still ok because the visible minimum on the screen is 1 pixel.

When you moved  a car for 6 pixel every 18msec... In SloMo it would now move every 36msec for 6 pixel. This is a pitty. You should change the code for both RealTime and SlowMo: Move the car 1 pixel every 3msec, then you become 1 pixel every 6msec in SlowMo.

You could write a main loop, that branches into drawing only when 16msec have passed:

Code (vb) Select
`Global DrawTimer:IntRepeat    If DrawTimer<Millisecs()          DrawTimer = Millisecs()+16          Cls          DrawAll....          Flip 0    Else          CalculateAll          Delay 1    Endif Until AppTerminate()Function CalculateAll()    If BallTimer<MyMillsecs()          BallTimer = MyMillsecs()+5          MoveBall()    endif     If PlayerTimer<MyMillsecs()          PlayerTimer = MyMillsecs()+10          MovePlayer()    Endif     If CloudTimer<MyMillsecs()          CloudTimer = MyMillsecs()+100          MoveClouds()    Endif     .....End Function `
So you will get 1000 main loops per second.

...back from Egypt

#### Derron

You should have physics steps "shared" across the objects. And not "every x milliseconds for car 1, every y milliseconds for body 5" (else you will need to split "collision checks" etc - so that they all check "at the same time" regarding physics/world simulation).

Also you sooner or later have fast moving objects which in slow mo would be "visible" if the steps "between updates" (or renders when interpolating/tweening) are too big.

60 Hz: people expect you to support 144hz today (and more).
Do not make your logic dependent on some assumed refresh rates (it looks "sluggish" if someone has 75hz or 30hz or ...) - make your logic independent from it. You get a delta time (time between "frames") and this is your factor for movement.
Adjust the factor and things move slower or faster "from frame to next frame".

You can limit how often physics update (1000 times a second -- or even only 30) but of course each update comes with CPU cycle cost. People proposed high physics update rates to avoid bullets not hitting a thin wall etc.
If your hardware is capable of simulating the physics 1000 times a second (plus rendering it eg 144 times a second) then you might come away by updating "less often" / skipping updates ... but it _will_ stutter visually here and there (depends on how the update cycles and the render calls are scheduled).

I think you should check how bullet time is done in other engines/languages and simply choose what they did.

bye
Ron

#### RemiD

#19
@Derron>>
QuoteI think you should check how bullet time is done in other engines/languages and simply choose what they did.
it is not that easy because a lot of things are done behind the scene for Unity engine and Unreal engine, whereas for Blitz3d you have to code more low level stuff.

QuoteAs long as the refreshing of the values is more often than 60Hz you cannot see any "non smooth" effect.
i think that with more than 15fps, even if each step of turn / move / animation frames are not really ideal, it would not be noticeable.

@Midimaster>>
i am going to implement your method with the same scene that i made (using delay for the slow motion effect) and see if it works well...
if my understanding of your method is correct, it should work...

#### Derron

#20
https://gamedev.stackexchange.com/questions/37976/how-to-code-time-stop-or-bullet-time-in-a-game
-> use deltatime and a modifier

I still do not understand why you have issues using this simple "modified deltatime" approach. Any physics are based on ...time. So if you adjust time, then physics just happens "slower".
If a character jumps then it has energy for the upwards movement and there is gravity which tries to "move it down" (with around 9,81m/s²). so the downwards acceleration is based on time too (t²).

The "time" grows by "deltatime" (or here: "Time = Time + deltatime * globalspeedModifier"). Each time you "update()" your entities (the jumping character) you do this by "jumpingCharacter.update(deltatime * globalspeedModifier)".
Inside that method you update the position of the element based on "how would it move per second" - but multiply this value with "deltatime" _and_ your individual slowmo-modifier (or speedup).
This way the same movement (moving from x10 to x100 - or jumping up and falling down) just takes "longer".

But the important thing is: the jump "curve" will be the same as with normal time - it just takes longer, or less long than "normal".

Using the deltatime approach even allows to use one of these "fancy" interpolation/easing formulas (start slow, become fast, finish slow ...)

bye
Ron

#### Derron

#21
The following code has 2 balls with the same initial velocity ("movement").
When I hit "space" they jump, when I hit "b" then for ball1 the time will pass at 50% speed (so deltatime * 0.5).
when jumping the jump for the "bullet time" ball will reach the same height, the same distance .. it just takes longer.

`SuperStrictFramework Brl.StandardIOImport Brl.GLMax2DImport "../source/Dig/base.util.deltatimer.bmx"Graphics 800, 600Global dt:TDeltatimer = New TDeltaTimer.Init(100, -1)dt._funcUpdate = Updatedt._funcRender = RenderGlobal ball1:TBall = New TBall(0, 200)Global ball2:TBall = New TBall(0, 400)Repeat dt.Loop()Until KeyHit(KEY_ESCAPE) or AppTerminate()'-----Function Update:Int() 'logic If KeyHit(KEY_SPACE) ball1.Jump() ball2.Jump() EndIf If KeyHit(KEY_B) if ball1.timeMod = 1.0 ball1.timeMod = 0.5 Else ball1.timeMod = 1.0 EndIf EndIf ball1.Update(dt.GetDelta()) ball2.Update(dt.GetDelta())End FunctionFunction Render:Int() 'render Cls ball1.Render() ball2.Render() Flip 0End FunctionType TBall Field x:Float, y:Float Field vx:Float, vy:Float Field groundY:Float Field size:Int Field timeMod:Float = 1.0 Global jumpStrength:int = 300 Global gravity:Float = 350 Method New(x:Float, y:Float) self.x = x self.y = y self.vx = 200 'initial horizontal speed in px/second self.size = 50 self.groundY = y 'don't go lower than this End Method Method Jump() 'jumping means adjusting your velocity ("upwards = neg. y") 'only jump if not falling/jumping if vy = 0 vy :- jumpStrength EndIf End Method Method Update(dt:Float) Local effDT:Float = dt * timeMod 'adjust position x :+ vx * effDT y :+ vy * effDT 'adjust velocity 'turn at left or right if x > 800 or x < 0 then vx = -1 * vx 'incorporate jumping vy :+ gravity * effDT 'do not go below ground if y >= self.groundY y = self.groundY vy = 0 EndIf End Method Method Render() DrawOval(x - size*0.5, y - size, size*0.5, size*0.5) End MethodEnd Type`

Attaching a little GIF animation (recorded with 15 fps ... so less smooth than in real time).

The DeltaTimer is from my DIG-framework but you can of course use your own stuff - just wanted to keep the code short:
https://github.com/TVTower/TVTower/blob/master/source/Dig/base.util.deltatimer.bmx

bye
Ron

#### RemiD

#22
@Derron >> thanks, i will take a look.

edit : i can't convert your code to bb because some parts are missing.

but whatever, your deltacoef * speedcoef (modifier) looks like what i have already tried (what is explained in the gamedev article) and it seems to work for simple turns / moves, but it somehow decreases the amplitudes of the turns / movements when the 'physics' are more complex.

back to using delay for the moment, but i will do more experiments later...

#### Derron

Please show me the "more complex" physics.

As said ... there is one thing in physics we can almost rely on - and this is "time". As long as you do not go near or past the speed of light you can use it for many things.
Means if something goes "wonky" if it's time "advances slower" then the algorithms of that physic whatever are most probably bugged/incorrect.

Think of "impact" - the force of eg a bullet hitting a body (to calculate how far the body will be moved backwards). the force is the same as the mass is the same and the speed is the same (if bullet and body have the same "speed modifier").
And this is now the interesting part: if the bullet moves at "normal speed" and that body in "bullet time" (eg half time speed) it means it will hit the body with "double the speed" (relatively spoken) and thus the applied force will automatically be higher for the body (and thus for the bullet on impact too).

As said - it all should work as "time" is what you can use as base - and time is what you kinda "adjust".

I hope you find the time to toy around with it - you better trust what many articles suggest you to do (they cannot all be wrong here).

bye
Ron

#### RemiD

@Derron >> yes thanks for the code example  , i will have to spend more time studying it.
but not my priority right now.

thanks

#### Midimaster

#25
I updated your code from post#15 to a more precise version.

Using the Millisecs() for calculation is dangerous

The Millisecs() timer has two problems.
1.
Adding values to it returns unexpected inaccurate time steps.

2.
Calling mor often than 500 times a second is to inacurate.

I tested this:

to use...
`If Time<Millisecs()    Time = Millisecs() + 1`
...does not call this IF-branch 1000 times a second as expected, but only ~450 times.  More than 55% deviation!!!!

And also for a bigger time distance...
`If Time<Millisecs()    Time = Millisecs() + 2`
...does not call this IF-branch 500 times a second as expected, but only ~300 times. More than 33% deviation!!!!

You better add the time distance to Time%

`If Time<Millisecs()    Time = Time + 1`...calls the IF-branch ~950 times a second. Only 5% deviation!!!!

and for a bigger time distance...
`If Time<Millisecs()    Time = Time + 2` ...calls the timer 499-501 times a second. Only 0.2% deviation!!!!

The same results with MyMillisecs()

So we can note, that
1.
...usings the Millisecs()-timer for calculating the next time step is not adwised!!

2.
...calling the Millisecs() with a time distance of 0 or 1 makes to big deviations. But using time distances "2" and more brings high accuracy.

Switching to SlowMo and back

If you handle a lot of object with the MyMillisecs() function you have to care about the moments when switching from one speed to another. It can happen that some actors stand still for a while. because of (old) unvalid timestamps.

To prevent this I expanded the MyMillisecs() to handle all Motion-Speed-Changes, so that the user needs not to care about it. This shortens (and simpyfies) the code for moving the actors

so your BB-example would now look like this:

`Graphics( 800, 600, 32, 2 ); new line:Global LastFakeTime%, LastRealtime%, LastSloMo#, FakeTime%Global SloMo# = 1....Repeat     ...    If( Time < MyMilliSecs() )        ; new line:        Time = Time+2        Y = 600/2-10/2        X = X +1 : If( X > 800-1 ) : X = 0 : EndIf    EndIf    ....Until....;new function Function MyMillisecs%()    If LastSloMo=0 Then  FakeTime = MilliSecs()    If (SloMo <> LastSloMo)        LastSloMo    = SloMo        LastRealTime = MilliSecs()        LastFakeTime = FakeTime    EndIf     FakeTime = LastFaketime + (MilliSecs() - LastRealTime) / SloMo    Return FakeTimeEnd Function    `

This would now work with any number of actors in the game:

Here a example with 2 actors: a ball and a jumping rectangle. Individual timing for each actor

`Graphics( 800, 600, 32, 2 ) Global SloMo# = 1Global X#, Y#, BallX#Global LastFakeTime%, LastRealtime%, LastSloMo#, FakeTime%Global RectTime% = MyMilliSecs()Global BallTime% = MyMilliSecs() Repeat        If( KeyHit(57)=1 )    ;SPACE          If( SloMo = 1 )              SloMo = 4                  Else If( SloMo = 4 )              SloMo = 1          EndIf     EndIf     MoveTheRect     MoveTheBall    SetBuffer(BackBuffer())    ClsColor(000,000,000)    Cls()         Rect( X, Y, 10, 10, 1 )        Oval( BallX, 400, 10, 10, 1 )        Text( 0, 0, "/"+SloMo )        Flip 0Until KeyDown(1)Function MoveTheRect()    If RectTime >= MyMilliSecs() Then Return      RectTime = RectTime+2    ; calculations of the object    Y = 300 -Sin(x)*100    X = X +1        If( X > 800-1 ) Then X = 0     End Function Function MoveTheBall()    If BallTime >= MyMilliSecs() Then Return      BallTime = BallTime+5    BallX = BallX +1    If( BallX > 800-1 ) Then BallX = 0End Function  Function MyMillisecs%()    If LastSloMo=0 Then  FakeTime = MilliSecs()    If (SloMo <> LastSloMo)        LastSloMo    = SloMo        LastRealTime = MilliSecs()        LastFakeTime = FakeTime    EndIf     FakeTime = LastFaketime + (MilliSecs() - LastRealTime) / SloMo    Return FakeTimeEnd Function `
...back from Egypt

#### Derron

QuoteYou better add the time distance to Time%

`If Time<Millisecs()    Time = Time + 1`...calls the IF-branch ~950 times a second. Only 5% deviation!!!!

Now think about WHAT it actually does ...
Now think about WHEN it actually does these things.

What your code does is - it "catches up". So if there is a stutter after 500 ms for 300 ms ... then within these 300 ms you do not "update", timer will be 300ms "below" millisecs().

If now the update cycle happens more than 1 time per millisecond it will "catch" up (run the code and increase "time + 1").
If the update cycle happens less than 1 time a millisecond (so maybe takes 10 ms for some reason) it won't catch up ... it will still be behind the current time.

As update cycles are not guaranteed to require the same time these "time changes" will happen "Non-smooth".

And thus makes an animation based on this "time" value ... run ... "wobbly", "unsteady" or simply non-smooth.

It is much easier to track time since "last call" and calculate a fraction of a second which has passed - and do your physics/animation based on this.
Saves you from hoping to have 1000 update calls distributed along the millisecond slots accurately - this won't happen.
Also calculating all these things for 1000 times a second will require some ... cpu ticks/time.
Not to talk about potential hickups when a Garbage Collector decides to do clean up (important).

@Midimaster
You might track "Millisecs()" of each cycle ... and at the end store it in a csv for later examination (or display "afterwards" so you do not influence what is done "during" your simulation).
This will show how "smooth" cycles happen - or not. Then you can also add "random" delay(1-5ms) to simulate "cpu load" or so. Maybe it exposes a flaw (or maybe it shows that it works nicely).

bye
Ron

#### Midimaster

#27
Here is your code from post#13 with a realistic physic and use of a MyMilliTimer()

Code (vb) Select
`;slow motion effect using MyMillisecs() by RemiD ( 20230413 );while on ground, press space to jump;while on air, hold altleft to slow down time Graphics3D( 640,480,32,2 ) Global Camera = CreateCamera()CameraRange( Camera, 0.1, 100 )CameraClsColor( Camera, 000, 000, 000 )PositionEntity( Camera, +10, 5, 25 )Global Light = CreateLight(1)LightRange Light,1000PositionEntity Light, 10,50,20 ;groundGlobal ground_meshground_mesh = CreateCube() : ScaleMesh( ground_mesh, 300.0/2, 0.1/2, 300.0/2) : PositionMesh( ground_mesh, 0, -0.1/2, 0 ) EntityColor  ground_mesh, 0, 55, 0 ;thingGlobal thing_movespeed#Global thing_jumpspeed#Global thing_flipspeed#Global GravitySpeed# = 1Const GAME_FACTOR# =0.3;**** SLOW MOTION TIMER ************************************Global LastFakeTime%, LastRealtime%, LastSloMo#, FakeTime%, FlipTime%Global SloMo#=1 Global thing_root = CreatePivot()Global thing_mesh = CreateCubicMan()EntityColor( thing_mesh, 255, 222, 222)MoveEntity( thing_mesh, 0, +0.875, 0 )EntityParent( thing_mesh, thing_root, True )SetBuffer( BackBuffer() )Global ManTimer% = MilliSecs()PointEntity camera, Thing_rootMoveEntity camera, 0,0,15PointEntity light, thing_mesh Repeat MoveMan If KeyDown(56) SloMo=4 Else SloMo=1 EndIf RenderWorld() Flip 0        Until( KeyDown(1)=1 )Function MoveMan() If ManTimer>MyMilliSecs() Return ManTimer = MyMilliSecs() +12  If( EntityY(thing_root,True) < 0 )   ;is on ground   PositionEntity( thing_root, EntityX(thing_root,True), 0, EntityZ(thing_root,True) )   thing_jumpspeed = 0   GravitySpeed = 1 ElseIf( thing_jumpspeed = 0 )   If( KeyHit(57)=1 ) ;jump    thing_movespeed =  0.1    thing_jumpspeed =  0.20    thing_flipspeed =  7.95  EndIf Else ;is on air   thing_jumpspeed = thing_jumpspeed - (GravitySpeed-1) * GAME_FACTOR   GravitySpeed  = GravitySpeed*1.002   ;turn & move   TurnEntity( thing_mesh, thing_flipspeed , 0, 0 )   MoveEntity( thing_root, 0, thing_jumpspeed, thing_movespeed) EndIfEnd Function  Function MyMillisecs%() If LastSloMo=0 Then  FakeTime = MilliSecs() If (SloMo <> LastSloMo) LastSloMo    = SloMo LastRealTime = MilliSecs() LastFakeTime = FakeTime EndIf  FakeTime = LastFaketime + (MilliSecs() - LastRealTime) / SloMo    Return FakeTimeEnd Function Function CreateCubicMan() mesh = CreateMesh() head = CreateSphere(32) ScaleMesh    head, 0.25, 0.30, 0.25 PositionMesh head, 0, 0.92, 0 EntityColor  head, 255,155,111 EntityParent head, Mesh chest = CreateCube() ScaleMesh    chest, 0.24, 0.3, 0.1 PositionMesh chest, 0, 0.27, 0 EntityColor  chest, 255,0,111 EntityParent chest, Mesh arms = CreateCube() ScaleMesh    arms, 0.85, 0.08, 0.08 PositionMesh arms, 0, 0.6, 0 EntityColor  arms, 255,0,111 EntityParent arms, Mesh hips = CreateCube() ScaleMesh    hips, 0.26, 0.15, 0.11 PositionMesh hips, 0, 0, 0 EntityColor  hips, 255,255,0 EntityParent hips, Mesh leg1 = CreateCylinder() ScaleMesh    leg1, 0.08, 0.4, 0.08 PositionMesh leg1, -0.15, -0.4, 0 EntityColor  leg1, 255,155,111 EntityParent leg1, Mesh leg2 = CreateCylinder() ScaleMesh    leg2, 0.08, 0.4, 0.08 PositionMesh leg2, +0.15, -0.4, 0 EntityColor  leg2, 255,155,111 EntityParent leg2, Mesh Return meshEnd Function`
...back from Egypt

#### RemiD

@Midimaster >> i will take a look at your code later, thanks

( maybe edit the description of the code with your name and date so that future viewers are not confused... )

#### Midimaster

Ah... following your PM I understand thst the actor should not follow a gravity, but a special (unrealistic) movement. so I adjusted the values of my example to get closer to your inital movement
now the values are:

`GAME_FACTOR# =0.003ManTimer = MyMilliSecs() +4 thing_movespeed =  0.03 thing_jumpspeed =  0.07thing_flipspeed =  -1.4GravitySpeed  = GravitySpeed*1.0005`

This is the new version:
`;; RemiD's code from post#13 modified by midimaster;while on ground, press <SPACE> to jump;while on air, hold <ALT LEFT> + <SPACE> to slow down time Graphics3D( 640,480,32,2 ) Global Camera = CreateCamera()CameraRange( Camera, 0.1, 100 )CameraClsColor( Camera, 000, 000, 000 )PositionEntity( Camera, 25, 5, 35 )Global Light = CreateLight(1)LightRange Light,1000PositionEntity Light, 10,50,20 ;groundGlobal ground_meshground_mesh = CreateCube() : ScaleMesh( ground_mesh, 300.0/2, 0.1/2, 300.0/2) : PositionMesh( ground_mesh, 0, -0.1/2, 0 ) EntityColor  ground_mesh, 0, 55, 0Global GroundPivot=CreatePivot()PositionEntity GroundPivot, 0,5,0 ;thingGlobal thing_movespeed#Global thing_jumpspeed#Global thing_flipspeed#Global GravitySpeed# = 1Const GAME_FACTOR# =0.003;**** SLOW MOTION TIMER ************************************Global LastFakeTime%, LastRealtime%, LastSloMo#, FakeTime%, FlipTime%Global SloMo#=1 Global thing_root = CreatePivot()Global thing_mesh = CreateCubicMan()EntityColor( thing_mesh, 255, 222, 222)MoveEntity( thing_mesh, 0, +0.875, 0 )EntityParent( thing_mesh, thing_root, True )SetBuffer( BackBuffer() )Global ManTimer% = MilliSecs()PointEntity camera, Thing_rootMoveEntity camera, 0,0,15PointEntity light, thing_mesh  For i%=0 To 30 c= CreateCylinder() ScaleEntity c, 0.2,10,0.2  PositionEntity c, -10,-2,-100 + i*10Next ;WireFrame TrueRepeat  MoveMan PointEntity camera, GroundPivot  If KeyDown(56) SloMo=4 Else SloMo=1 EndIf  RenderWorld() Flip 0       Until( KeyDown(1)=1 )Function MoveMan() If ManTimer>MyMilliSecs() Return  ManTimer = MyMilliSecs() +4  If( EntityY(thing_root,True) < 0 )    ;is on ground   PositionEntity( thing_root, EntityX(thing_root,True), 0, EntityZ(thing_root,True) )   thing_jumpspeed = 0   GravitySpeed = 1   ElseIf( thing_jumpspeed = 0 )   If( KeyHit(57)=1 ) ;jump   thing_movespeed =  0.03    thing_jumpspeed =  0.07   thing_flipspeed =  -1.4 EndIf Else ;is on air   thing_jumpspeed = thing_jumpspeed - (GravitySpeed-1) * GAME_FACTOR   GravitySpeed  = GravitySpeed*1.0005   ;turn & move   TurnEntity( thing_mesh, thing_flipspeed , 0, 0 )   MoveEntity( thing_root, 0, thing_jumpspeed, thing_movespeed)   MoveEntity GroundPivot, 0, 0, thing_movespeed  EndIfEnd Function  Function MyMillisecs%() If LastSloMo=0 Then  FakeTime = MilliSecs() If (SloMo <> LastSloMo) LastSloMo    = SloMo LastRealTime = MilliSecs() LastFakeTime = FakeTime EndIf FakeTime = LastFaketime + (MilliSecs() - LastRealTime) / SloMo    Return FakeTimeEnd Function  Function CreateCubicMan()  mesh = CreateMesh()  head = CreateSphere(32) ScaleMesh    head, 0.25, 0.30, 0.25 PositionMesh head, 0, 0.92, 0  EntityColor  head, 255,155,111 EntityParent head, Mesh chest = CreateCube() ScaleMesh    chest, 0.24, 0.3, 0.1 PositionMesh chest, 0, 0.27, 0  EntityColor  chest, 255,0,111 EntityParent chest, Mesh arms = CreateCube() ScaleMesh    arms, 0.85, 0.08, 0.08 PositionMesh arms, 0, 0.6, 0  EntityColor  arms, 255,0,111 EntityParent arms, Mesh hips = CreateCube() ScaleMesh    hips, 0.26, 0.15, 0.11 PositionMesh hips, 0, 0, 0  EntityColor  hips, 255,255,0 EntityParent hips, Mesh leg1 = CreateCylinder() ScaleMesh    leg1, 0.08, 0.4, 0.08 PositionMesh leg1, -0.15, -0.4, 0  EntityColor  leg1, 255,155,111 EntityParent leg1, Mesh leg2 = CreateCylinder() ScaleMesh    leg2, 0.08, 0.4, 0.08 PositionMesh leg2, +0.15, -0.4, 0  EntityColor  leg2, 255,155,111 EntityParent leg2, Mesh Return meshEnd Function`
...back from Egypt