Importing scripts with functions

Started by Aviva, Sun 22/10/2017 09:50:10

Previous topic - Next topic

Aviva

Hey people!

I am trying to organize my scripts and find Import/Export very useful functions to do this. However, since nesting is not allowed in AGS, I am a bit lost on how to import script without making them functions themselves.

This is what I have now to export code:

Code: ags
function variableChecker() {
  export variableChecker;


but because I have this function in that script:

Code: ags
function uiMainInventory_OnClick(GUIControl *control, MouseButton button)
{


AGS does not allow me to import it. How do you guys handle this and what are best practices when it comes to organizing scripts in AGS?

Thanks in advance :)
Working on "Drained", a distopian RPG

Snarky

#1
Hi Aviva,

I'm having a hard time understanding exactly what you mean, and I wonder if you might be confused about how this works in AGS.

There are (AFAIK) four things you can share between scripts in AGS. In all of these cases, the things you share will be available to scripts lower down in the script list (including room scripts). This happens at compile time, so you never put the import/export instructions inside any functions (which would mean, if it were possible, that they'd only be shared when the function was run):

1. Functions

To share a function, you write it in a script module, and put an import in the module header (you don't need to export the function):

Code: ags
// Module Header
import function myFunction();

// Module Script
function myFunction()
{
  // function body
}


2. Variables

To share a variable (making it global), you define it in the module script, export it there, and import it in the module header:

Code: ags
// Module Header
import int myVariable;

// Module Script
int myVariable;
export myVariable;


3. Data Types

You can also define new data types (enums and structs). To share these between script you just write them directly in the module header; you don't need to do any importing (but if you want to have a function inside a struct, that does need to be imported):

Code: ags
// Module Header
enum MyEnum
{
  eValue1,
  eValue2,
  eValue3,
  eValue4
};

struct MyStruct
{
  int Field;
  import function MyMethod();
};

// Module Script

function MyStruct::MyMethod()
{
  // function body
}


4. Constants

Finally, you can #define constants. Any instance of these constants in the code will be replaced by their definition before the code is compiled. (You can do a lot of stuff with #define constants, but usually it's just a way to name meaningful values rather than having to use "magic" numbers in the code.) Again, you simply put the definitions in the header:

Code: ags
// Module Header
#define MAGIC_NUMBER 375
#define WELCOME_STRING "Welcome to my game"

Aviva

Sorry if I didn't explain properly what I want to do. I want to keep Global Script clean and store the functions currently there in seperate scripts. For example, the function that blocks linear movement is about 30 lines and it's all in my Global Script. I want to store that function elsewhere to keep my Global Script clean and readable. I have made scripts for these functions in the folder hierarchy, so I now have the script containing said function placed at Scripts > Main > Functions > keyLinearBlock. I want to import that script into my Global Script as the function originally is placed there and needs be used in the repeatedly_execute_always() function.

If I understand it correctly, this would not work as of now because the script contains a function, right? So my question then would be: how do others order their Global Script and keep it all clean and readable?
Working on "Drained", a distopian RPG

Snarky

#3
You don't need to import the function in your Global Script. By putting the import line in the keyLinearBlock header, it will automatically be available in Global Script.

What you need to do is simply to call the function from repeatedly_execute_always() in the Global Script.

Here's an example:

Code: ags
// keyLinearBlock Header
import function blockLinearMovement();    // Share the function with other scripts

// keyLinearBlock Script
function blockLinearMovement()    // Here is the function
{
  // 30 lines of code moved out of the global script
}

// Global Script
function repeatedly_execute_always()
{
  // Other stuff...
  blockLinearMovement();    // Call the function
  // Other stuff...
}

Aviva

Thanks, that already helps me :) So here is what the code looks like in the script. It still throws a nested error, because of the functions in there. How would you handle that?

Code: ags
function keyLinearBlock (){

////////////////////// DISABLE LINEAR MOVEMENT //////////////////////

int old_mx, old_my;
    void handleInput() {
      int mx = IsKeyPressed(eKeyD) - IsKeyPressed(eKeyA);  // only A => -1, A & D => 0, only D => 1
      int my = IsKeyPressed(eKeyS) - IsKeyPressed(eKeyW);
      if (mx * my != 0) return; // only allow either A/D or W/S
      // rotate by 45°, stretch to 2:1
      int tmx = (mx + my) * 2;
      int tmy = -mx + my;
      // player is still holding down same key as last frame, check for end of walkable area / do nothing
      if (mx == old_mx && my == old_my) {
        int a = GetWalkableAreaAt(player.x + player.WalkSpeedX * tmx / 2, player.y + player.WalkSpeedX * tmy / 4);
        if (a == 0) {
          player.StopMoving();
          player.PlaceOnWalkableArea();
        }
        return;
      }
      // player picked new direction or released movement key
      player.StopMoving();
      player.Walk(player.x + tmx * 1000, player.y + tmy * 1000, eNoBlock, eAnywhere);
      old_mx = mx;
      old_my = my;
    }
    
        function abs(int a) {
      if (a>=0) return a;
      return -a;
    }
     
    function sgn(int a) {
      if (a>0) return 1;
      if (a<0) return -1;
      return 0;
    }
    
    
}
Working on "Drained", a distopian RPG

Snarky

The problem is that you've put everything inside the keyLinearBlock() function. You don't need to (and can't) do that.

Just remove that, so that it looks like this:

Code: ags
// Module Script: keyLinearBlock.asc
////////////////////// DISABLE LINEAR MOVEMENT //////////////////////

int old_mx, old_my;
void handleInput() {
  int mx = IsKeyPressed(eKeyD) - IsKeyPressed(eKeyA);  // only A => -1, A & D => 0, only D => 1
  int my = IsKeyPressed(eKeyS) - IsKeyPressed(eKeyW);
  if (mx * my != 0) return; // only allow either A/D or W/S
  // rotate by 45°, stretch to 2:1
  int tmx = (mx + my) * 2;
  int tmy = -mx + my;
  // player is still holding down same key as last frame, check for end of walkable area / do nothing
  if (mx == old_mx && my == old_my) {
    int a = GetWalkableAreaAt(player.x + player.WalkSpeedX * tmx / 2, player.y + player.WalkSpeedX * tmy / 4);
    if (a == 0) {
      player.StopMoving();
      player.PlaceOnWalkableArea();
    }
    return;
  }
  // player picked new direction or released movement key
  player.StopMoving();
  player.Walk(player.x + tmx * 1000, player.y + tmy * 1000, eNoBlock, eAnywhere);
  old_mx = mx;
  old_my = my;
}
    
function abs(int a) {
  if (a>=0) return a;
  return -a;
}
     
function sgn(int a) {
  if (a>0) return 1;
  if (a<0) return -1;
  return 0;
}


Code: ags
// Module Header: keyLinearBlock.ash

import void handleInput();
import function abs(int a);
import function sgn(int a);


And to call in Global Script:

Code: ags
// GlobalScript.asc

function repeatedly_execute_always()
{
  // ...
  handleInput();
  // ...
}

Aviva

Works like a charm! Thank you very much :D One final question though. The last script if have has a reference to another function and I get the error Undefined token 'cmbHandler'. Do you know how I can fix that?

Code: ags
function cmbHumanHotspot_OnClick(GUIControl *control, MouseButton button)
{
 if (mouse.Mode == eModeUseinv) {
   cmbHandler();
   } 
}
Working on "Drained", a distopian RPG

Snarky

Any function you call (that isn't built-in to AGS) has to be defined either above it in the same script, or in a script higher up in the list and shared with this script (typically by having an import statement in the script header for the script where it's defined).

So you have to do that.

However, in this case cmbHumanHotspot_OnClick() is probably an event handler: a function that is linked to from the event pane of a GUI Control, Hotspot, Character, etc. Event handlers must be in the Global Script (for GUI and character events) or the room script (for Room, Object, Hotspot and Region events), and since these are at the bottom of the script list, you can't call these functions from any other scripts.

The way to work around this problem is to split the function into the event handler "shell" and the actual function implementation. Actually, it looks like you're already doing that, more or less, since cmbHumanHotspot_OnClick() doesn't really do much more than call cmbHandler();

So what I would recommend is to put cmbHandler() either in the same script or a higher script as the function you call cmbHumanHotspot_OnClick() from (and add the import line to that script header so that it can be called from cmbHumanHotspot_OnClick()). Then replace the call to cmbHumanHotspot_OnClick() with:

Code: ags
  if (mouse.Mode == eModeUseinv)
    cmbHandler();

Aviva

#8
This is my repeatedly execute function now:

Code: ags
// Also works when game is blocked
function repeatedly_execute_always() 
{
  cmbHumanHotspot_OnClick(GUIControl *control, MouseButton button);
  uiMainInventory_OnClick(GUIControl *control, MouseButton button);
  uiCombatTest_OnClick(GUIControl *control, MouseButton button);
  uiConsoleEntry_OnActivate(GUIControl *control); 
  uiEmergency_OnClick(GUIControl *control, MouseButton button);
   
  keyLinearBlock();
  keyCodes();
  
  uiECS.Text = String.Format("%d", ecs);
}


And I get this error (the error is thrown for line 4 in the example) :

Code: ags
GlobalScript.asc(19): Error (line 19): Parse error in expr near 'MouseButton'

Working on "Drained", a distopian RPG

Snarky

#9
There's a lot of weirdness here. I think you should start by reading or rereading the manual, particularly the introduction to functions. After you've done that, you can then have a look at the explanation below.

First weird point, why are you calling all these functions in the first place? According to its name, a function like uiEmergency_OnClick() is an event handler that should be called automatically when you click on uiEmergency (which is presumably a button or something). There shouldn't be a reason to call it from repeatedly_execute_always(). (In general, you should avoid putting code in the repeatedly_execute...() functions, particularly if you're a novice AGS coder. Most code does not belong there! It's a typical beginner's mistake.)

Secondly, when you call a function, you have to provide values for the arguments (aka parameters; the bit inside the parenthesis). What you've done is to just copy the argument declaration: the description of the types of things the function expects as arguments. It's like you're filling in a form, and where the first field says "Fill in your name here", instead of putting your name you've just written "Fill in your name here" again.

So when the function declaration has "GUIControl *control" (or, as I would prefer to write it, "GUIControl* control"), what that means is "When you call this function, you need to provide a GUIControl* here. It can be whatever you like, but inside this function we'll refer to it by the name control."

In other words, if you were to call this function (and again: I don't think that's necessary or a correct thing to do!), in the function call you would need to replace GUIControl* control by the name of a particular GUIControl (cmbHumanHotspot, for example). Similarly you would need to replace MouseButton button with a certain button value, e.g. eMouseButtonLeft. The values depend on what the function expects: an OnClick() function usually expects the first argument to be the GUIControl that was clicked on, and the second argument to be the mouse button the player clicked. The fact that you don't know these things in the repeateadly_execute_always() function should be a warning that you're doing something wrong.

Khris

Just want to add that modules support several engine functions, among them repeatedly_execute_always.

So to call handleInput(), there's no need to import it and call in the GlobalScript's repeatedly_execute_always, one can do this right in the module by adding the function to its main script, below the definition of handleInput:
Code: ags
void repeatedly_execute_always() {
  handleInput();
}


When AGS is about to call repeatedly_execute_always(), it goes through the modules from top to bottom and checks if they have that function defined and if so, calls it.


As for having those GUI handler functions in repeatedly_execute_always(), that doesn't make any sense. I guess the intention is to move them out of the GlobalScript, which can be done like this:
Code: ags
// module header
import void cmbHumanHotspot_Click();

// module script
void cmbHumanHotspot_Click() {
 ...
}


We still must handle the click in the GlobalScript first though:
Code: ags
function cmbHumanHotspot_OnClick(GUIControl *control, MouseButton button) {
  if (button == eMouseLeft) cmbHumanHotspot_Click();
}


If the are lots of buttons in the game, a slightly shorter way is possible:
Code: ags
function anyButton_OnClick(GUIControl *control, MouseButton button) {
  if (button != eMouseLeft) return;
  // call module functions
  if (control == cmbHumanHotspot) cmbHumanHotspot_Click();
  if (button == someOtherButton) someOtherButton_Click();
}

This requires to manually insert "anyButton_OnClick" into the text field in all the GUI editor's button events.

Snarky

Quote from: Khris on Mon 23/10/2017 10:29:17
Just want to add that modules support several engine functions, among them repeatedly_execute_always.

So to call handleInput(), there's no need to import it and call in the GlobalScript's repeatedly_execute_always, one can do this right in the module by adding the function to its main script, below the definition of handleInput:
Code: ags
void repeatedly_execute_always() {
  handleInput();
}


When AGS is about to call repeatedly_execute_always(), it goes through the modules from top to bottom and checks if they have that function defined and if so, calls it.

Yes, but given that there seems to be some confusion about basic things like how to call functions, or why you can't define functions inside functions, I thought it would be better to take it one step at a time. (I also didn't suggest bundling the module functions inside a struct, for the same reason.)

SMF spam blocked by CleanTalk