Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
writing basic AI
#1
I am currently trying to write a basic AI equal or better than the original. I already have quite a bit of it working and I want to lay down the path of it's creation here: function by function, explaining everything along the way.

planned featrues (Click to View)

Please comment on what features you would add, where you would write the script differently to make it better, or just what function you would like to see next or better explained.

So I will start of with the main function, the sequence that runs once every frame the game updates it's screen. I will keep it as simple as possible for now and exclude every feature I am currently not working on.
    C-Code:
void id(){
//loading
   array<int>o = get_objects();
//reset
   inputs();
//movement
   if( o[0] != -1 && attack_dodgeable(o) ) { dodge(o); }
   else if( armed(self.num) || closer_than(o[1],o[3]) ) { approach_opponent(o); }
   else if( !armed(self.num) && closer_than(o[3],o[1]) ) { approach_item(o); }
//action
   if( o[0]!=-1 && !attack_dodgeable(o) ){ react(o); }
   else{ act(o); }
}
As you can see I have divided the main function into four sections

//loading
The get_objects function will mostly gather all relevant object numbers so they can later be loaded whenever they are needed. I am using an array to store them here, which is like a variable with many slots. I just called it "o" for object. Slot 0 (o[0]) will contain the object number of the attack that will first hit our character, slot 1 (o[1]) the closest opponent, slot 2 the second closest opponent and slot 3 the most desired item. The get_object function may eventually return more object numbers, but that's it for now.
//reset
The inputs function will simply reset all keys to stop the character from pressing them so he can perform new actions.
//movement
This section will decide on how the character will move, so it will only deal with how the directions keys will be pressed:
  • If there is an attack (slot 0 is not -1) and it can be dodged the character will move to doge it. I'm giving the two new functions here the whole set of loaded object numbers instead of only the attacks object number so it is possible to prevent running from one danger into another.
  • If there was no attack to be dodged and the opponent is either closer than the item, or the character already holds an item he will approach his opponent.
  • If the item is closer than the opponent and the character is unarmed he will approach the item.
//action
The last section will decide on how the character will act, that means defend, jump, attack and special moves:
  • If there is an attack that was not dodgeable the character will react to it.
  • If there was no attack to react to the character will be able to act freely.

As you can see I have not included teammates, criminals and commands yet. I do have the logic for criminals and commands already working, but I do not want to deal with testing in stage mode as long as the AI isn't good in vs mode and the commands cannot work perfectly with the current ddraw.dll yet - maybe they do, I have to try again.

If you aren't interested in anything else the next post will feature the get_objects function.
Reply
Thanks given by: Silverthorn , mfc , John Fighterli , Marko
#2
Important feature: Jan/John/Sorcerer actively seeking out and healing teammates with low hp, but sufficient dark hp.
My Creations: (Click to View)

Return (String) System.getNewsOfTheDay();
Barely active, expect slow responses. If at all.


Greetz,
Alblaka
Reply
Thanks given by:
#3
(08-19-2012, 03:34 PM)Alblaka Wrote:  Important feature: Jan/John/Sorcerer actively seeking out and healing teammates with low hp, but sufficient dark hp.

That is ID specific.

Here is a picture of a duck in a cup:
[Image: doty7Xn.gif]

10 ʏᴇᴀʀs sɪɴᴄᴇ ɪʀᴄ ɢᴏᴏᴅ.ɪ ᴡᴀʟᴋ ᴛʜʀᴏᴜɢʜ ᴛʜᴇ ᴇᴍᴘᴛʏ sᴛʀᴇᴇᴛs ᴛʀʏɪɴɢ ᴛᴏ ᴛʜɪɴᴋ ᴏғ sᴏᴍᴇᴛʜɪɴɢ ᴇʟsᴇ ʙᴜᴛ ᴍʏ ᴘᴀᴛʜ ᴀʟᴡᴀʏs ʟᴇᴀᴅs ᴛᴏ ᴛʜᴇ ɪʀᴄ. ɪ sᴛᴀʀᴇ ᴀᴛ ᴛʜᴇ sᴄʀᴇᴇɴ ғᴏʀ ʜᴏᴜʀs ᴀɴᴅ ᴛʀʏ ᴛᴏ sᴜᴍᴍᴏɴ ᴛʜᴇ ɢᴏᴏᴅ ɪʀᴄ. ɪ ᴡᴀᴛᴄʜ ᴏᴛʜᴇʀ ɪʀᴄ ᴄʜᴀɴɴᴇʟs ʙᴜᴛ ɪᴛ ɪs ɴᴏ ɢᴏᴏᴅ. ɪ ᴘᴇsᴛᴇʀ ᴢᴏʀᴛ ᴀɴᴅ ᴛʀʏ ᴛᴏ ʀᴇsɪsᴛ ʜɪs sᴇxɪɴᴇss ʙᴜᴛ ɪᴛ ɪs ᴀʟʟ ᴍᴇᴀɴɪɴɢʟᴇss. ᴛʜᴇ ᴇɴᴅ ɪs ɴᴇᴀʀ.ɪ ᴛʜᴇɴ ᴜsᴜᴀʟʟʏ ʀᴇᴀᴅ sᴏᴍᴇ ᴏʟᴅ ɪʀᴄ ʟᴏɢs ᴀɴᴅ ᴄʀʏ ᴍʏsᴇʟғ ᴛᴏ sʟᴇᴇᴘ.


Reply
Thanks given by:
#4
(08-19-2012, 03:34 PM)Alblaka Wrote:  Important feature: Jan/John/Sorcerer actively seeking out and healing teammates with low hp, but sufficient dark hp.
yes i am planning to include loading the teammate that requires healing most (or the group need in general)
it can be used for healing moves later but as silva said this does not directly belong to writing a new basic ai
i want to first use it to help out the weakest character or call the team together - i added it to the list

@silva: please spoiler your cup duck
thank you

Moving on with the get_objects function, it will return an array of object numbers that we can later base all decisions on.

    C-Code:
int[] get_objects(){
   array<int>o = {-1,-1,-1,-1};
   for ( int i = 0; i < 400; ++i ){
      if( is_object(i) && target.team != self.team ){
 
//get attack that will hit first
         if( hits_sooner_than(i,o[0]) ){
            o[0] = i;
         }
 
//get closest opponent
         if( is_opponent(i) && ( ( closer_than(i,o[1]) && is_opponent(o[1]) ) || !is_object(o[1]) ) ){
            o[1] = i;
         }
 
//get second closest opponent
         else if( is_opponent(i) && ( ( closer_than(i,o[2]) && is_opponent(o[2]) ) || !is_object(o[2]) ) ){
            o[2] = i;
         }
 
//get closest item
         if( is_item(i) && ( ( closer_than(i,o[3]) && is_item(o[3]) ) || !is_object(o[3]) ) ){
            o[3] = i;
         }
      }
   }
   return o;
}


As usual with finding certain objects I have a for loop that goes through all 400 objects there are. Before that I create and fill the array that will be used to store the object numbers with -1 values, which will remain in case objects for a certain category are not found.
In case the currently loaded number inside the loop belongs to an existing object that is on another team it is further checked whether it is a dangerous attack, an opponent or an item.

//get attack that will hit first
If the object will hit the character sooner than the previously loaded object its number will be written to o[0] (slot 0).
This can be an attack, opponent or item. Anything really.
//get closest opponent
If the object is an opponent and closer than the previously loaded opponent (or the first opponent loaded) its number will be written to o[1] (slot 1).
//get second closest opponent
If the object is an opponent and not closer than the so far closest opponent but closer than the previously second closest one its number will be written to o[2]. (If I will ever load more opponents sorted by their distance I will try to combine these two into an internal for loop.)
//get closest item
Same as the two above except for items. I have yet to think up a way to rank items by more than just their distance (preferring a farther bottle over another weapon if it is needed). If you have a good script idea write it down.

The conditions for the closest objects are a little long - maybe i can simplify them later.

As you can see I have added an is_object(int i) function I will use for better readability inside the id function as well:
updated id function (Click to View)
Reply
Thanks given by: John Fighterli , Rhino.Freak
#5
In this post I will show you all functions that are used inside the get_objects function as they are all pretty simple one liners.

    C-Code:
bool is_object(int i){
   return ( loadTarget(i) != -1 ) ? true : false;
}
This function loads the object slot i, and if the loadTarget function returns a real object type and not -1 it returns true as the slot i contains an object.

    C-Code:
bool is_opponent(int i){
   return ( loadTarget(i) == 0 && target.team != self.team ) ? true : false;
}
This function is very similar to is_object. It returns true if object number i contains a character (type 0) that is on another team.

    C-Code:
bool is_item(int i){
   return ( is_object(i) && ( target.state == 1004 || target.state == 2004 ) ) ? true : false;
}
Another simple function. If i contains an object that is inside a pickable state it returns true. This isn't perfect for the sake of selecting an item as falling items that are about to be pickable (but not yet) will be ignored until they stay still. I should probably rename this function into is_pickable.

    C-Code:
bool closer_than(int i, int j){
   return ( ( square_distance(self.num,i) < square_distance(self.num,j) && is_object(i) ) || !is_object(j) ) ? true : false;
}
This function simply compares the distance of object i to the character with the distance of j to the character. If i is closer or j does not exist it returns true.

    C-Code:
bool hits_sooner_than(int i, int j){
   return ( is_attacking(i) && hit_time(i) != -1 && ( hit_time(i) < hit_time(j) || hit_time(j) == -1 ) ) ? true : false;
}
Now things become a little more complex. This function compares the times it will take i and j to reach our character. If i is attacking and reaching the character sooner it will return true.


The functions square_distance and is_attacking are quite simple. I will either post these two or the more complex hit_time function next.


The previous get_objects function contained a logic error concerning items. Items can actually be on the same team as soon as they have been hit or picked up, thus items hit or thrown by the character were ignored until an opponent used them. As the other three conditions already contain team checks I just changed the != self.team to != self.num to simply omit the character itself instead of team objects:
updated get_objects function (Click to View)
Reply
Thanks given by: John Fighterli
#6
    C-Code:
bool is_attacking(int j){
   if( !is_object(j) || target.id == 999 ){ return false; }
 
   int x = target.state;
   int i = 0;
   do{ x /= 10; i++; }while( x > 0);
   x = 1;
   for( i; i > 1; --i ){ x *= 10; }
 
   return ( target.team != self.team && ( ( target.type == 0 && frame(j,60,99) ) || target.state/x == 3 || target.state == 18 || target.state == 19 || target.state == 1002 || target.state == 2000 ) ) ? true : false;
}
So this function is supposed to return true if the target is attacking. With the first line I return false whenever this object does not exist or is just a broken weapon piece.

If that's not the case, in the last line I return true if the object is attacking. The easiest option is: return true if the target is on a different team and in state 3, the attacking state. But this will only recognize characters doing basic attacks because all projectiles use attacking states like 3000, 3005 and 3006. Also some character attacks use state 301. I did not want to check for all of these separately, so I simply checked whether the state begins with a 3. For that the code in between determines the length of the state and creates a 1 with trailing zeros to have the same amount of digits (1 for state 3, 100 for 301, 1000 for 3000/3001/etc). If dividing the target state by this number equals 3 then the state starts off with 3. There are also other attacking states such as the light and heavy throwing weapon states and the fire states 19 and 18 (I now notice I did not take into consideration that team mates with state 18 are attacking you too).

Now this function is anything but perfect, because there was no way to truly check for itrs yet. Version 3.0 will allow me to simply check whether the target is on a different team and has a damaging itr in its current frame. That will be a lot simpler and much more efficient than this.

    C-Code:
int square_distance(int s, int t){
 
   loadTarget(s);
   int sx = target.x;
   int sy = target.y;
   int sz = target.z;
 
   loadTarget(t);
   int tx = target.x;
   int ty = target.y;
   int tz = target.z;
 
   return (sx-tx)*(sx-tx) + (sy-ty)*(sy-ty) + (sz-tz)*(sz-tz);
}
The square distance function loads the coordinates of two objects and returns their absolute squared distance (Pythagoras shall guide you). As I don't want to waste space and resources with a square root function to turn this into a real distance it is only ever possible to compare two squared distances with each other. If there ever is a need to compare a real distance with this one it can simply be squared as well.

next: the hit time function and an updated is attacking function
Reply
Thanks given by: John Fighterli
#7
It's been such a long time that I updated this that the is_attacking function has changed entirely from the actual update I wanted to post.

So I will start off with the old hit_time function I wanted to post:
    C-Code:
float hit_time(int i){
   //returns time till attack hits
   //diagonal range is awful - need to redo it
   //maybe also use a hit line function
   if(!is_object(i)){return -1;}
   float t=-1;
   float xt=t;
   float zt=t;
   float vx=dvx(i)-dvx(self.num);
   float vz=dvz(i)-dvz(self.num);
   if(vx!=0){xt=abs((abs(xdistance(self.num,i))-80)/vx);}else{xt=0;}
   if(vz!=0){zt=abs((abs(zdistance(self.num,i))-15)/vz);}else{zt=0;}
   if(xt>zt){t=xt;}else{t=zt;}
 
   int x=target.x+t*vx;
   int z=target.z+t*vz;
   if(range(0,80+abs(dvx(i)),abs(xdistance(self.num,i)))&&range(0,15+abs(dvz(i)),abs(zdistance(self.num,i)))){t=0;}
   else if(abs(self.x-x)>abs(xdistance(self.num,i))||abs(self.z-z)>abs(zdistance(self.num,i))||!range(0,80,abs(self.x-x))||!range(0,15,abs(self.z-z))){t=-1;}
   else if(vx==0&&vz==0){t=-1;}
   return t;
}
All this little thing does is estimate how long it will take a given object to reach a certain range around the character with it's current speed. -1 means the object either doesn't exist anymore or will not reach at all (not moving or not intersecting). This is very useful to determine the most imminent threat. For example a distant super arrow might still hit sooner than a closer slow blast, thus better try to dodge that and cope with the blast later. All those calculations are some simple yet ugly math that could and should be done better.

On to the new is_attacking function. This one has gotten a little ridiculous and even beyond the current dlls capabilities. But here it still is:
    C-Code:
bool is_attacking(int o){
   //true if an itr of o overlaps with a self.bdy
   if(loadTarget(o)!=-1&&o!=self.num){
      if(self.data.frames[self.frame].bdy_count>0&&self.blink==0&&target.data.frames[target.frame].itr_count>0){
         for(int i = 0; i < target.data.frames[target.frame].itr_count; ++i){
	        if((target.team!=self.team||target.state==18||target.state==12)&&
			   (self.state!=12||target.data.frames[target.frame].itrs[i].fall>=60)&&
			   target.data.frames[target.frame].itrs[i].kind!=1&&
		       target.data.frames[target.frame].itrs[i].kind!=2&&
			   (game.objects[o].unkwn16[0]!=0||
			   target.data.frames[target.frame].itrs[i].kind!=4)&&
			   (game.objects[game.objects[o].unkwn6].data.frames[game.objects[game.objects[o].unkwn6].frame1].wpoint.attacking!=0||
			   target.data.frames[target.frame].itrs[i].kind!=5)&&
		   	   target.data.frames[target.frame].itrs[i].kind!=6&&
		   	   target.data.frames[target.frame].itrs[i].kind!=7&&
		   	   target.data.frames[target.frame].itrs[i].kind!=8&&
		   	   target.data.frames[target.frame].itrs[i].kind!=14&&
		   	   target.data.frames[target.frame].itrs[i].effect!=4){
               for(int j = 0; j < self.data.frames[self.frame].bdy_count; ++j){
	              if(intersect(sBdy_rect(j,self.frame),tItr_rect(i,target.frame)))return true;
   			   }
            }
         }
      }
   }
   return false;
}
First of all it checks for the existence of given object o and whether it is the character itself.
Then it checks whether the character itself has a bdy, is not blinking(invulnerable) and whether the given object has an itr.

If that object has itrs that could hit our characters bdy it goes on to cycle through all of them to see if they are harmful and intersecting with a bdy. (yes this function will check whether it is technically already too late - but that's ok)

The harmful check is by far the longest obviously. First of all there are several itr kinds that aren't harmful at all.
The catching kind 1 inside walking frames is generally harmless. The picking kinds 2 and 7 are harmless. The super punch kind 6 is harmless. The healing kind 8 is harmless. The blocking kind 14 is harmless. And anything with effect 4 is also harmless. (maybe I should invert the kind part and only note down harmful kinds - but this way seemed better at first)

Then there are of course some more delicate situations such as burning or thrown characters (where the team does not matter) or weapons (where the itr kind 5 is always present but only activated by the character).
So I also check whether the object is either not on the same team, burning (state 18) or falling (state 12) and whether the object has a throwinjury attached (unkwn16[0]). So only burning objects, thrown falling characters or objects from another team are considered harmful. And last but not least: kind 5 itrs from weapons are only considered harmful if the holding characters (unkwn6) wpoint attacking is not 0. Yeah go figure that one line out.

Once a harmful itr is found it is double checked with every bdy of our character via yet another 3 functions: intersect(rectangeA, rectangleB), sBdy_rect(bdy_number, self.frame_number) and tItr_rect(bdy_number, target.frame_number). The last two of those get an array of the bdy and itr coordinates and the first one checks whether they overlap (in all 3 dimensions).

So this function truly checks whether an itr is about to hit or not. The problem currently is that multiple itrs or bdys with the same kind (mostly Henrys flute) will crash the game as they return the wrong kind after the first one.

I will keep trying to fully rebuild basic AI but I am not sure whether I can keep track of it here the way I started it as I've only recently discovered the use of a lot of built in variables that can help making it less deterministic which I have yet to understand. However if you want to view the fully working basic AI I had done last year you can download LF 1over2 as the boss from stage 2 uses it.
Reply
Thanks given by:




Users browsing this thread: 1 Guest(s)