Jump to content

Sequentially repeating actions until completion


01271
 Share

Recommended Posts

I wanted to avoid a pyramid of if else and setTimeOut too, what I want to do is have a character make his way across the screen, from his "home" position, go to the enemy's position, apply damage, and then return.

 

So far I've been looking into promises to have code execute one after the other but that doesn't work with the game loop, a promise is a promise and it's just good for one loop operation.

Then I started working on a queuing function that runs a function every iteration until it returns true but it's spiralled out of control for complexity.

Code is included below.

I'm aware that my function for moving a player to a point doesn't work and that there are issues but those are things I can solve, what I can't solve is how do I make a sane, clean method for having the player queue through different actions to take?

 


game.PlayerFightEntity = me.Entity.extend({

	init: function(x, y, settings) {
		// call the constructor
		settings.height = 128;
		settings.width = 128;
		settings.image = me.loader.getImage("playerfight");
		this._super(me.Entity, 'init', [x, y, settings]);
		game.playerFight = this;
		this.alwaysUpdate = true;
		//target types = single, number, all, self.

		var speed = 0;
		this.animationState = 'home';
		this.queue = [];
		this.home = this.pos;
		this.targetPosition = this.home;

		this.startAttack = function(attackIndex, enemyIndex) {
			console.log('started attack.');
			this.attacks[attackIndex].activate(enemyIndex);
		};

		this.attacks = [{
			target: 'single',
			name: 'tackle',
			aspect: 'physical',
			damage: 10,
			activate: function(enemyIndex) {
				this.target = game.fightEnemies[enemyIndex].position;
				this.queue = [
					function() {
						this.battleFunctions.moveTo(this.targetPosition);
						return false;
					}.bind(this),
					function() {
						return this.battleFunctions.applyDamage(enemyIndex);
					}.bind(this),
					function() {
						return this.battleFunctions.moveTo(this.home);
					}.bind(this)
				];
			}.bind(this)
		}];

		this.runQueue = function() {
			var success = false;
			console.log(this.queue.length);
			if (this.queue.length > 0) {
				console.log(success);
				success = this.queue[0]();
				if (success) {
					this.queue.shift();
				}
			}
		};

		this.battleFunctions = {
			applyDamage: function(enemyIndex) {
				game.fightEnemies[enemyIndex].damage(10);
			},
			moveTo: function(target) {
				var angle = Math.atan2(target.y - y, target.x - x);
				var ySpeed = Math.sin(angle) * speed;
				var xSpeed = Math.cos(angle) * speed;
				this.pos.x *= xSpeed;
				this.pos.y *= ySpeed;
				console.log(target, angle, xSpeed);
				return (Math.round(this.pos.x) == target.x &&
					Math.round(this.pos.y) == target.y); // check if we've reached the point or even good enough.
			}.bind(this),

			moveToEnemy: function(enemyIndex) {
				this.moveTo(game.fightEnemies[enemyIndex].position);
			},
			returnHome: function() {
				this.moveTo({
					x: 10,
					y: 10
				});
			}
		};

		function attack(enemyIndex, attackID) {
			// setTimeout(this.endAttack(), 3000);
			this.attacks[attackID].activate(enemyIndex);
		}

		function endAttack() {
			// clean up stuff after attacking
			game.activeMenuSelector = 1;
		}
	},

	update: function(dt) {
		this.runQueue();
		this.body.update(dt);
		return true;
	},

	onCollision: function(response, other) {
		// Make all other objects solid
		return false;
	}
});

 

Link to comment
Share on other sites

Hello!

You might want a Finite-State Machine for this, since your described actions map perfectly to one. Here is some pseudo code to illustrate:

var FSM = {
  "fsmReset" : function () {
    /* Reset the state machine index */
    this.fsmIndex = 0;
  },

  "fsmUpdate" : function (dt) {
    /* Call current state */
    this.states[this.fsmIndex](dt);
  },

  "next" : function (dt) {
    /* Advance to the next state */
    this.fsmIndex = (this.fsmIndex + 1) % this.states.length;

    if (dt) {
      // If Delta-Time is provided, call the next state immediately
      this.fsmUpdate(dt);
    }
  },
};


var SoldierFSM = me.Entity.extend({
  "init" : function (x, y, settings) {
    this._super(me.Entity, "init", [ x, y, settings ]);

    /* Create a list of states for FSM */
    this.states = [
      this.idle,
      this.moveToEnemy,
      this.stopMoving,
      this.attackEnemy,
      this.returnHome,
      this.stopMoving,
    ];

    this.fsmReset();
  },

  "update" : function (dt) {
    /* Update FSM (calls the current state and advances states as necessary) */
    this.fsmUpdate(dt);

    this.body.update(dt);
    return this._super(me.Entity, "update", [ dt ]);
  },

  /* State implementations */

  "idle" : function (dt) {
    if (this.timeToChargeEnemy()) {
      this.next();
    }
  },

  "moveToEnemy" : function (dt) {
    this.walkToPosition();

    if (this.enemyInAttackRange()) {
      this.next();
    }
  },

  "attackEnemy" : function (dt) {
    this.attack();

    if (this.timeToReturnHome()) {
      this.next();
    }
  },

  "returnHome" : function (dt) {
    this.walkToPosition();

    if (this.madeItHome()) {
      this.next();
    }
  },

  "stopMoving" : function (dt) {
    this.body.vel.set(0, 0);
    this.next(dt);
  },

  /* Convenience methods used by the state implementations */

  "timeToChargeEnemy" : function () {
    /* TODO */
    return true;
  },

  "enemyInAttackRange" : function () {
    /* TODO */
    return true;
  },

  "timeToReturnHome" : function () {
    /* TODO */
    return true;
  },

  "madeItHome" : function () {
    /* TODO */
    return true;
  },

  "walkToPosition" : function () {
    /* TODO; something like this ... */
    this.body.vel = this.body.maxVel.scaleV(this.walkDirection);
  },

  "attack" : function () {
    /* TODO */
  },

}, FSM);

You can see the code can get long, but it has very shallow depth. This is a little more than pseudo code; it's almost a complete implementation. ;)

The idea here is that the FSM class is a mixin to be used with me.Entity (or other classes). And it's very simple; you can reset the state, advance the state, and call the current state.

The real meat and potatoes lies in the state list that you define; this list just points to functions which accept the update Delta-Time argument. On each frame, the current state function is called, and it decides when to advance to the next state. This can be a timer, a random event, an entity collision, etc. In the example above, the decision is just "return true", so the state will always advances immediately.

As you can see, an FSM can get very complicated, even though the API is dead simple. The states themselves execute on every frame, so it's important to keep that in mind. Mostly the states will probably do nothing except wait for some event so the FSM can be advanced. For more really advanced state trees, you can get crazy with things like a method to jump directly to any arbitrary state index, or even swap out the state list with an entirely new one.

Anyway, this will really keep your code clean, and it will run in lockstep with the engine. So you don't have to do anything tricky with async functions.

Link to comment
Share on other sites

That's pretty great! I've made my own solution of a "function queue" work while waiting and I'll get right on changing it into a finite state machine instead because that's what I wanted all along without knowing exactly how I'd do it.

This is my currently working code for archival purposes, don't want to just leave without showing the world my solution, if someone were to come looking it wouldn't be nice to them.

 


game.PlayerFightEntity = me.Entity.extend({

	init: function(x, y, settings) {
		// call the constructor
		settings.height = 128;
		settings.width = 128;
		settings.image = me.loader.getImage("playerfight");
		this._super(me.Entity, 'init', [x, y, settings]);
		game.playerFight = this;
		this.alwaysUpdate = true;
		//target types = single, number, all, self.

		var speed = 0;
		this.animationState = 'home';
		this.queue = [];
		this.home = this.pos;
		this.targetPosition = this.home;

		this.startAttack = function(attackIndex, enemyIndex) {
			console.log('started attack.');
			this.attacks[attackIndex].activate(enemyIndex);
		};

		this.attacks = [{
			target: 'single',
			name: 'tackle',
			aspect: 'physical',
			damage: 10,
			activate: function(enemyIndex) {
				this.targetPosition = game.fightEnemies[enemyIndex].pos;
				this.queue = [
					function() {
						return this.battleFunctions.moveTo(this.targetPosition);
					}.bind(this),
					function() {
						return this.battleFunctions.applyDamage(enemyIndex);
					}.bind(this),
					function() {
						return this.battleFunctions.returnHome();
					}.bind(this)
				];
			}.bind(this)
		}];

		this.runQueue = function() {
			var success = false;
			if (this.queue.length > 0) {
				success = this.queue[0]();
				if (success) {
					this.queue.shift();
				}
			}
		};

		this.battleFunctions = {
			applyDamage: function(enemyIndex) {
				console.log("attack!");
				game.fightEnemies[enemyIndex].takeDamage(10);
				return true;
			},
			moveTo: function(target) {
				var speed = 10;
				var angle = Math.atan2(target.y - this.pos.y, target.x - this.pos.x);
				var ySpeed = Math.sin(angle) * speed;
				var xSpeed = Math.cos(angle) * speed;
				this.pos.y += ySpeed;
				this.pos.x += xSpeed;
				return (Math.abs(this.pos.x - target.x) < 3 && Math.abs(this.pos.y - target.y) < 3); // check if we've reached the point or even good enough.
			}.bind(this),

			moveToEnemy: function(enemyIndex) {
				return this.moveTo(game.fightEnemies[enemyIndex].position);
			},
			returnHome: function() {
				return this.moveTo({
					x: 100,
					y: 300
				});
			}
		};

		function attack(enemyIndex, attackID) {
			console.log('attack!');
			// setTimeout(this.endAttack(), 3000);
			this.attacks[attackID].activate(enemyIndex);
		}

		function endAttack() {
			// clean up stuff after attacking
			game.activeMenuSelector = 1;
		}
	},

	update: function(dt) {
		this.runQueue();
		this.body.update(dt);
		return true;
	},

	onCollision: function(response, other) {
		// Make all other objects solid
		return false;
	}
});

 

Link to comment
Share on other sites

  • 3 weeks later...

I've been slacking off.

So far I've started integrating that previous solution into the game but there's a barrier to progress I've been encountering.
If I have the FSM class extending the init function, I get the error and trace:

TypeError: proto is undefined[Learn More]  melonJS.js:4184:21
api.pull /melonJS.js:4184:21
    game.BattleScreen<.onResetEvent /battle.js:18:26
    me.ScreenObject<.reset /melonJS.js:12456:13
    _switchState /melonJS.js:12651:17
    bound _switchState self-hosted

The battlescreen line is this: me.game.world.addChild(me.pool.pull("SoldierFSM", 100, 300, {width:128, height:128}));

and if I add an empty init, I get

TypeError: rect is undefined[Learn More]  melonJS.js:8301:13
    Quadtree.prototype.getIndex melonJS.js:8301:13
    Quadtree.prototype.insert melonJS.js:8401:25
    Quadtree.prototype.insertContainer melonJS.js:8358:21
    api.update melonJS.js:2872:21
    DebugPanel<.patchSystemFn/< debugPanel.js:290:17
    singleton.patch/<.value</< melonJS.js:28481:39
    _renderFrame melonJS.js:12613:13

If I have an init in the fsm with the _super init then my sprites are just invisible.

Link to comment
Share on other sites

you also need to register your object ( :

me.pool.register("SoldierFSM", game.SoldierFSM);

not that your SoldierFSM object is currently declared under the global namespace (based on the above comments), where you should rather do it under game (to work with the register example here) or your own namespace to avoid polluting the global one (low chances though to have somehting else also using SoldierFSM under the global scope).

Link to comment
Share on other sites

Well unfortunately the registration is already there, it's pulling into the game so it seems like it's the extension part isn't working.

me.pool.register("SoldierFSM", game.SoldierFSM); was already in my game project file beside my other registrations.

I tried to register the FSM itself

me.pool.register("FSM", game.FSM);

and it now complains about not having an init function.

I think extending the object is a problem for this case, is there a class without an init it can extend?

Link to comment
Share on other sites

the fsmreset is from the FSM class that parasyte wrote.

I can't find any documentation on extending classes like parasyte did. It makes it hard for me to know exactly what is going on.

I took the SoldierFSM and the FSM classes above and put them in melonjs to no avail because when the FSM class doesn't have an init, it screws over the SoldierFSM with an error and when it does, the SoldierFSM can't run his own init function he just runs the parents'.

 

At the end of the SoldierFSM class there's a "}, FSM);" so I know that's supposed to be how it's inheriting from the parent but I don't see overrides or inheritance working well on it so far.

Link to comment
Share on other sites

The FSM class that I wrote above is a mixin. See the documentation for info about mixins: https://github.com/parasyte/jay-extend#jayextend Technically the superclass is also just a mixin; but at least one of the mixins must provide an init method.

I made a mistake when I wrote the code (I didn't actually test it, lol! In my defense, I did say it was only Pseudo-code...) FSM should just be a bare object; don't pass the object to me.Object.extend(). Also there's another typo with a missing comma. Anyway, I fixed these two things above, and it should be working now! I tested it (this time for reals) with jay-extend. Works great.

Sorry for the confusion!

Link to comment
Share on other sites

  • 2 weeks later...

I can't believe I keep running into problems like this.

 

Ok so I have the following, but there's a scope issue with the array.

When I try to use my code, I get an error that 'TypeError: this.getEnemyPosition is not a function[Learn More]'.

This is because for some reason, the scope for 'this' in the procedures/states arrays is local to the array, as if I'd been using curly brackets.

    moveToEnemy: function(dt) {
        console.log(this);

gives me

Array [ game.PlayerFightEntity<.moveToEnemy(), game.PlayerFightEntity<.attackEnemy(), game.PlayerFightEntity<.returnHome() ]  fsm.js:76:3
 

Am I doomed to referring to functions externally by calling game.PlayerFightEntity.whatever instead of this.whatever? Doesn't seem like it would scale well.

 

var FSM = {
	"fsmReset": function() {
		/* Reset the state machine index */
		this.fsmIndex = 0;
	},

	"fsmUpdate": function(dt) {
		/* Call current state */
		this.states[this.fsmIndex](dt);
	},

	"next": function(dt) {
		/* Advance to the next state */
		this.fsmIndex = (this.fsmIndex + 1) % this.states.length;

		if (dt) {
			// If Delta-Time is provided, call the next state immediately
			this.fsmUpdate(dt);
		}
	}
};


game.PlayerFightEntity = me.Entity.extend({
	init: function(x, y, settings) {

		settings.image = me.loader.getImage("playerfight");
		settings.width = 128;
		settings.height = 128;
		this._super(me.Entity, "init", [x, y, settings]);

		this.alwaysUpdate = true;

		// create a bunch of different variables to hold for FSM position choosing.
		this.targetedEnemyID = 0;
		this.home = {
			x: 100,
			y: 100
		};

		/* Create a list of states for FSM */

		this.procedures = {
			tackle: [this.moveToEnemy, this.attackEnemy, this.returnHome]
		};

		this.states = [
			this.idle
		];
		this.fsmReset();
		game.playerFight = this;
	},

	update: function(dt) {
		// console.log(this.body.pos, "position");
		/* Update FSM (calls the current state and advances states as necessary) */
		this.fsmUpdate(dt);

		this.body.update(dt);
		return this._super(me.Entity, "update", [dt]);
	},

	/* State implementations */

	idle: function(dt) {
		// if (this.timeToChargeEnemy()) {
		// this.next();
		// }
	},

	getEnemyPosition: function() {
		return game.fightEnemies[this.targetedEnemyID].position;
	},

	moveToEnemy: function(dt) {
		console.log(this);
		var enemyPos = this.getEnemyPosition();
		this.walkToPosition(enemyPos, dt);


		if (this.enemyInAttackRange()) {
			this.next();
		}
	},

	attackEnemy: function(dt) {
		this.attack();

		if (this.timeToReturnHome()) {
			this.next();
		}
	},

	returnHome: function(dt) {
		this.walkToPosition();

		if (this.madeItHome()) {
			this.next();
		}
	},

	stopMoving: function(dt) {
		this.body.vel.set(0, 0);
		this.next(dt);
	},

	/* Convenience methods used by the state implementations */

	timeToChargeEnemy: function() {
		/* TODO */
		return true;
	},

	enemyInAttackRange: function() {
		/* TODO */
		return true;
	},

	timeToReturnHome: function() {
		/* TODO */
		return true;
	},

	madeItHome: function() {
		/* TODO */
		return true;
	},

	walkToPosition: function(x, y) {
		/* TODO; something like this ... */
		var angle = Math.atan2(target.y - this.pos.y, target.x - this.pos.x);
		var ySpeed = Math.sin(angle) * speed;
		var xSpeed = Math.cos(angle) * speed;

		this.body.vel.y -= this.body.accel.y * me.timer.tick;
		this.body.vel.y += this.body.accel.y * me.timer.tick;
	},

	attack: function() {
		/* TODO */
	},

	startProcedure: function(actionName) {
		this.states = this.procedures[actionName];
	},

	selectEnemy: function(enemyID) {
		this.targetedEnemyID = enemyID;
	}

}, FSM);

 

Link to comment
Share on other sites

Thanks for that. I've got another question now, I've got a finite state machine working but I want to have it pass variables to the functions.

I want my character to wait for an arbitrary amount of seconds before doing his next action.

 

	wait: function(seconds) {
		if (this.waitTimer !== null) {
			this.setTimeout(function() {
				this.waitTimer = null;
				this.next();
				return true;
			}.bind(this), seconds * 1000);
		}
	},

Yet this is inherently opposed to the previous design philosophy of having each state in a list like this:

			tackle: [this.moveToEnemy, this.wait, this.attackEnemy, this.returnHome]

 

With such a list, am I doomed to have each "state" in a hash with the values I'll run on it?

Is it possible to store them with their future use values without doing a hack job of it? IE

 

tackle: [this.moveToEnemy, this.wait(5), this.attackEnemy(10), this.returnHome]

Link to comment
Share on other sites

The primary way to achieve this (function currying) in JavaScript is through closure bindings.

But I think what you really want is to imply the state from the caller. E.g. whoever is calling `this.next()` should be responsible for passing any arguments to the next state. In the original example, only a single argument can be passed, and that's the `dt` delta time. Likewise, all state methods receive the `dt` argument first, as you can see in the `fsmUpdate` implementation. It can be extended to pass arbitrary arguments on the `next` and `fsmUpdate` methods. I'll leave it up to you to play with, but the magic `arguments` reference (and Function.prototype.apply()) will help you here. Optionally, you can use rest parameters if you're on ES6.

There's one tricky point that will probably catch you off-guard, though, and that's that every state executes every frame. So your wait method gets called every frame and it will create new timers on each call (notice your `this.waitTimer` is not nullified until the timeout expires). Remember that the state machine *is* state, so you should create a timeout and immediately advance to the next state (which simply does nothing). When the timeout expires, it advances to the *next* state which will continue doing useful work. This is the only state you need; conditions like `if (this.waitTimer !== null)` are kind of useless in a state machine.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...