1. Hello Guest! It's with a heavy heart that we must announce the removal of the Legacy GMC Archive. If you wish to save anything from it, now's the time! Please see this topic for more information.
    Dismiss Notice

Scripting for intermediates: Utility-Functions

Discussion in 'Tutorials' started by ZeDuval, Oct 26, 2016.

  1. ZeDuval

    ZeDuval Member

    Joined:
    Jun 20, 2016
    Posts:
    118
    GM-version: GMS
    Target-Platform: All


    Content:
    1. Introduction
    2. Colours & Terms
    3. Utility-Scripts & Function-Wrapping
    4. An extension as a bundle, turning scripts into functions
    5. Last words


    1.) Introduction
    Welcome, fellow Gamemaker,

    I keep a personal Utility-extension with general-purpose-functions. I use this extension as a way to bundle these functions, a bundle that can easily be extended and imported into new projects. I see scripts as something affiliated to a specific project. Utility-scripts should be polished and turned into functions, often replacing original built-in functions. In this tutorial, which is targeted at intermediates, I'll show you how to create Wrapper-Functions and a Utility-extension for yourself. You should, however, already have experience with GML and fundamental knowledge about script arguments, loops, conditionals, and multi-dimensional variables.

    2.) Colours & Terms

    I'll use the colour-coding I'm used to, since it's better to differentiate, for example, scripts and functions.
    Colours: Normal Text, Keywords, Values, Comment, Constants, Built-in-Variables, Functions, Script Names, Resource Names

    A Utility-Script is a script that has a general purpose, meaning that it is not affiliated to a specific project and can therefore be imported and used in every new project.

    Function-wrapping means to create a script and to use an existing function and add or improve its functionality, in order to replace the original function. In the best case, the original functionality will be also preserved.



    3.) Utility-Scripts & Function-Wrapping
    3.1.)

    For a start, we're gonna add 4 scripts. If more are wanted, just ask! Let's begin right away, for a better understanding, with something simple:

    Original Function: instance_create()
    Arguments: x,y,obj
    Returns: Real
    Problem: Exactly 3 arguments must be provided.
    Goal: Controller-objects are mostly invisible and therefore created at the x/y - coordinates 0,0. We want a function that can be used with or without the x and y arguments. If only the object is provided to the function, x and y will both default to 0.

    In the resource-tree on the left, right-click on Scripts and choose Create Group from the dropdown-menu. Name it Utility-Functions. Now right-click on this group and...

    Create Script and name it instance_new.
    /// instance_new(x,y,obj)
    if argument_count==1{

    return instance_create(0,0,argument[0]);
    }else if argument_count==3{
    return instance_create(argument[0],argument[1],argument[2]);
    }

    If the amount of arguments provided to instance_new() is exactly 1, an instance of the object specified with this single argument is created at the position 0,0. If the amount of arguments is exactly 3, all arguments will be forwarded to the original function.
    When wrapping a function like this, it is important to also forward the return-value of the original function, if there is one. In this example, the return-value of instance_create() is the index of the newly created instance and we have to forward it using the return statement, in order to get the index from our script, like this:

    var my_controller = instance_new(obj_controller);

    There are many different ways to code this little script, I'll show you a few variations:

    The way I would do it, a little change to the if-clause:
    /// instance_new(x,y,obj)
    if argument_count>1{

    return instance_create(argument[0],argument[1],argument[2]);
    }else{
    return instance_create(0,0,argument[0]);
    }

    Making use of a local var:
    /// instance_new(x,y,obj)
    var i;
    if argument_count==1{

    i = instance_create(0,0,argument[0]);
    }else{
    i = instance_create(argument[0],argument[1],argument[2]);
    }
    return i;

    Using a switch instead of an if-clause:
    /// instance_new(x,y,obj)
    switch argument_count{
    case 1:return instance_create(0,0,argument[0]);
    case 3:return instance_create(argument[0],argument[1],argument[2]);
    }
    Note: In my opinion, with only 2 possibilities, neither the creation of a local var, nor the use of a switch-case, makes sense. Within more complex scripts however, they might become the better choice. For now, just stay with the initial, top-most code.




    3.2.)

    Before we continue with something more complex, we'll add one more simple script. This time, we'll improve a function without argument by adding the possibility to provide one:

    Original Function: instance_destroy()
    Arguments: -
    Returns:
    N/A
    Problem: It is expected to use this function always from within the instance you want to destroy.
    Goal: Controller-objects are often used to handle many instances of other objects. We want the possibility to destroy specific instances or all instances of a specific object from within the controller-objects.

    Create Script and name it destroy.
    /// destroy(obj)
    if argument_count==0{

    instance_destroy();
    }else if argument_count==1{
    with(argument[0]){instance_destroy();}
    }

    From within another object, we can now provide the instance-to-be-destroyed as an argument. If the argument is the index of an instance, this instance will be destroyed. If the argument is an object-index, all instances of this object will be destroyed! From within the instance-to-be-destroyed itself, we can use destroy() without argument just like the original function. I see room for improvement, but for now we leave it like this.



    3.3.)

    This time we're not gonna wrap a function in the traditional sense, simply because there isn't a function for what we wanna do. Also, we're gonna write a script that takes a variable amount of arguments:

    Original Function: -
    Arguments: -
    Returns: -
    Problem:
    When creating an array with the values "a","b","c","d","e", we need to write:

    var a; a[0]="a"; a[1]="b"; a[2]="c"; a[3]="d"; a[4]="e";

    Goal:
    A Utility-script to create an array by providing the array-elements as arguments.

    Create Script and name it array.
    /// array(val1,val2,...)
    var a,i=argument_count;repeat i a[ i]=argument[--i];return a;
    One line for something so useful! Now we can create arrays like this:

    var a = array("a","b","c","d","e");

    Note: The array gets created and filled from behind because that's faster than doing it the other way around, where the array, to be increased, gets actually created anew again and again(as far as I know).




    3.4.)

    For the last example, we're gonna write something super useful, and also step by step. Create Script and name it debug_console.

    Original Function: show_debug_message()
    Arguments: string
    Returns: N/A
    Problem: Exactly 1 argument must be provided, no less, no more. If I want to add an empty line, I need to provide an empty string, "", as argument. If multiple variables get concatenated using +, every non-string-value must be wrapped within string(). Also, the function-name show_debug_message() is long af.
    Goal: We want a shorter name, the possibility to output an empty line and a variable amount of arguments.

    /// debug_console(val1,val2,...)
    var s='',i=0;repeat argument_count s+=' '+string(argument[i++]);
    show_debug_message(s);
    Usage & Debug-Console-Output:
    debug_console("hello","world"); // =>
    hello world

    Right now, we can provide a variable amount of values. They will all be concatenated, seperated by spaces. Real values will turned into string values inside the script. If no argument is provided, the output will be an empty line. Every good console-output needs a time-stamp! Here we go:

    /// debug_console(val1,val2,...)
    var s='',i=0;repeat argument_count s+=' '+string(argument[i++]);
    show_debug_message(date_time_string(date_current_datetime())+' |'+s);
    Usage & Debug-Console-Output:
    debug_console("hello","world"); // =>
    03:42:56 | hello world

    Cool, cool. But that's not even its final form! Now, let's add the super useful additional info from within which object and event the script is executed by adding 2 local vars and 1 switch-case:

    /// debug_console(val1,val2,...)
    var o=object_get_name(object_index),e='',s='',i=0;repeat argument_count s+=' '+string(argument[i++]);
    switch event_type{
    case
    0:e='Create';break;
    case 1:e='Destroy';break;
    case 2:e='Alarm';break;
    case 3:e='Step';break;
    case 4:e='Keyboard';break;
    case 5:e='Mouse';break;
    case 6:e='Collision';break;
    case 7:e='Other';break;
    case 8:e='Draw';break;
    case 9:e='KeyRelease';break;
    default:e='Room';o='noone';}
    show_debug_message(date_time_string(date_current_datetime())+' | '+o+' | '+e+' |'+s);
    Usage & Debug-Console-Output:
    debug_console("hello","world"); // =>
    04:02:03 | obj_init | Create | hello world



    4.) An extension as a bundle, turning scripts into functions

    Up until now, we created some scripts, that are bundled by putting them into one script-folder. Now we're gonna create an extension to bundle our scripts and turn these scripts into functions.

    In the resource-tree on the left, right-click your Utility-Functions folder under Scripts and choose Export Group of Scripts... Name the file utility_functions and save it at some place you'll find again. Delete the Utility-Functions folder under Scripts.

    Then, in the resource-tree, right-click on Extensions and choose Create Extension from the dropdown-menu. Name it Utility. Now right-click on this extension and choose Add File, browse for the utility_functions.gml and choose it. If you now double-click on utility_functions.gml inside your extension, the 4 scripts should open in an tabbed script-window.

    Right-click on utility_functions.gml and choose Add Function. Double-click this new function. On the right (External Name), insert instance_new, same on the left(Name). In the Help field, insert instance_new(x,y,obj) Returns: Real. Don't change anything below and press OK. Repeat this procedure for destroy and array.

    Now for debug_console, add a function, insert debug_console as External Name. In the Name field, insert dc and in the Help field, insert dc(val1,val2,...) Returns: N/A. This way, you can create short function names for very often used functions.





    5.) Last words

    If you have any questions: Feel free to ask. Suggestions, critique, whatever: Please post!
     
    Last edited: Oct 26, 2016
  2. KurtBlissZ

    KurtBlissZ Member

    Joined:
    Jun 21, 2016
    Posts:
    200
    I just skimmed over this and I like it. I'll probably look into making a lot of mine utility like scripts as extensions, seems like it would save a lot of time.

    For scripts I always prefer putting brackets around the optional arguments like this
    /// instance_new([x], [y], obj)

    Some common function wrapping I do is, make a script that does room_goto_next and room_goto_previous checking if they exists first. Also another script that basically the same as your instance_new script but instead it only works if the instance doesn't exist yet.
     
    Last edited: Oct 26, 2016
    Bentley and ZeDuval like this.
  3. amusudan

    amusudan Lousiest of Potatoes

    Joined:
    Jun 20, 2016
    Posts:
    183
    This is brilliant, thanks for sharing.
     
    Bentley and ZeDuval like this.
  4. amusudan

    amusudan Lousiest of Potatoes

    Joined:
    Jun 20, 2016
    Posts:
    183
    Bump, this deserves attention.
     
    Bentley and TheMatrixHasMe like this.
  5. yakmoon

    yakmoon Member

    Joined:
    Sep 20, 2016
    Posts:
    33
    first of all, I did read the entire thread.
    2- I'd like to thank you for putting effort into writing this thread.
    3- please open your heart to my critics as they are to help you become better at what you are doing:
    - the instance_new is literally useless and is just a waste of time, you could use it in different functions, our friend suggested to check for existing object and that is more useful.
    - the destroy(), array[], debug_console() are great, I've never even thought about doing things like this, and thank you for sharing the idea.
    - a great new function would be draw text where you say:
    main function:
    text_draw('hello word', x, y, c_white,1, font0, fa_center,fa_middle);
    variants:
    text_draw('hello word', x, y, c_white);
    text_draw('hello word', x, y, c_white,1);
    text_draw('hello word', x, y, c_white,1, font0);
    text_draw('hello word', x, y, c_white,1, font0, fa_center);
    text_draw('hello word', x, y, c_white,1, font0, fa_center,fa_middle);
    the code:
    /// text_draw(text,x,y,color,alpha,font,halign,valign);
    switch (argument_count)
    {
    case 4:
    draw_text_color(argument[1],argument[2],argument[0],argument[3],argument[3],argument[3],argument[3],1);
    break;
    case 5:
    draw_text_color(argument[1],argument[2],argument[0],argument[3],argument[3],argument[3],argument[3],argument[4]);
    break;
    case 6:
    draw_set_font(argument[5]);
    draw_text_color(argument[1],argument[2],argument[0],argument[3],argument[3],argument[3],argument[3],argument[4]);
    break;
    case 7:
    draw_set_font(argument[5]);
    draw_set_halign(argument[6]);
    draw_text_color(argument[1],argument[2],argument[0],argument[3],argument[3],argument[3],argument[3],argument[4]);
    break;
    case 8:
    draw_set_font(argument[5]);
    draw_set_halign(argument[6]);
    draw_set_valign(argument[7]);
    draw_text_color(argument[1],argument[2],argument[0],argument[3],argument[3],argument[3],argument[3],argument[4]);
    break;
    }
     
    Last edited: Nov 14, 2016
    ZeDuval likes this.
  6. ZeDuval

    ZeDuval Member

    Joined:
    Jun 20, 2016
    Posts:
    118
    Adding some additional, custom error-handling, is always a good idea! I should force myself to do this more frequently! :rolleyes:

    Thank you very much, kind sir! :)

    You're welcome. I'm thinking about a second part with some more... wicked stuff, once I find the time.

    Being the very first snippet to start with, this was just a code-base that can be extended in many different ways, for example with the mentioned Singleton-functionality:

    /// new_Singleton(obj)
    var xx=0,yy=0,oi=argument[argument_count-1];
    if instance_number(oi)>0 return instance_find(oi,0);
    if argument_count>1{xx=argument[0];yy=argument[1];}
    return
    instance_create(xx,yy,oi);

    My "personal" new() function for example handles this Singleton-stuff based on the object-prefix and limits the execution of itself so that the script containing new() can be in a step event without creating zillions of instances.

    In addition to debug_console - called log() in my utility compilation - I have pop() => more or less the same, but outputs using show_message(), and my latest favorite: cap() - Used to display key<>value pairs in the window-title:

    /// cap( key, val )
    var s="",i=0;

    repeat argument_count/2{
    s+=" "+string(argument)+" : "+string(argument[i+1])+" |";i+=2;
    }
    window_set_caption(s);

    typical all-time-favs =>

    cap("rms",room_speed,"fps",fps,"fps_real",fps_real);


    Yes, there are tons of possibilities! Good ideas! You could also add an option to reset draw-options to previous values after drawing the text! :) And thanks for your feedback (and reading the entire thing :D)!
     
  7. This is a great tutorial and I think a more apt name would have been a simple, "How to build your own functions". To be honest, the wording made me skip past it until I came in here out of curiosity today. But I'm glad I did!

    I've been looking for the ability to create functions so stumbling upon this nugget is something quite useful in adding to the tool belt so to speak!

    I wish this ability was more apparent in the documentation. As someone who has spent their fair share of time with their nose in the documentation does anyone know if this was there all along and I was missing it?

    Anyhow, thanks for taking the time to bring this to the forums ZeDuval!

    Edit: Getting a strange error when running game. It says there is an error on load and that it is unable to find the function I created. Even though it lights up orange when I type it in to code like a function should, so it recognizes the function as a function. I looked and it was in the folder where it should be, so I don't get the issue. Tried restarting Gamemaker but that didn't work either.
     
    Last edited by a moderator: Dec 11, 2016
    Bentley likes this.
  8. Hyomoto

    Hyomoto Member

    Joined:
    Jul 7, 2016
    Posts:
    1,078
    You have a couple of sloppy implementations here. The first, most obvious one, is that you are checking if argument_count is greater than 1, which means this script will fail if 2 are provided. The second is that, when you code for multiple arguments, you should set the code up in a way as to make it as bulletproof as possible. In your case you are pushing the 'id' from the front to the back, which means if you do the aforementioned 2 arguments you can more easily cause it to fail when it could recover. Lastly, your else is unnecessary since return effectively cancels further processing. Let me show you what I mean:
    Code:
    /// instance_new(obj, x, y)
    if argument_count == 3{ return instance_create( argument[1], argument[2], argument[0] ) }
    return instance_create( 0, 0, argument[0] );
    None of this is required of course but it's good to think about these things: especially if you are accepting variable arguments. In this case, if I forget a third argument, or I provide too many arguments, my first expected one is always the object id, making it harder to not include and thereby reducing the chance the script will outright fail. The other thing is the removal of else reduces the operation count by one since we know if the first one didn't fire, we want to use the second one. Lastly, checking directly for the number of arguments we want 3, instead of > 1, is another safety to prevent it failing if 2 are provided. Since this is a utility script it might be used quite often, it's best to optimize as much as you can.

    I admit I really like your use of event_type in debug_console: that's an interesting idea. Giving yourself useful output and minimizing input? I'm all for it, and that's a clever way of achieving that. That said, I do think it's a bit decadent overall since how much of this information can you possibly use? Half your line is used up by things that aren't the value you wanted to monitor, and you should be limiting your debugging to stuff you what actually want to watch. This type of readout is great for picking out results from the crowd, but you can achieve the same effect by limiting your testing area. It's not bad, it just seems like it would be overkill 90% of the time. I used to do this a lot until I realized, oh yeah, I'm the one using this and in this case simpler, more robust is generally better. As a final critique: you really should give your code room to 'breathe'. You are often packing a lot of stuff into a single line which not only makes it burdensome to read, that is also the playground of errors. Doubly so since the built-in debugger can be a bit vague sometimes. Still, proper code is that which works for you so my observations are just that, observations of a dissenter. For comparison this is what my 'debug_console' looks like:
    Code:
    /// @description log( values )
    /// @param values
    if developer_mode & system.useLogging == false { exit }
    
    var _prefix = object_get_name( object_index ) + " :: ";
    var _string = "";
    
    for ( var _i = 0; _i < argument_count; _i++ ) {
        _string = _string + string( argument[ _i ] );
    
    }
    show_debug_message( _prefix + _string );
    
    I'm not going to tell you it's better, just more legible. You can clearly see what each step is doing and there's little left up to the imagination. If you are going to teach people how to build functions you should always remember the best, most important rule of all: write your code so, that when you read it later, it's easy to see what it does. Some people use comments, I just stopped packing everything so tightly together. Otherwise you'll end up with this:
    Code:
    /// @description popcnt(value)
    /// @param value
    //   any :: function; returns the number of bits set in value
    var _v = argument0, _c;
    
    _c = _v - ( ( _v >> 1 ) & $DB6DB6DB )
            - ( ( _v >> 2 ) & $49249249 );
    
    return ( _c + ( _c >> 3 ) & $C71C71C7 ) % 63;
    
    Best to be clear, even when optimizing.
     
    Bentley and renex like this.
  9. redmassacre

    redmassacre Member

    Joined:
    Jul 29, 2016
    Posts:
    7
    Hi, ZeDuval
    Thanks for you well-described tutortal.
    I will use your utilites :)
    One question - is using functions is better optimized or any other pros vs scripts using?
    I mean - If I use tabbed scripts - I can also export-import this bunch of code with one flle also.
    And with scripts i can use F12 for drlldown to code instead of manual navigation (functions F12 go to Help reference, but I have no my functions descr there)
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice