turn based battle system help

Started by Anshinin, Tue 10/03/2015 01:47:53

Previous topic - Next topic

Anshinin

Okay so looking through the forums I've found a lot of posts on the basics of putting these systems together, however I have a few more questions in making the battle system more dynamic.

I have enemy HP set to a specific GlobalVariable and each enemy who has unique health has their own unique variables. When I use attacks they have to subtract the damage dealt minus that particular enemies HP variable.
What I want to know is is it possible to set some one to detect what enemy is in the battle and set the right GlobalVariable accordingly so you don't have to write every individual enemy down and how it gets affected by that item.

I want to set up some sort of base enemy ai system that activates whenever the right GlobalVariable for them is drawn. Like their attack power multiplied by the power of the attack makes them do their attack, and then runs a text display for what attack they used. If they go below a certain health level they'll add new attacks to their possible usage.

I also wanted the ability to during the main gameplay earn some sort of "spell" that lets you summon someone to help you in battle. So I want to know how to add another character to select from for attacking in fights.

My main goal is really just to minimize the amount of code needed to make battles more efficiently, so I just gotta set up rooms with NPCs and the battle system will work universally, alongside some specified attack names and what not. Any way of making this a more fleshed out system that works will would be rly appreciated.

Vincent

Dumb answer that solves a quarter of your message ;


- struct
Declares a custom struct type in your script.
Structs allow you to group together related variables in order to make your script more structured and readable. For example, suppose that wanted to store some information on weapons that the player could carry.
Code: ags

struct Weapon {
  int damage;
  int price;
  String name;
}; 


Now, you can declare a struct in one go, like so:
Code: ags

Weapon sword;
sword.damage = 10;
sword.price = 50;
sword.name = "Fine sword";

Much neater and better organised. You can also combine structs with arrays




- Arrays
Arrays allow you to easily create several variables of the same type.
Code: ags

int health[50]; 


This example declares 50 int variables, all called health.
You access each seperate variable via its index (the number in the brackets). Indexes start from 0, so in this case the health array can be accessed by indexes 0 to 49. If you attempt to access an invalid index, your game will exit with an error.

Here's an example of using the array:

Code: ags

health[3] = 50;
health[4] = 100;
health[player.ID] = 10; 


this sets Health 3 to 50, Health 4 to 100, and the Health index that corresponds to the player character's ID number to 10.




// ============================================================================ //

Hopefully this will help you

Anshinin

Those arrays seem very helpful and I understand that, however I'd like a little more clarifications on the use of structs.

Where would you put this and how would it be able to execute as a usable weapon item for example, or if I were to use them to make my enemy bank and have the different characteristics of the enemies? I'm just not entirely sure how I'd use it, though I understand what it is doing.

Ghost

It's common practise to declare a struct in a script header. You can use the GlobalScript's header, but to avoid clutter I suggest you create a new script that deals with all your weapon-related stuff.

As Vincent said structs are great in tandem with arrays. To elaborate a bit:

A struct is a template. It holds no data and can not be used "as is". An array is a collection of those templates filled with data. With a weapon struct you can create an array of weapons and you would "use" that data by referring to a position within the array that holds the data for a certain weapon.

You will have to create the array (usually at the top of a script file- NOT the header). Then you will have to fill the array with data:

Code: ags

weapon[0].name = "Somewhat pointy stick";
weapon[0].damage = 12;


Once you have data in your array you can access it from your script by referencing the weapon at position X:

damage_dealt_to_player = weapon[X],damage;

Now there are several ways to work with this, but I think you get the point- in order to have someone "use" a weapon you would most likely give him a variable that is a position in your "weapon list".

Hope this gets you started.

Anshinin

Okay so this has so far gotten me where I want to be with making my weapons and attacks, but the question of attacking the enemies is still something I need help on.

When I activate a spell or weapon, how do I let it know what enemy to attack without having to declare it for each individual enemy? Like I don't want to have to write out what each spell will affect each individual enemy with, and I don't even think that'd work.

So how do I get it to detect there is an enemy to attack when the weapon or spell is selected?

Snarky

Well, what is it based on? Do you select the targets? Is it some kind of area effect? Is it whoever's closest? Some kind of relationship between the type of spell/weapon and the type of enemy?

Basically, you'll want to keep track of all the relevant information for each enemy unit, and then when it's time to attack, if the player doesn't designate a target, you have to run some sort of calculation on that data to decide which enemies are targeted.

Coding a fully working battle system in AGS is quite complex (I know, I tried and gave up once), and if you're new to structs and arrays I'd expect it to be pretty overwhelming. You can limit it and hack it in various ways to make it a lot easier, so it really depends on what exactly you have in mind.

Have you looked at the code for OceanSpirit Dennis: Scourge of the Underworld (DX), or Ray of Hope? It has a very simple battle system (though it's one-on-one, so no need to pick your enemies). Almost all the OSD games are open-source, and you can see a list of them here; it can be a very helpful resource to reference (though not always an example of the best way to do something).

Anshinin

I probably wasn't very clear with my last post, excuse me as I was very tired when I wrote that :tongue:

Anyway, for the most part my battles are going to be 1v1 and I wanted the game to know what enemy to attack within the script. I think the solution I've come up with (though untested) is to make a variable for the enemies health in the script when you run an attack, and then in each room declare what that variables respective GlobalVariable is.

So x - 5 would be the enemy taking 5 damage
and in the battle room I would say x = whatever globalvariable the enemies health is being declared as.

I'll look at the source code for those games though since that seems like it'll be useful.

Snarky

Again, the best way to go about it depends very much on the specifics of what you're trying to do. To provide useful advice, we'd pretty much need a detailed breakdown of exactly how you want it to work.

For example, are the enemies you will be fighting "generic" opponents that you might have to fight copies of multiple times (like "an orc"), or are they unique "bosses" that you defeat once and then never see again? That makes a big difference to how you code it. But I'm pretty sure that the right answer is NEVER a globalvar for the enemy's hitpoints.

Anyway, here's some hints, with a quick outline of how I'd write a simple version:

Code: AGS
// In the header
#define MAX_ENEMIES 12 // How many different enemies you have in the game

/// All the stats and data on an enemy, plus methods you can perform on an enemy
struct Enemy
{
  String name;

  // Stats, whichever you need for your system
  int strength;
  int defense;
  int magicDefense;
  int health;

  // This is for displaying the enemy on screen
  Character* body;
  int id;

  // Methods you can call on an enemy
  import int Attack();
  import bool IsAlive();

  /// Finds the id of the enemy with this name 
  import static int Find(String name);
};

// TODO: Also add structs for weapons, spells, etc. (as Vincent suggests)

// These are methods defined in the script that you want to make available to be called in other files (room script, etc.)
import void AttackAnimation(this Character*)
import void InitEnemies();

// Also, you want all scripts to be able to see this array
import Enemy enemy[MAX_ENEMIES];


Code: AGS
// In the script
Enemy enemy[MAX_ENEMIES];
export Enemy enemy;

// Set all the stats for your enemies (another way would be to read this info from a file)
void InitEnemies()
{
  int i=0;
  enemy[i].name = "Dracula";
  enemy[i].strength = 10;
  enemy[i].health = 50;
  enemy[i].defense = 20;
  enemy[i].magicDefense = 50;
  enemy[i].body = cDracula;
  enemy[i].id = i;
  i++;

  enemy[i].name = "Koopa";
  enemy[i].strength = 30;
  enemy[i].health = 100;
  enemy[i].defense = 60;
  enemy[i].magicDefense = 10;
  enemy[i].body = cKoopa;
  enemy[i].id = i;
  i++;

  // ... and so on
}

// This runs automatically when the game starts
function game_start()
{
  InitEnemies();
}

bool Enemy::IsAlive()
{
  return (this.health > 0);
}

/// Call this to make the enemy attack you (returns amount of damage the enemy has done)
int Enemy::Attack()
{
  // Very simple example, just does a random amount from 0 to enemy.strength
  int damage = Random(this.strength);
  return damage;
}

static int Enemy::Find(String name)
{
  int i=0;
  while(i<MAX_ENEMIES)
  {
    if(enemy[i].name = name)
      return i;
    i++;
  }
  return -1;
}

// Show the animation for this character's attack
void AttackAnimation(this Character*)
{
  // This is just an example of how you might do a simple animation
  this.LockView(this.IdleView); // If you've put the attack animation as the character's idle view
  this.Animate(0, 2, eOnce, eBlock, eForwards);
  this.UnlockView();
}


... you could add much more stuff, but that's a start.

Then I would write a couple of functions to set up and run a battle. Something like:

Code: AGS
// Header
// Setup the battle by choosing which enemies to fight
import void InitBattle(int enemyId1, int enemyId2 = -1, int enemyId3 = -1, int enemyId4 = -1);
// Let the enemies attack you. Returns true if you were killed
import bool EnemyTurn();
// Attack one of the enemies you're facing. Returns true if you killed it
import bool AttackEnemy(int id);

// These are the enemies you're facing in this battle
import int battleEnemy[4];


Code: AGS
// Script
int battleEnemy[4];
export int battleEnemy;
int liveEnemyCount;

// All arguments apart from enemyId1 are optional and default to -1 (disabled)
void InitBattle(int enemyId1, int enemyId2, int enemyId3, int enemyId4)
{
  battleEnemy[0] = enemyId1;
  battleEnemy[1] = enemyId2;
  battleEnemy[2] = enemyId3;
  battleEnemy[3] = enemyId4;

  if(enemyId1 >= 0)
    liveEnemyCount++;
  if(enemyId2 >= 0)
    liveEnemyCount++;
  if(enemyId3 >= 0)
    liveEnemyCount++;
  if(enemyId4 >= 0)
    liveEnemyCount++;
}

// All the enemies in the battle attack in turn
bool EnemyTurn()
{
  int i=0;
  while(i<4)
  {
    int e = battleEnemy[i];
    if(e != -1 && enemy[e].IsAlive())
    {
      int damage = enemy[e].Attack();
      Character* body = enemy[e].body;
      body.AttackAnimation();
      myHealth -= damage;  // myHealth might be a globalvar
      if(myHealth <= 0)
         return true;       // Do whatever happens when you lose a battle
    }
    i++;
  }
  return false;
}

void AttackEnemy(int id)
{
  int e = battleEnemy[id];
  if(e != -1 && enemy[e].IsAlive())
  {
      int damage = Random(myStrength); // Again, here myStrength could be a globalVar
      enemy[e].health = enemy[e].health - damage;
      PlayerAttackAnimation(); // Defined somewhere
      if(!enemy[e].IsAlive())
      {
        Character* body = enemy[e].body;
        body.DeathAnimation(); // Which you define just like AttackAnimation
        liveEnemyCount--;
        return true;
      }
      else return false;
  }
  return false;
}


And then to fight a battle with Dracula and Koopa, you would do something like:
Code: AGS

void FightBattle()
{
  InitBattle(Enemy.Find("Dracula"),Enemy.Find("Koopa")); // This adds Dracula and Koopa to the battle

  // Then we just run battle turns until the player or all the enemies are dead 
  while(myHealth > 0 && liveEnemyCount > 0)
  {
     int target = PlayerPicksTarget(); // A method you've written to pick who to attack
     AttackEnemy(target);
     EnemyTurn();
  }
  if(myHealth > 0)
  {
    // Victory!
  }
  else
  {
    // Game Over
  }
}

Anshinin

Okay I'm working through reading and understanding that code so I can implement it since that's actually extremely useful to look at! A lot better than some of the examples I've seen digging through the forum.

Some of the features I'm trying to implement in my combat system and just a general idea of how I want it to work is as such.


  • As of right now all battles are 1v1, and will work like minibosses that won't appear after you've defeated them
  • There will be an option to fight with weapons you've found, magic spells which are represented with cards, and a "time manipulator" combo system (i'll explain)
  • You earn experience for individual stats based on usage (e.g. everytime you hit the enemy with a weapon you get +2 attack exp)
  • The magic spells will have a few different usages from basic attacks, summoning companions, or hypnotizing the enemy
  • The "time manipulator" thing is a little complicated but the general idea is being able to save up "time points" by not acting in your turn (hold up to 4 at a time) and then using them for various little things like hitting the enemy multiple times (the idea is that different versions of your character attack from different timelines), "rewinding" to revive yourself, fast forwarding time beyond the enemies turn so they can't hit you, etc etc
    This feature isn't entirely fleshed out yet but the general idea is there.
  • There is also the option to access your inventory for manual healing potions and items to increase your performance in battle
  • Finally an escape option if you need to

The numbers above the health bar are temporary.

Snarky

#9
OK, the fact that it's 1v1 and you only fight each enemy once does make it much easier, and it would in fact be possible to store enemy health as a globalvar, but I still wouldn't recommend it. It's better to keep all the enemy data in a struct. But you could do away with the battleEnemy array and just use the index into the enemy[] array (which you get with Enemy.Find("Enemy Name")) directly.

Some of the other features, OTOH, complicate things again. The battle system I tried to implement a couple of years ago was a FF-style Active-Time Battle, and I don't mind telling you it was an utter pain to implement (why I never finished it).

Let me know if you have any questions about the code. It demonstrates a bunch of AGS scripting concepts: how to define, export and import variables, arrays and functions, how to use structs and member-methods on structs, extender functions, etc. (Those are some keywords you could try looking up if you don't understand what's going on.) Good luck!

Edit: Another tip would be to use a common struct both for the player and the enemies, if they have similar types of stats, e.g.:
Code: AGS
// Header
struct Fighter
{
  String name;

  int strength;
  int defense;
  int health;

  int id;
  // ...
};

import Fighter enemy[MAX_ENEMIES];
import Fighter playerFighter;

// Script
Fighter enemy[MAX_ENEMIES];
Fighter playerFighter;
export enemy;
export playerFighter;

Anshinin

Okay so I know this is old but Snarky gave me permission (and preferred) that I just post here about it.

I tried putting this all together in a specific room and set up everything in the header but it's giving me an error

Code: ags
room11.asc(33): Error (line 33): Variable 'InitBattle' is already imported


This is the code in the room http://pastebin.com/vxpQDZPB

and this is the header, the only other place InitBattle is mentioned http://pastebin.com/FtJphJzJ


Gilbert

I haven't read the codes (and this thread thoroughly) but when you put "import something" into the script header, you're importing something defined in the Global script into rooms.
Since in your codes, InitBattle() is imported to the rooms in the header(I don't know whether it's defined in the Global script since it's not shown), it will generate an error when it is defined again in a room.
I'm not sure what you're after, but:
1. If the function InitBattle() is intended to be accessible to all the rooms, move the definition of the function from the room script to the Global script; or
2. if the function is only to be used in that single room, just remove that "import" line from the header.

Anshinin

That makes sense, I'm just confused about putting it in the GlobalScript because I don't know how to start the fight sequence so I thought I'd put it in a specific room for the battle, since that's how I did it with GlobalInts but I wanted to get away from that since Snarky said that wouldn't be very efficient.

Snarky

In the example code, InitBattle() is part of the battle system, and is defined... ideally within a dedicated script module, to keep things organized, though you can also put it in the global script. You can then call it from the rooms where you want to start a battle.

It's not that global vars are inefficient, it's that they're messy, particularly if you need to keep track of many things of the same kind (e.g. hit points for a dozen different enemies). But in any case, variables and functions work differently.

Anshinin

How would I call it from a room?

Snarky

You just... call it, like any other function. You already have it one place in the code:

Code: ags
InitBattle(Enemy.Find("Fuzball"),Enemy.Find("Kate"));


Incidentally, I'm sure the code I posted back then could be reorganized in a cleaner, better way (I don't like the way FightBattle() hardcodes certain enemies, for example, and when you just have two enemies in a battle, a separate InitBattle() function seems a bit redundant). I was just sketching out in a general way how you could express the data you need and the overall logical structure, it's not complete or perfect in any way.

Anshinin

I guess I'm just having a hard time wrapping my head around this, now that this has been set up how is it supposed to display the enemies bodies? And how does it know what XY position they should go to?

Snarky

This code doesn't do any of that stuff. It only deals with the logic of the battle itself, how to display it is up to you. In the sample, there are references to functions such as body.AttackAnimation(), body.DeathAnimation() and PlayerAttackAnimation(), which are examples of the kinds of functions you might want to write for the visual side.

If you just want to display the enemies on the screen, add a bit in the function that sets up the battle to put the bodies of the enemies somewhere. You'll have to decide where you want them. In your mock-up it looks like they always appear in a fixed position on the right side of the screen, and in that case you could just hard-code it.

Anshinin

As for running the part where you attack, and then your enemy attacks, could I attach that to a button on a battle screen GUI? So it'd run your attack and their attack.

Khris

Every GUI button has an onclick event, and you can assign a function to it just like you would to a "look at hotspot X" event. The function will be added to the global script. To call room code from the global script, you can use the CallRoomScript() function and put its counterpart "function on_call(int p) { ... }" into the room script.

SMF spam blocked by CleanTalk