Better Completion for CustomAI
Add these top level variables.
var self:CharacterAiScript = self;
var character:Character = character;Naturally, character refers to the player character the cpu is controlling.
This should allow you to use the api completions that should come with the characte AI
Absolutely do not set these variables to anything else.
Making use of CPU Level
Checking your CPU level is pretty straight forward, simply check
character.getPlayerConfig().levelAs for how you can use this, a simple way would be to add a random chance element to every decision which scales with level. Now we're gonna go through several examples of basically using our level + some randomness to create a true/false value we can use to control the rate of something scaling to level.
Linear Scaling, upto 100% chance
var level = character.getPlayerConfig().level:
var calculatedRate = level/9;
var isSmartEnough = Random.getValue() < calculatedRate;
if (isSmartEnough) {
// Do thing
}Linear Scaling, upto 75% chance
var level = character.getPlayerConfig().level:
var calculatedRate = (level/9) * 0.75;
var isSmartEnough = Random.getValue() < calculatedRate;
if (isSmartEnough) {
// Do thing
}Linear Scaling, Only levels 5-9, upto 50%
var level = character.getPlayerConfig().level:
var calculatedRate = ((level-4)/(9-4)) * 0.5;
var isSmartEnough = Random.getValue() < calculatedRate;
if (isSmartEnough) {
// Do thing
}Do notice that the only thing that changes here is calculatedRate
Now let's say you wanted non linear scaling, you can use EaseTimer.interpolate, to generate your own values for calculatedRate, this has the benefit of not having to code easing functions yourself.
Which ends up getting used like this, note that you can just switch out the ease type with the last argument, to see what feels better
var level = character.getPlayerConfig().level:
var calculatedRate = EaseTimer.interpolate(0,1, (level/9), EaseTimer.EASE_OUT_CUBIC);
var isSmartEnough = Random.getValue() < calculatedRate;
if (isSmartEnough) {
// Do thing
}Ensuring your CPU does not immediately start attacking on training mode
Basically, do this check
self.getBehavior() == AiBehavior.ATTACKThough you'd probably not want to repeat this line of code all the time so, I'd suggest wrapping all your attack logic at the very least in one top level function, and return early if it's false.
function checkAttack() {
if (self.getBehavior() != AiBehavior.ATTACK) {
return;
}
}Of course you can also seperate your logic into smaller functions that get called by this.
Disabling and Enabling Moves so the Engine doesn't perform them
You can also prevent certain moves that the built-in AI uses First you wanna check out ChracterAiActions for all the stuff you can control, but generally it comes down to:
Enabling An Action
self.enableAction(CharacterAiActions.SPECIAL_UP);
self.enableAction(CharacterAiActions.SPECIAL_UP_AIR);Disabling An Action
self.disableAction(CharacterAiActions.SPECIAL_UP);
self.disableAction(CharacterAiActions.SPECIAL_UP_AIR);Getting A Target Foe
Using the Built-in Functionality
var foe:Character = null;
if (Std.isOfType(self.getTargetFoe(),Character)) { // Type Checking
foe = self.getTargetFoe(); // We set the actual value here
}Manual Method
function getTargetFoe():Character {
var closestDistance = Math.POSITIVE_INFINITY;
var chosenFoe:Character = null;
var myPoint:Point = new Point(self.getX(), self.getY());
var foePoint:Point = null;
for (foe in self.getFoes()) {
if (foePoint == null) {
foePoint = new Point();
}
foePoint.init(foe.getX(), foe.getY());
var currDistance = myPoint.distanceSquared(foePoint);
if (currDistance < closestDistance) {
closestDistance = currDistance;
chosenFoe = foe;
}
}
foePoint.dispose();
myPoint.dispose();
return chosenFoe;
}Accessing the character
The character the AI is controlling can simply be accessed with the character variable, as usual you can check their state, position, etc, as you would a foe for instance.
if (character.inState(CState.SPECIAL_UP)) {
Engine.log("Do something while in up special state");
}refer to Character, GameObject and Entity for any methods you want to use, they all apply here just as well.
Of course, you can always access exports, so be sure to provide information in exports that you think can help your ai make better decisions.
Performing Inputs as the AI
Adding Inputs
Inputs are done via addInputOverrides(), but how does it work? addInputOverrides accepts an array with an even number of items, with this structure: NOTE: THIS IS PSEUDO CODE, DO NOT COPY THIS DIRECTLY
self.addInputOverrides([
inputs, frameDuration,
inputs, frameDuration,
...
]);Alright, now that we have a semblance of the structure, how do we fill it in? frameDuration if fairly straightforward, it simply tells us how long to hold the inputs this would be a positive number.
now for the inputs themselves...
Buttons Bitset and Bit-wise Stuff
For passing inputs, we use the Buttons class, please take a look at it here https://shifterbit.github.io/fraymakers-api-docs/classes/Buttons
Basically inputs is a combination but Buttons constants, but they have to be combined in a particular manner using bit-wise OR, if you've messed with Common.onButtonsPressed or Common.onButtonsHeld this should feel a bit familiar to you:
Single Button
Buttons.SPECIALMultiple Buttons at once
Buttons.SPECIAL | Buttons.UPButtons.SPECIAL | Buttons.UP | Buttons.LEFTNo Buttons At all
0Examples of Performing Inputs
Performing Up Special
self.addInputOverrides([
0, 1, // For ONE FRAME, we let go of all out buttons
Buttons.SPECIAL | Buttons.UP, 1 // NEXT FRAME, PRESS UP AND SPECIAL
0, 1 // NEXT FRAME AFTER THAT, LET GO OF ALL BUTTONS
]);Charging Down Strong for 60 Frames before Letting Go
self.addInputOverrides([
0, 1, // For ONE FRAME, we let go of all out buttons
Buttons.STRONG | Buttons.DOWN, 60 // NEXT 60 FRAMES, HOLD DOWN AND STRONG
0, 1 // NEXT FRAME AFTER THAT, LET GO OF ALL BUTTONS
]);Performing an input in the foes direction Lets assume our targetFoe isn't null, and we wanted to perform a tilt in the opponents direction, we can do:
var direction:Int = self.getTargetFoe().getX() > character.getX() ? Buttons.RIGHT : Buttons.LEFT;
self.addInputOverrides([
0, 1, // For ONE FRAME, we let go of all out buttons
Buttons.TILT | direction, 1 // NEXT 1 FRAME, PRESS TILT in whatever direction is
0, 1 // NEXT FRAME AFTER THAT, LET GO OF ALL BUTTONS
]);Performing a Frame Perfect Hadouken Motion Input Because You're a street fighter character or something
var direction:Int = self.getTargetFoe().getX() > character.getX() ? Buttons.RIGHT : Buttons.LEFT;
self.addInputOverrides([
0, 1, // For ONE FRAME, we let go of all out buttons
Buttons.DOWN, 1,
Buttons.DOWN | direction, 1,
direction, 1
0, 1 // NEXT FRAME AFTER THAT, LET GO OF ALL BUTTONS
]);Clearing Queued inputs
self.clearInputOverrides();Clears out any inputs waiting to be done that have been added by addInputOverrides
Checking if there's inputs being queued up
self.hasInputOverrides();Just checks if you have any inputs waiting to be performaned.
Gathering Information on the foe
Checking X and Y Distances Manually
First thing's first, you probably want completion on the foe itself
var foe:Character = null;
if (Std.isOfType(self.getTargetFoe(),Character)) { // Type Checking
foe = self.getTargetFoe();
}Now assuming the foe variable isn't null, we can gather information, let's say you wanted to check if the foe is above you, pretty straight forward:
var yDistance = character.getY()-foe.getY();
var foeHeight = Math.abs(foe.getEcbFootY()-foe.getEcbHeadY()); // We also get the height here
if (yDistance > foeHeight) {
// Do thing
}Maybe you also wanna check horizontal distance as well as left/right
var xDistance = character.getX()-foe.getX();
var xDistanceAbs = Math.abs(xDistanceAbs);
var isLeftOfMe = xDistance > 0;
if (xDistanceAbs < 70 && !isLeftOfMe) { // check if they're within 70 units of us and to the right of us
// Do thing
}Now Let's combine both checks into one, so now we're checking if:
- The opponent is to the left of us
- The opponent is within 70 units of us on the left
- We are above the opponent
var yDistance = character.getY()-foe.getY();
var foeHeight = Math.abs(foe.getEcbFootY()-foe.getEcbHeadY());// We also get the height here
var xDistance = character.getX()-foe.getX();
var xDistanceAbs = Math.abs(xDistanceAbs);
var isLeftOfMe = xDistance > 0;
if (yDistance > foeHeight && xDistanceAbs < 70 && !isLeftOfMe) {
// Do thing
}We can also look at other information such as speed/state/facing direction etc, but you can refer to Character, GameObject and Entity for any methods you want to use, they all apply here just as well.
With this you can make somewhat basic decisions like "What do I do if the foe is at least 300 x units away from me horizontally but below me", etc.