Jump to content

[TOPIC: topicViewTemplate]
[GLOBAL: userSmallPhoto]
Photo

Object state tracking techniques part deax
Started by laurasweet8888 Feb 27 2017 10:06 AM

- - - - -
8 replies to this topic
transitions objects states
[TOPIC CONTROLS]
This topic has been archived. This means that you cannot reply to this topic.
[/TOPIC CONTROLS]
[modOptionsDropdown]
[/modOptionsDropdown]
[reputationFilter]
[TOPIC: post.html]
#1

laurasweet8888

[GLOBAL: userInfoPane.html]
laurasweet8888
  • Contributor

  • 111 posts
  • Corona SDK

This is a continuation of my first post but since it is a tangent I thought I would start a new thread.  In any case, I was thinking about getting my objects to change states in an orderly fashion and it was brought up in the last thread that I should just cancel my old transition before starting a new one.  As I thought that was a great idea I got to thinking that I do stuff in the callback for that first transition.  For example: 

 

What if I don't want the gamer to be able to move the hero while he is rotating from left to right. Otherwise, it looks stupid because hero is pulled sideways.  So I have some logic that says 

 

movefunction()

    if not hero.isRotating then 

       movehero()

   end 

end 

 

Later in the code I set the hero.isRotating flag on right before the transition begins and depend on the callback to set if off.  

 

If I want to do something else that needs to cancel the first transition, it also cancels the run of the callback function and the isRotating flag is stuck on which of course leaves the hero stuck in the middle of the screen not accepting input from anything because he thinks he is rotating.  Who here thinks the callback should run when the transition is cancelled?  There are probably cases where you would need it and cases where it messes things up.  Who knows.   

 

Just has a test case I ran this: 

local function testsomething ()
		local function callback1 (obj)
			print("test case listener1 ended")
		return 
		end 
		print ("start of test case --------------------------------------")
		local test1 = transition.to(hero.image, {time=4000, delay=0, alpha = 1 , transition = easing.inOutExpo , onComplete = callback1 })
		transition.cancel(test1)
	return 
	end 

Callback1 was never called.  

 

This is probably standard stuff for the seasoned game programmer but I thought I would share just in case someone else might benefit.  

 

So in my case,  if the rotation is less than 100 degrees I use a transition to rotate him.  Over 100 degrees I trigger a sprite animation and depend on the spritelistener to set the rotation flag off.  So there is the added complexity of syncing up transitions and sprite animations.  

 

fun times,  fun times.  



[TOPIC: post.html]
#2

Rob Miracle

[GLOBAL: userInfoPane.html]
Rob Miracle
  • Moderator

  • 25,243 posts
  • Enterprise

Let me chime in and give you a different way to look at this.

 

When a transition is running, while know at the start when it will complete, your code is likely going to be off doing other things when it does complete, so having an onComplete option lets you explicitly execute code when your transition is done.

 

However, when you cancel a transition, the code cancelling it is active at that moment and you're in control of what happens next. If you want your onComplete listener to fire, simply call the function after you cancel the transition:

transition.cancel(test1)
callback1( hero.image )

You've accomplished your goal without changing the definition of onComplete (since "cancelled" is not "complete").

 

Rob



[TOPIC: post.html]
#3

laurasweet8888

[GLOBAL: userInfoPane.html]
laurasweet8888
  • Contributor

  • 111 posts
  • Corona SDK

Rob,  

 

    I agree with you in this simple case.  It is all about simplifying complexity for me.  I am assuming an image can only have one active transition because the callbacks get messed up otherwise.  So when I want to cancel one to start a new one I need to know which callback to callback.  Hero could be blinking, doing some sprite movement, spawning, etc...   So the hero object needs to track these things and provide the ability to cancel the one that is currently running.  

    I just wanted to share how not to do it so people wouldn't run into the same bugs.  Logic errors are hard to find let me tell ya.  When I wanted hero to blink I just fired off a new transition with a new callback.   This worked most of the time but some times I was left hanging when the transition callbacks clobbered each other.  



[TOPIC: post.html]
#4

laurasweet8888

[GLOBAL: userInfoPane.html]
laurasweet8888
  • Contributor

  • 111 posts
  • Corona SDK

As I dig around in the code to make things better and more consistent, I see that I did this when I wanted hero to blink 5 times.  

function HF:zapped()
		local function zapped_callback( obj )
			local objCT = obj.containerTable
			objCT.zapped_count = objCT.zapped_count + 1 
			print ("zapped_callback" , objCT.zapped_count)
			if  objCT.zapped_count == 5 then 
				transition.cancel(objCT.zapped_trans) 
				transition.to(obj, {time=0, alpha= 1, transition = easing.inExpo } )	
				objCT.isZapped = false 
			end	
		end 
	if not self.isZapped then 
		self.isZapped = true 
		self.zapped_trans = transition.blink(self.image, {time=1000, onRepeat = zapped_callback  } )	
		self.zapped_count = 1				
	end 			
return
end 

So in this case, I am using onRepeat instead of onComplete.  So if  I just cancel the transition the hero could possibly be left in a invisible state.  The cancel of each transition needs to be handled appropriately for the type of transition that is currently running.  So tracking which transition is running is important so that the proper cancel technique can be employed. 



[TOPIC: post.html]
#5

davebollinger

[GLOBAL: userInfoPane.html]
davebollinger
  • Corona Geek

  • 1,296 posts
  • Enterprise

  So tracking which transition is running is important so that the proper cancel technique can be employed. 

 

I tend to shy away from using transitions to keep track of "state", that is, I don't use them for anything related to "logic".  They're fine for "fire-and-forget" visual effects, but using them for logic tends to lead to a spaghetti code of onComplete's that can be a nightmare to untangle.  If you peruse the recent forum posts you'll find several current threads related to convoluted transition logic.
 
So, let's consider the opposing extreme - using an enterFrame loop for everything - just for comparison.  (there may exist a happy balance between the two approaches, but you'll have to determine what it is based on your own usage - this opposite extreme is offered just to illustrate alternatives)
 
Within an enterFrame loop you can easily set up some sort of FSM (Finite State Machine) as per your original topic.  It need not be formally implemented, something ad-hoc would likely suffice - the point is that YOU will be entirely in control of what happens during each and every frame.
 
Unless your FSM is very elaborate and detailed, you'll likely find it easier to "blend" a set of discrete and exclusive finite states with some other non-exclusive states.  "Huh?" you say?  Ok, for example...
 
"Standing", "Running", "Jumping", "Swimming", "Flying", etc, might all be considered exclusive states - if you're doing one, then you are not doing any other.  That tends to look something like this:
 
object.enterFrame = function(self)
  if (self.state=="running") then
    -- do running stuff
  elseif (self.state=="jumping") then
    -- do jumping stuff
  elseif ...
  -- where each state could potentially transition to another state for the NEXT frame
  -- based on some type of "controller" input
  -- for instance if currently running, and jump button pressed, then state = "jumping"
  -- but if currently jumping, then jump button ignored (unless you support double-jumps)
Non-exclusive states tend to be things like "wasRecentlyInjured", "didRecentlyCollectPowerup" - things that might apply during any of the exclusive states.  (that is, you can be injured while running/flying/etc, for example)  These states might make your character blink red, or glowing with power-up stars, or whatever.  These are typically NOT part of the if/elseif structure above, but handled and tracked separately (ie, not using the ".state" property, but rather their own dedicated state property).
 
  if (self.injured==true) then -- roughly equivalent to your "isZapped"
    if (self.injuredCountdown%20<10) then
      self:setFillColor(1,0,0) -- blink red
    else
      self:setFillColor(1,1,1) -- unblink
    end
    self.injuredCountdown = self.injuredCountdown - 1
    if (self.injuredCountdown <= 0) then
      self.injured = false
      self:setFillColor(1,1,1) -- and make sure we're unblinked
    end
  end
No transitions needed, no cancel()'s needed, no onComplete's needed.  I'd probably add "helpers" to setup all the state changes, to keep things "simpler" elsewhere, for example:
 
object.becomeInjured = function(self)
  self.injured = true
  self.injuredCountdown = 100 -- # frames to do the blinky thing
  -- (clever readers might see how the above two could be combined into one,
  -- but left separate here to help clarify intent of code)
end
etc, hth


[TOPIC: post.html]
#6

laurasweet8888

[GLOBAL: userInfoPane.html]
laurasweet8888
  • Contributor

  • 111 posts
  • Corona SDK

 

 

 but using them for logic tends to lead to a spaghetti code of onComplete's

 

I am living the dream here let me tell ya. 

 

I definitely get what you are saying and like it but...

 

1.  I like the way the transitions look. So in your example would you fire off a transition and then set a counter and just assume the transition completes in X number of frames.  

 

2.  Would I do the same for sprites?  Normally, I would play a sprite seq to do something and put self in some state.  The sprite listener would set that state off when the seq completes.   It does create the same spaghetti nonsense though and I don't like it for sure.

 

L

 

PS.   This kind of change at this point would be a pretty major change in game structure.  Not looking forward to that.  :(



[TOPIC: post.html]
#7

davebollinger

[GLOBAL: userInfoPane.html]
davebollinger
  • Corona Geek

  • 1,296 posts
  • Enterprise

1.  I like the way the transitions look. So in your example would you fire off a transition and then set a counter and just assume the transition completes in X number of frames.  

 

1) In the "extreme opposite" approach above?, no.  Keep in mind that an enterFrame loop can do everything a transition can, including calling easing functions, etc, it'll just be less "convenient".  So.. have a counter, calc a "t" value for easing if/as necessary (t=counter/maxCounter), apply modified values to object, when timer is up then your "faux transition" is complete, transition state as desired.

 

2) it would increment the frame of the sprite sequence manually, then transition state as desired

 

But maybe you're asking about a "hybrid" rather than "extreme" approach, right?  In that case, you'd want various "sit and spin" states that just do nothing except check for some external input (triggered elsewhere by an onComplete, fe) and when it's received then change state as desired.  You'd still have the same potential problems with cancel's though - your state might never advance (because onComplete never triggers the controller input).  So you still have to cancel with care, and/or build in some failsafe "timer/counter" as you suggest, and advance anyway.  (yuck, that's "logic by luck" as i call it, rather than by design)



[TOPIC: post.html]
#8

laurasweet8888

[GLOBAL: userInfoPane.html]
laurasweet8888
  • Contributor

  • 111 posts
  • Corona SDK

I now see that the use of callbacks is very problematic and should be used sparingly.  This seems to be created by a SDK limitation that one callback can overwrite another.  Luckily, the SDK does allow for 2 transitions to be going at the same time.  So I noticed that I can make hero blink in one transition and rotate in another.  This is important to me because the rotate logic runs based on a screen touch listener and is nowhere near the update hero logic.  This allows this to be possible: 

 

gameloop:

if  hero.isZapped then 
				if not hero.isZappedbegun then -- start being zapped 
					hero.isZappedbegun = true 
					hero.isZappedtrans = transition.blink(hero.image , { time=1000, transition = easing.inExpo} )
				end 
				if hero.isZappedendtime < currenttime then  -- end zapping 
					hero.isZapped = false 
					hero.isZappedbegun = false 
					transition.cancel ( hero.isZappedtrans )
					hero.isZappedtrans = transition.to ( hero.image , { time=250, alpha = 1 ,transition = easing.inExpo} )
				end 
			else	
			end 

Collision Listener: 

 

hero:becomezapped( 5 ) -- blink X times

--  called from hero collision listener 
function HF:becomezapped( count )
	local cnt = count or 3 
	if not self.isZapped then -- only one zap at a time. 
		self.isZapped = true 
		self.isZappedbegun = false 
		self.isZappedcount = cnt 
		self.isZappedendtime = GlblData.currenttime + cnt 
	end 	
return 
end 

This logic avoids the use of callbacks and allows for the use of transitions if they are preferred.  Note that the begin logic can be enhanced with other things like,  sprite animations,  color changes, spawning of other objects like stars around his head or some indication of loss of life.  

Let's say I want to turn him yellow at the same time, I could do this: 

self.image.fill.effect = "filter.monotone"
 
	self.image.fill.effect.r = 1
	self.image.fill.effect.g = 1
	self.image.fill.effect.b = 0
	self.image.fill.effect.a = .7

image is really a sprite in my case but it does turn it yellow for all the sprites in the sequence.  That is really nice, so I don't have to go back to blender and create a whole other version of hero with a yellow tint.   



[TOPIC: post.html]
#9

davebollinger

[GLOBAL: userInfoPane.html]
davebollinger
  • Corona Geek

  • 1,296 posts
  • Enterprise

That seems like a reasonable "compromise" use of transitions for a non-exclusive state. :)

 

I'll just add one more bit about FSM's (if you choose to use one for exclusive states)  -- because Lua is a good fit for them.  Let's say you eventually end up with like 20 states (not an unreasonable number), then even a big if-elseif test for your state branch starts to become cumbersome.  So table it!  For example, setting up a state table..

local stateTable = {}
stateTable["running"] = function(entity) print(entity.name .. " is running") end
stateTable["flying"] = function(entity) print(entity.name .. " is flying") end
-- etc, for ~20 more states..

Because now you could dispatch really simply like so:

stateTable[player.state](player)

Although in practice, you'd potentially want each entity to have it's own state table, so it really looks more like:

player.stateTable[player.state](player) 

This approach also allows you to easily "modularize" your states (if your game is getting big enough to warrant it), fe

stateTable["running"] = require("game.player.state.running")
stateTable["swimming"] = require("game.player.state.swimming")

..where each of those require's loads in a single dedicated state function.  They'd each look something like this

-- game.player.state.running
return function(entity)
  print(entity.name .. " is running")
end

Then, once you had that much in place, (whew!), you could expand each state function to be its own state object with separate methods for a one-time start of state, an ongoing update of state, and a one-time exit of state, for example:

-- game.player.state.running
return {
  enter = function(entity)
    print(entity.name .. " will be running")
  end,
  update = function(entity)
    print(entity.name .. " is running")
  end,
  leave = function(entity)
    print(entity.name .. " is no longer running")
  end,
}

Then, having a stateTable full of those objects, in your "state change" helper, you'd call it something like:

function changeState(entity,newstate)
  entity.stateTable[entity.state].leave(entity) -- leave the old state
  entity.state = newstate -- assign the new state
  entity.stateTable[entity.state].enter(entity) -- enter the new state
end

and your "state update" helper looks something like:

function updateState(entity)
  entity.stateTable[entity.state].update(entity)
end

and so on,.. fwiw, hth




[topic_controls]
[/topic_controls]