ambiera logo

Ambiera Forum

Discussions, Help and Support.

folder icon Ambiera Forum > CopperCube > Programming and Scripting
forum topic indicator Is using
person icon
dekon_17
Registered User
Quote
2023-08-15 18:14:49

My previous version of AI was made, pretty much, entirely out of "if-else" statements. From what I know, it's not a particularly good approach to making AI for enemies. So, instead, I tried searching for what a "state machine" is, and after finding pretty much nothing (if certain examples based on some library that is definetly not present in Coppercube), I decided to make... Somewhat of a state machine. For that, I used "switch" loop, and a formula that would declare the state.

The whole reason why I am asking this question is, firstly, the formula itself being "quite lengthy", and, secondly, because I already heard about one guy who made pretty much an entire game with "if-else", and it wasn't really great.

This is how formula looks:
if (HealthDif < 0)
HealthDif = 0;

var State = Math.ceil (HealthDif) * (1 + Alert * 2 + (SoundHeard - SoundHeard * Alert) + this.Aimed * NoCol + 2 * !NoCol * Alert);
There are (currently) six states - 0 for being dead; 1 for, basicaly, being alive and detecting player in front; 2 for reacting to sounds (only shooting right now); 3 for aiming; 4 for shooting; 5 for following player.

I am a bit worried because amount of calculations seem to be quite heavy, I don't know if this even is more optimized than a bunch of "if-else"-s.

So... if there are some people who understand this subject, is this approach better than making a lot of "if-else"-s (I believe, I mentioned this a lot already), or am I being, to put it more culturally, "not smart enough"?

Edit: it seems like forum cut the name of this thread. It was saying something like 'Is using "switch" better than "if-else" for AI?'. Weird.


person icon
okeoke
Registered User
Quote
2023-08-15 21:57:29

Hi dekon_17,

I believe, it's better to use a state machine.
You can find a nice video explaining how it works here:https://youtu.be/_Hv9eyero6o. In the video this guy uses TS, but if I remember correctly he pretty much goes through the whole implementation.

I also rewrote the same code to use with coppercube at some point. It should be working, but I'm not 100% sure, since I modified it a lot after that for specific games. This one should do the same as video tutorial.

function StateMachine() {
this.states = {};
this.currentState;
this.changeStateQueue = [];
this.isChangingState = false;
}

StateMachine.prototype.addState = function (name, config) {
if (!config) {
config = {};
}

var state = {
name: name
};

if (config.onEnter) {
state.onEnter = config.onEnter.bind(this);
} else {
state.onEnter = function () { };
}

if (config.onUpdate) {
state.onUpdate = config.onUpdate.bind(this);
} else {
state.onUpdate = function () { };
}

if (config.onExit) {
state.onExit = config.onExit.bind(this);
} else {
state.onExit = function () { };
}

this.states[name] = state;

return this;
}

StateMachine.prototype.setState = function (name, node) {
if (!this.states[name]) {
print('Tries to set the unknown state ' + name);
return;
}

if (this.currentState && this.currentState.name === name) {
return;
}

if (this.isChangingState) {
this.changeStateQueue.push(name);
return;
}

this.isChangingState = true;

if (this.currentState && this.currentState.onExit) {
this.currentState.onExit(node);
}

this.currentState = this.states[name];
print(ccbGetSceneNodeProperty(node, 'Name') + ' changes state to ' + name);

if (this.currentState && this.currentState.onEnter) {
this.currentState.onEnter(node);
}

this.isChangingState = false;
}

StateMachine.prototype.update = function (node, delta) {
if (this.changeStateQueue.length > 0) {
this.setState(this.changeStateQueue.shift());
}

if (this.currentState && this.currentState.onUpdate) {
this.currentState.onUpdate(node, delta);
}
}

Function.prototype.bind = function (newThis) {
if (typeof this !== "function") {
throw new Error(this + "cannot be bound as it's not callable");
}
var boundTargetFunction = this;
return function boundFunction() {
return boundTargetFunction.apply(newThis);
};
};


person icon
okeoke
Registered User
Quote
2023-08-15 22:00:34

In order to use it, you inherit your behavior class from it:

// constructor
var behavior_TestStateMachine = function () {
}

// inherit from state machine class
behavior_TestStateMachine.prototype = Object.create(StateMachine.prototype);
behavior_TestStateMachine.prototype.constructor = behavior_TestStateMachine;

// onanimate
behavior_TestStateMachine.prototype.onAnimate = function (node, timeMs) {
// do your stuff here
}
//


If you never used this before you can read about ES5 inheritance here:https://eli.thegreenplace.net/20...

You also need Object.createa polyfill to use in coppercube desktop:

Object.create = function (o) {
function F() { }
F.prototype = o;
return new F();
};


person icon
okeoke
Registered User
Quote
2023-08-15 22:08:46

Now you add states to your behavior class:

behavior_TestStateMachine = function () {
StateMachine.call(this);
this
.addState('idle', {
onUpdate: this.idleOnUpdate
})
.addState('move', {
onUpdate: this.moveOnUpdate
})
.addState('attack', {
onUpdate: this.attackOnUpdate
});
}


Remember to call StateMachine.call(this) - I believe, the article about inheritance contains detailed information why it is needed.

Set initial state and call update() method inside your on animate:

behavior_TestStateMachine.prototype.onAnimate = function (node, timeMs) {
if (!this.lastTime) {
this.lastTime = timeMs;
this.setState('idle', node);
return;
}

var delta = timeMs - this.lastTime;
this.lastTime = timeMs;
if (delta > 200) delta = 200;

this.update(node, delta);
}


Each of your states is a separate method:

behavior_TestStateMachine.prototype.idleOnUpdate = function (node, delta) {
//put idle logic here
}

behavior_TestStateMachine.prototype.moveOnUpdate = function (node, delta) {
//put move logic here
};

behavior_TestStateMachine.prototype.attackOnUpdate = function (node, delta) {
//put attack logic here
};



person icon
okeoke
Registered User
Quote
2023-08-15 22:11:28

If you need some additional actions then state machine enters or exists some specific step you can redefine onEnter and onExit methods for each state.

You switch to another state like:

this.setState('attack', node);

Assuming you're switching from 'idle' to 'attack' state, it will execute idle on exit method once, than attack on enter method once, and than will call attack on update method every frame.

I believe, that is the cleanest approach that you can use.

person icon
dekon_17
Registered User
Quote
2023-08-21 07:59:43

Woah. Didn't expect that state machines can be this big (my code right now is 278 lines, that includes pathfinding, which takes almost a third of that, and 4 functions besides required one - "onAnimate"). I will try to do something like that, but I can't say if I will be able to get this done. I also suppose that it might be bigger than what I have now. However, as I said, "I suppose", so, I am not sure about all that.

Regardless, thanks for information, okeoke, couldn't find all that before on my own.

person icon
Dieter
Guest
Quote
2023-08-21 11:01:00

Well, I've heard that too, that a bunch of IF structures are deemed amateurish. But I don't agree. A lot of academic coders declare some coding standards, but never code anything by themselves, Or then it's bugged bloatware.

Sure bad code can contain lots of IFs, but so can good code. The point is, a state machine does not exclude IF structures at all, but they make it more effective. Trying to squeeze all functionality into a single formula is overcomplicating the task in my view.

What is much more important in a state machine, is a priority system, that switches states, based on a "current mode" code and its priority, that works more like the charts, rather than like nested IF structures, that are hard to maintain.

But you can still use a ton of IFs in order to attest a certain mode to a certain NPC. But that's just a number. The respective actions are then executed in the state machine based on the current mode alone, which is just a number, a code, like I said. It helps to keep the actions structured, while allowing wide-spread, unstructured mode influencers.

Just my opinion.


Create reply:










 

  

Possible Codes


Feature Code
Link [url] www.example.com [/url]
Bold [b]bold text[/b]
Image [img]http://www.example.com/image.jpg[/img]
Quote [quote]quoted text[/quote]
Code [code]source code[/code]

Emoticons


icon_holyicon_cryicon_devilicon_lookicon_grinicon_kissicon_monkeyicon_hmpf
icon_sadicon_happyicon_smileicon_uhicon_blink   






Copyright© Ambiera e.U. all rights reserved.
Contact | Imprint | Products | Privacy Policy | Terms and Conditions |