Posts: 2,340
Threads: 78
Joined: Mar 2008
06-11-2012, 01:42 PM
(This post was last modified: 07-05-2017, 05:24 AM by STM1993.
Edit Reason: changed [code ] tag to [code=ai ] tag
)
Silva's latest holy blessing gives us the power to write our own special moves AI (and with SomeoneElse on boat even the basic AI). I will try to give an overview here as to what this means exactly and how to do it. And I hope to update this as Silva progresses with adding features and I learn more tricks to abuse this (from you?). So please comment to make this a good reference for anyone to create some AI.
The whole thing works without editing the lf2.exe and will instead read your AI scripts at start up (and auto reload them if you are using the debug version, so there is no need to restart for changes to apply). To just play with custom AI from others, download the release version here. Drop the dll into your lf2 directory and also create a folder called AI to put the scripts into. To get started with creating your own AI, download the debug version here. Paste that dll into your lf2 directory. Starting your lf2.exe now will open up a console as well, giving you information about the running AI scripts. Now you can also create a folder called AI inside your lf2 directory which will contain your AI scripts. The scripts are written in Angel Script, which uses basic C syntax. All parts we need from it here are really easy. Also angel scripts are basically text files except their file ending is ".as", so you can edit and create them with any text editor you want. They should be named with the number of the id they shall be used for.
Every AI script will need to contain at least one of these functions:
The id() function - this replaces all the characters AI and gives you control over everything it does.
The ego() function - this function will be called by the original basic AI in case you do not use an id() function.
Obviously if you just want to make a melee character that simply uses certain special move inputs in a smart way you will only use the ego() function. Here is an example of a really simple AI script:
AI-Code:
int ego(){
if (self.hp < 400){
DdJ();
}
return 0;
}
|
This code could be useful for John as he will try to heal himself whenever his hp is below 400. But the AI for real attacks will require a little more complex conditions to be executed at the right time. Note that the ego() function is not the main AI function of your character and thus needs to 'report back' to the function it has been called from. This is done with return 0;. The 0 will give the control back to the basic AI (the id() function). If you write return 1; you can retain control in the ego() function. If you create your own functions later on they may return other values depending on their purpose.
So here is everything you will need and more:
Conditions are your best friends. Aside from the mp limitation noted inside the characters data these will help the AI decide when to use what move. The basic syntax for it looks like this:
AI-Code:
if (condition){//do if condition is met
command(); //perform command
}
|
But you can also branch it off into many different conditions with else ifs or just an else in case no condition is met:
AI-Code:
if (condition1){
command1();
}
else if (condition2){
command2();
}
else {
command3();
}
|
Now looking closer at the conditions themselves you will need operators for them to work.
Operators allow you to compare things and chain multiple conditions together. These are the comparing operators you will probably use:
AI-Code:
variable == 1 //variable equals 1
variable != 1 //variable does not equal 1
variable >= 1 //variable is bigger or equal to 1
variable <= 1 //variable is smaller or equal to 1
variable > 1 //variable is bigger than 1
variable < 1 //variable is smaller than 1
|
These are the chaining operators you will need to have multiple conditions:
AI-Code:
condition1 && condition2 //condition1 and condition2 are met
condition1 || condition2 //either condition1, condition2 or both are met
|
You can also chain them in more complex ways with brackets:
AI-Code:
condition1 && (condition2 || condition3) //condition1 and at least condition2 or condition3 are met
|
And to compare things as conditions to perform moves you will need all kinds of information from the character and it's target: variables.
Silva has given us access to a lot things from both the character (self) and the target and will hopefully even add more. All following variables will need to be prefixed with either self. or target. to work for your script:
AI-Code:
id //useful to determine what character (or other object) your target is
num //object number (something from 0 to 399, 0 to 7 for human players)
type //object type
weapon_type//type of the held weapon (there are special numbers for knife and boomerang)
weapon_held//object number of the held weapon, -1 if none is held
frame //useful for combos (in a frame to perform a leap attack?)
state //useful for a general look at an unknown opponent (is the target falling/defending/DoP?)
team //useful to interact with teammates (fusion/healing/team combos)
clone //-1 for a normal character, 3 for a clone (possibly other values)
fall //when hit the fall value rises and slowly drops back to 0; >0: DoP, >70: knocked out
bdefend //when hit in state 7 this value rises and slowly drops back to 0; >70(/100 for armors): broken defense
arest //attack rest - time until the attacker can do a single hit again
vrest //victim rest - time until a character can be hit again (multi hit)
blink //after state 14 returns a number that runs down from 15 to 0 while blinking
shake //negative number for a hit character, positive for an attacker
ctimer //runs down from 300 to 0 when catching someone
mp //rises to 500; if below 0 only starts rising again if attack is pressed (reset to 0)
hp //rises/heals up to dark_hp
dark_hp //effective maximum health during a fight
max_hp //can be above 500 for enemies in stage mode
facing //false when facing right; true when facing left
x_velocity //absolute velocity
y_velocity //..
z_velocity //..
x //absolute positions on the background
z //..
y //..
D //0, 1 if respective key is pressed
J //..
A //..
left //..
up //..
right //..
down //..
DlA //0, these all return 1 as long as the last pressed key is D,
DuA //2 if the second key (direction or J) was last pressed
DrA //and 3 if the full sequence has been pressed
DdA //(returns to 0 once the input has been accepted for a move
DlJ //or been voided by pressing a different key)
DuJ //l, u, r, d stand for left up right down
DrJ //..
DdJ //..
DJA //..
|
Most important of all these for your usual moves are the position of the character and the target, which determines whether the target is within range for a certain move. There are also some variables that do not belong to self. or target. and will allow you to know what kind of a fight is being played:
AI-Code:
stage_bound //
stage_clear //
current_phase //
current_phase_count//
current_stage //
bg_width //right background border (left=0)
bg_zwidth1 //top background border
bg_zwidth2 //bottom background border
mode //0: VS, 1: Stage, 2: 1on1, 3: 2on2, 4: Battle, 5: Demo, 6: Playback, 7: Quit
difficulty //-1: CRAZY!, 0: difficult, 1: normal, 2: easy
|
And you can even create your own variables like this:
AI-Code:
int xdistance = self.x-target.x;
|
The simplest way of checking a range is to subtract character and target position to get the distance and then compare it with the range in which the move hits the target:
AI-Code:
if (((self.x-target.x) => 0) && ((self.x-target.x) =< 80))
|
But this way the character will only perform the move when the target is within 80px to the right of our character. To make this work to the left as well and also only when the character is facing his target we will include the self.facing into the distance calculation like this:
AI-Code:
(self.x-target.x)*(2*(self.facing?1:0)-1)
|
The additional calculations turn the self.facing from 0 and 1 into -1 and 1. Now the character should also not perform the move when the target is too far away on the z- axis. This is a little easier because it doesn't matter whether the target is above or below our character and Silva has implemented an absolute function to turn any value into a positive one:
AI-Code:
if (abs(self.z-target.z) =< 8)
|
With this condition the move will only be executed when the target is within 17px on the z-axis The y-axis usually isn't quite as interesting as the other two, but if you've got a melee move that cannot hit falling characters you might want to check whether the target is on the ground:
Now with ranged moves the whole thing gets more complex as the z-range increases with projectiles that can be aimed up and down as they move away. With Clide I have found that you can simply do a division of x and z distance to get an angle you can compare:
AI-Code:
if (abs(100*(self.z-target.z)/(self.x-target.x)) =< 15)
|
Now that the conditions are set you can perform your move by putting one or several of the available commands.
These are all the commands you can give the character:
AI-Code:
D(x,y); //x=1,y=0: the key is pressed
J(x,y); //x=1,y=1: the key is pressed and/or held (useful for walking)
A(x,y); //x=0,y=0: the key is released (required if you are using the void id() function)
left(x,y); //..
up(x,y); //..
right(x,y);//..
down(x,y); //..
DlA();
DuA();
DrA();
DdA();
DlJ();
DuJ();
DrJ();
DdJ();
DJA();
|
This is as far as you will need to read for creating simple AI scripts, if you already have something working and want to create more complex things: read on. If you have trouble with your script: read the error and debugging section.
There are some helpful predefined functions:
AI-Code:
abs(i); //returns the absolute value of i (negative numbers will be turned positive)
rand(i); //randomly returns a number from 0 to i
loadTarget(i); //this allows you to change your target, while i is a number from 0 to 399
//the function will return the object type or -1 if the object does not exist
//also the objects i=0,...,7 are always the human characters
|
But you can also create your own functions:
AI-Code:
int sum(int x, int y){
int z = x + y;
return z;
}
|
The first line defines what variable type the function will return and what variable types it will be using. Here are two examples of how this one could be called:
AI-Code:
int variable = sum(self.x, self.y);
if (sum(target.z, target.x) < 0){}
|
As you can see a function can be used within calculations and conditions, making it do complex calculations with given variables and letting you use the returned value like a variable. In the first sample the function is used to write the sum of the characters x and y coordinates to a variable and the second one is used to have the sum of the targets z and x coordinates as a condition. So if there is a calculation you need to do very often but with differing variables just create a function for it.
If you ever need a function that does not return anything you can just call it "void functionname()", just like the void id() function.
If you just don't know what you are dealing with and need to see some of those values you are basing your conditions on you can use the function print(), and if you don't like the console running full also clr(). Here is a sample:
AI-Code:
print("text"); //prints text in the console
print(variable); //prints the value of the variable in the console
print("\n"); //adds a new line in the console
print(variable+"text\n");//prints a value and text with a new line
clr() //clears the console
|
If you have made a syntax error within your script the console will spurt out really helpful error messages. Here are a few examples on how to interpret and treat them:
AI-Code:
ai\0.as(8, 10): ERROR : Expected ';'
|
This error simply means that within the file "0.as" you forgot to put a ";" in line 8 position 10. Simply go there and add it, problem fixed.
AI-Code:
ai\0.as(11, 2): ERROR : Unexpected end of file
|
This error is a little less helpful than the previous one, but it simply means you forgot to close a bracket somewhere. You can just add one at the end, but that may not be the right position so better fully check your script. Maybe you have also opened one too many?
AI-Code:
ai\0.as(6, 38): ERROR : No conversion from 'bool' to math type available.
|
This means you are trying to use a boolean variable there (like self.facing, it can only be true or false) in a mathematic calculation. To convert the variable to the respective mathematic values simply add "?1:0" at its end. (eg.: self.facing?1:0; 1 is true, 0 is false)
If you are running into error messages you do not understand just post them together with your script and I shall add an explanation here.
So you are getting more advanced with your AI and want to know more about other game objects not selected as a target by the basic AI. loadTarget(i) will do this for you. It loads the target variables of object number i and returns the type of it or -1 if it does not exist. There are 400 object slots in the game (0 to 399) and the first 8 are reserved for the human players. So if you want to pick one of these without knowing it's number you will need a for loop to check out all 400 of them:
AI-Code:
for (int i = 0; i < 400; ++i){
if (loadTarget(i) == 0 && target.team != self.team){
print("opponent "+i+" has "+target.hp+"hp\n");
}
}
|
This example will print out the hp from all opponents. But it will only have object number 399 selected afterwards. We will need to add a few variables and another condition to select a certain one of them. I will go with the opponent that has the least health:
AI-Code:
int k = 0;
int hp = loadTarget(0);
for (int i = 0; i < 400; ++i){
if (loadTarget(i) == 0 && target.team != self.team && target.hp < hp){
hp = target.hp;
k = i;
}
}
loadTarget(k);
|
You can of course also try to get the opponent closest to you, do a combination of both, look for weapons within reach or check for hostile projectiles. However there is one problem: the basic AI will always reselect the target by it's own conditions if you are only using the ego() function. You should better use this to base only special actions on a self selected target. The most useful area for this is probably anything team related such as healing moves or the fusion. John for example could check all his teammates, whether they are close enough to him and in need of health. And Jan could simply check all teammates to perform her healing angels.
Thanks given by: The Lost Global Mod , Ramond , A-Man , Alblaka , Electric2Shock , qwertz143 , Apocalipsis , Simoneon , Hero destroyer , Reaper , Gad , MnM , Rhino.Freak , mfc , Marshall , Yasines , professional DCer , HappyHouR , John Fighterli , The Hari , Dragon5 , Memento , empirefantasy , Jed37 , Gespenst , Dr. Time , TamBoy , XTIAN , Alapottra , Bamboori , bashscrazy , AmadisLFE , Marko , LéoSilva , Deep , Arokh , T.O.R.N.A.D.O , STM1993 , the mad maskman , Nyamaiku , Rellotscrewdriver , Ghanu , Luigi600 , Mesujin
Posts: 1,556
Threads: 77
Joined: May 2011
Well, pretty good tutorial. Though a better way I used to debug with was putting print functions inside every if block. When the com try to use the move in the if block, it prints out the string I assigned (which are... difference between both x pixels and both y pixels, the move combination and the mp points.). And there is something I have been trying to do but failed, make the com stand (doin nothing) when the opponent is on ground(laying). And is there any way we can make things go random. For instance, the com randomize between either using this move or that, or maybe this AI or that!!.
Thanx again YinYin, and of course lets not forget Silva for this awesome system.
Regards,
Posts: 1,696
Threads: 60
Joined: Jan 2009
Ow, I'm totally going to write AI scripts for all my chars :3
Btw, how about
- RNG
- self_weaponid
- detecting T0 objects on same x/y coordinate (sticking ik8's)?
Version 1.10 out now!
Version 1.02 out now!
Version 1.22 out now!
Return (String) System.getNewsOfTheDay();
Barely active, expect slow responses. If at all.
Greetz,
Alblaka
Thanks given by:
Posts: 2,340
Threads: 78
Joined: Mar 2008
Okay i've updated the first post with
state
blink
and the return command
also i have updated Clides AI for 0.4
@A-MAN: well you can use the print function in any way you like
the new return function should allow you to stop a character from moving around - but the safety distance from lying enemies cannot be overridden yet
also i dont know about randomizing yet - usually there are random functions based on system time or something - need to look up whether chaiscript has something of that kind but i doubt it
@Alblaka: the self_weaponid i understand, the other two i don't - maybe elaborate those in the actual hex project thread from silva
also looking forward to more AI
UPDATE: added a new section about target loading and changed all code to C syntax
next i will add a little more about how i debug and test
and after that a new section with samples
Thanks given by:
Posts: 1,073
Threads: 38
Joined: Mar 2008
i'll merge these in later, just posting coz i worked out some magical stuff.
you can define functions like the following example, and you can have function pointers:
C-Code:
def func1() {
return true;
}
def func2() {
return false;
}
var v1 = func1();
var v2 = func2();
var f3 = func1;
var f4 = func2;
var v3 = f3();
var v4 = f4();
clr();
print("v1: ${v1}"); // true
print("v2: ${v2}"); // false
print("v3: ${v3}"); // true
print("v4: ${v4}"); // false
|
in functions, you cannot access global variables, so anything within a function has to be passed in as a parameter (maybe you can define new variables in functions, untested). so if you want to manipulate globals, you have to do it in the global code space (i.e. not in a function).
C-Code:
var xRange = self_x - target_x;
var yRange = self_y - target_y;
var zRange = self_z - target_z;
// function pointers
var approachTargetX;
var departTargetX;
var approachTargetZ;
var departTargetZ;
// setup x commands
if (xRange <= 0) {
approachTargetX = right;
departTargetX = left;
} else {
approachTargetX = left;
departTargetX = right;
}
// setup z commands
if (zRange <= 0) {
approachTargetZ = down;
departTargetZ = up;
} else {
approachTargetZ = up;
departTargetZ = down;
}
|
Azriel~
Thanks given by:
Posts: 2,340
Threads: 78
Joined: Mar 2008
06-17-2012, 08:32 PM
(This post was last modified: 06-17-2012, 08:33 PM by YinYin.)
hmm - it seems like the latest update makes your way of using functions not work anymore
i've added the new additions/changes and extended the first text a little giving more of the important info
if i can get into using self defined functions i may delay the announced examples and extended debug/testing to instead add this as an extra section
Thanks given by:
Posts: 1,696
Threads: 60
Joined: Jan 2009
06-19-2012, 02:59 PM
(This post was last modified: 06-19-2012, 03:22 PM by Alblaka.)
Gave it a try and ran into a slight issue with my character Striker. Using following AI:
Code: {
if (self.hp < 400){
DdA()
}
return 1
}
DvA is a transformation move, turning the character into a different ID one. The second form again has a DvA transforming him back.
Using this script, the AI spammed DvA as soon as the condition triggered and as well performed DvA in the frame right after the transformation.
I fixed this issue by adding a self.frame check to the condition, but this bug sort of means the AI script of ID #1 runs for 1 frame of ID #2 (or is the DvA input simply being carried over for a single frame by engine?)
Nothing serious, just dunno'ing.
On a sidenote, kudos for the flawless execution and easyly installation. Took me 5 minutes to get the setup running... Now I can turn my chars into actual companions/opponents :3
edit:
Quote:Also when writing your AI, keep in mind that you do not need to restart LF2 for changes to apply.
Damnit, couldn't you say that earlier?! XD Or rather, in a more visible spot.
editedit:
Code: Target 0 loaded, it's a -1
Target 0: 149
Target 1 loaded, it's a -1
Target 1: 149
Target 2 loaded, it's a -1
Target 2: 149
Target 3 loaded, it's a -1
Target 3: 149
Target 4 loaded, it's a -1
Target 4: 149
Target 5 loaded, it's a -1
Target 5: 149
Target 6 loaded, it's a -1
Target 6: 149
Target 7 loaded, it's a -1
Target 7: 149
Target 8 loaded, it's a -1
Target 8: 149
Target 9 loaded, it's a -1
Target 9: 149
ClosestDistance: 149
Appears loadTarget does always return -1 to me, whilst 'target' consistently points at the enemy (and thus the distances displays are always the same ones).
Did I miss something?
Code: {
clr()
var closestDistance = 10000
var initialTarget = target.num
for (var i = 0; i < 10; ++i)
{
var j = loadTarget(i)
print("Target ${i} loaded, it's a ${j}")
if (target.team != self.team)
{
var d = (self.x-target.x)*(2*self.facing-1)
print("Target ${i}: ${d}")
if (d < closestDistance)
{
closestDistance = d
}
}
}
loadTarget(initialTarget)
print("ClosestDistance: ${closestDistance}")
return 0
}
AI is meant to determine the closes hostile T0 object's distance to check whether it can perform something leaving the AI vulnerable for a few moments.
Version 1.10 out now!
Version 1.02 out now!
Version 1.22 out now!
Return (String) System.getNewsOfTheDay();
Barely active, expect slow responses. If at all.
Greetz,
Alblaka
Thanks given by:
Posts: 2,340
Threads: 78
Joined: Mar 2008
06-19-2012, 03:19 PM
(This post was last modified: 06-19-2012, 03:23 PM by YinYin.)
(06-19-2012, 02:59 PM)Alblaka Wrote: I fixed this issue by adding a self.frame check to the condition, but this bug sort of means the AI script of ID #1 runs for 1 frame of ID #2 (or is the DvA input simply being carried over for a single frame by engine?) i think its the engine
check it by printing the DdA
print(self.DdA)
0(blank) is nothing
1(smiley) is defend pressed
2(filled smiley) is defend down pressed
3(heart) is defend down attack pressed
the value should go back to 0 once the input has been accepted (by a hit_Da)
or voided by pressing a different key
for some cases this doesnt seem to happen (think woody: D>A+J will do both blasts and tiger dash)
(06-19-2012, 02:59 PM)Alblaka Wrote: On a sidenote, kudos for the flawless execution and easyly installation. Took me 5 minutes to get the setup running... Now I can turn my chars into actual companions/opponents :3 hell yes!
edit: Quote:Damnit, couldn't you say that earlier?! XD
reward for reading that far
edit: nope, loadTarget is currently only returning -1 - but the function itself works - so you can check for something else than the type (state/team may work)
Thanks given by:
Posts: 1,696
Threads: 60
Joined: Jan 2009
06-19-2012, 03:47 PM
(This post was last modified: 06-19-2012, 03:56 PM by Alblaka.)
(06-19-2012, 03:19 PM)YinYin Wrote: edit: nope, loadTarget is currently only returning -1 - but the function itself works - so you can check for something else than the type (state/team may work)
Oh well... think I will check for state 3000 to deflect projectiles then ^^'
edit:
Hmm... any chance you can drop us dvx variables in the next update? It's hard to code a persistent deflect if projectiles can have different speeds.
editedit:
I think it was a BAD idea to give Striker's AI the ability to use his deflect (Defend+timing) on projectiles... he started to rip off my a** now...
Version 1.10 out now!
Version 1.02 out now!
Version 1.22 out now!
Return (String) System.getNewsOfTheDay();
Barely active, expect slow responses. If at all.
Greetz,
Alblaka
Thanks given by:
Posts: 2,340
Threads: 78
Joined: Mar 2008
added your name to the velocity request on silvas to do list
also i am trying to make davis deflect projectiles with his bare hands right now - quite a bit more difficult than a deflect defense ...
Thanks given by:
|