Jump to content

Photo

A script noob writes his first script (and has questions)


  • Please log in to reply
7 replies to this topic

#1 Geoffrey

Geoffrey

    Chosen One

  • Members
  • Gender:Unspecified

Posted 21 October 2021 - 03:02 PM

So I've been working my way through Saffith's tutorials (again and again, always catching things I've missed), and this is my first 'useful' script, intended to add a durability mechanic to the wooden sword:

int S1_usecount = 0;
int S1_maxuse = 19; // Total number of uses less 1.

float breakthresh = 0.75; // Breaking threshold.
	// When the usecount reaches this percentage of the maxuse, SFX_BREAKING will play on use.

const int SFX_BREAKING = 57;
const int SFX_BROKEN = 58; // The tapping SFX have been used as placeholders.

item script Stick
{
	void run()
	{
		if(S1_usecount < S1_maxuse) // Increase counter every swing.
		{
			S1_usecount += 1;

			if(S1_usecount > S1_maxuse * breakthresh) // Play SFX after breaking threshold.
			{
				Game->PlaySound(SFX_BREAKING);
				Waitframe();
			}

			Waitframe();
		}
		else if(S1_usecount >= S1_maxuse)
		{
			S1_usecount = 0;
			Game->PlaySound(SFX_BROKEN);
			Link->Item[I_SWORD1] = false;
			Waitframe();
		}
	}
}

I've improved the script a few times, namely by nesting the second if statement, which used to be its own else if.

 

I also tried editing the script by integrating int usecount and int maxuse as local variables so that the script could be applied to any item and customized via D0, D1, etc. But I got turned around when it came to Link->Item[I_Sword1] = false;, since I didn't know what to replace I_SWORD1 with in order to make the script 'generic', simply removing the item running the script. I guess I could do this with D2, but is it possible to make this automatic?

 

I'm curious also whether there are any code improvements that I could make. I'm not asking for improvements to what the script does, but rather to how it's written. Since I've already improved it a couple of times as I've figured more out, help would be appreciated in this regard.

 

I had one further question: There's a point in Saffith's beginner tutorial where he asks you to make a script that reduces a value by 25%. I accomplished this by writing Link->HP *= 0.75;, but he did it by writing Link->HP -= Link->HP * 0.25;. I like that my code is simpler, but I like that his code retains the original phrasing, so to speak; in order to write my code, I first had to do a mental calculation that he lets the compiler do. After thinking about this for a while, I wonder whether there is any instance where his code would be the objectively better option or whether it will always come down to preference.

 

I don't have internet at home right now, so responses may be delayed. Thanks.



#2 Emily

Emily

    Scripter / Dev

  • ZC Developers
  • Gender:Female

Posted 21 October 2021 - 03:41 PM

So:

1. 'maxuse' should be a parameter (D0 or such)

2. 'usecount' should be an array, as such:

int S1_usecount[256];

Then you would access it like such:

if(S1_usecount[this->ID] < maxuse)

3. "Link->Item[this->ID] = false"

4. "*= 0.75" is more optimized code than "-= () * 0.25"

 

....Only one problem; I don't know if "this->ID" works in 2.53, that might only have been added for 2.55. If it doesn't compile, then that would mean there isn't a good way to do it in 2.53.

 

Also, for future reference: If you have any variable that you assign a value, and *never change again*, then it should be constant, not variable. For instance, in the code you posted, "breakthresh" is initialized to 0.75, and then never modified. (Given, it would be generally better to pass it as a parameter, rather than declaring it globally; but if you didn't want to use a parameter, you could make it "const float" instead of "float").

This is important, as you have a limited number of variables, but an unlimited number of constants; as constants are processed at compile-time.



#3 Geoffrey

Geoffrey

    Chosen One

  • Members
  • Gender:Unspecified

Posted 21 October 2021 - 04:58 PM

Thanks, Emily. I appreciate your response.

 

I'm still using 2.50.2, since it's the latest Linux release. I forgot to mention that in the first post. (...Any chance of that changing in the future?)

 

I hadn't realized that the number of variables was limited, so that's helpful too. My reason for not wanting to use a parameter is that I wanted one value to hold true for every item running the script and also for that value to be editable in one place, in case I should change my mind about it.

 

So it seems that the general structure of my script is good, which is gratifying. Yay!



#4 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 21 October 2021 - 05:09 PM

An item script only runs for one frame, so Waitframe will actually just quit. In this script, it doesn't matter, because there's no code that would run after it.
 

if(S1_usecount < S1_maxuse) // Increase counter every swing.
...
else if(S1_usecount >= S1_maxuse)

In this case, you could just use else instead of else if. If a < b is false, a >= b must be true, so you already know the second condition is true when you get there.
 

....Only one problem; I don't know if "this->ID" works in 2.53, that might only have been added for 2.55. If it doesn't compile, then that would mean there isn't a good way to do it in 2.53.

It doesn't. But if I remember correctly, this will work:

int GetItemId(itemdata data)
{
    for(int i=0; i<256; i++)
    {
        itemdata data2=Game->LoadItemData(i);
        if(data==data2)
            return i;
    }
    // It shouldn't be possible to get here
    return 0;
}

So you'd do this:

Link->Item[GetItemID(this)] = false

I'm still using 2.50.2, since it's the latest Linux release. I forgot to mention that in the first post. (...Any chance of that changing in the future?)

Hopefully, yes. I can build newer versions, but there are a couple of issues that need to be worked out (mainly, there's no sound).
 

My reason for not wanting to use a parameter is that I wanted one value to hold true for every item running the script and also for that value to be editable in one place, in case I should change my mind about it.

That's a good instinct. The primary goal of a lot of software engineering is making code easier to change, and defining a value in just one place is one way to do that. If you do want every item running the script to use the same value, a constant is easier to adjust. But you can get the best of both worlds by making the constant just a default that can be overridden:

const int DEFAULT_MAX_USES=19;
item script Stick(int maxUses)
{
    if(maxUses==0)
        maxUses=DEFAULT_MAX_USES;


#5 Geoffrey

Geoffrey

    Chosen One

  • Members
  • Gender:Unspecified

Posted 21 October 2021 - 05:16 PM

Thanks a bunch, Saffith! That's really helpful. Now I'll have to rack my brain a little in order to understand the parts that are a little opaque to me at present. :)

 

Good to hear that a new Linux build may be possible!



#6 Geoffrey

Geoffrey

    Chosen One

  • Members
  • Gender:Unspecified

Posted 30 October 2021 - 04:52 PM

So I've written my second useful script and I have a few questions, if anyone's got the time to answer. It's a signpost script whose activation is intended to mimic that of Treasure Chest combos (that is, the player has to walk up into it for so many frames):

ffc script Signpost
{
	void run(int m) // Enter D0 as message ID.
	{
		this->X = GridX(this->X);
		this->Y = GridY(this->Y);

		while(true)
		{
			int fc = 0; // Frame counter.

			while(Link->X >= this->X-8 && Link->X <= this->X+8
			&& Link->Y >= this->Y+8 && Link->Y <= this->Y+16
			&& Link->Dir == DIR_UP && Link->InputUp == true)
			{
				fc++;

				if(fc == 20)
				{
					Screen->Message(m);
				}
			Waitframe();
			}
		Waitframe();
		}
	}
}

So I have a couple of questions:

 

1. I chose 20 frames because it seemed a good approximate, but how long do you actually have to walk into a treasure chest before it opens?

 

2. I chose a while loop instead of a for loop because it seemed like a pain in the butt to put such a long condition into a for loop, but would a for loop have been better? I guess I could format it so that it wouldn't actually be a pain in the butt.

 

3. Could someone eli5 the difference between a while loop and an if statement? The nested while loop was initially an if, but it didn't work (maybe because an if statement doesn't loop until false?). Also, I'm curious why I need to nest one while loop within a while(true); I tried commenting out the while(true){}, and the code compiled, but it didn't work.

 

4. As of 2.50.2, is there any way to get the screen's message string and make the script default to that if D0 is left empty?

 

5. Is there anything else that I could have done differently?

 

Thanks. :)



#7 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 31 October 2021 - 12:08 AM

1. I chose 20 frames because it seemed a good approximate, but how long do you actually have to walk into a treasure chest before it opens?

8 frames. An easy way to count frames in the engine is to press F4 - the game will advance one frame each time you press it.
 

2. I chose a while loop instead of a for loop because it seemed like a pain in the butt to put such a long condition into a for loop, but would a for loop have been better? I guess I could format it so that it wouldn't actually be a pain in the butt.

Nah, while is fine. I'd go with if, but that's the next question. :P
 
Generally, I'd say that the word you'd use in English is the same word you should use in ZScript. Do this while some condition is true. Do this for each thing (enemy on the screen, item in an array, etc.) or for n seconds. Not a hard and fast rule, but it's good for code to reflect your thinking.
 

3. Could someone eli5 the difference between a while loop and an if statement? The nested while loop was initially an if, but it didn't work (maybe because an if statement doesn't loop until false?).

if doesn't work here because the variable fc is declared inside the outer loop. Each time through the loop, it creates a new variable and initializes it to 0, so it'll never go above 1 before it starts over. With the inner loop, it doesn't reach the declaration again as long as Link is in place.

If you move the declaration of fc to before the loop, if will work with if. You also have to reset it to 0 if Link's not pressing against the sign.
 

Also, I'm curious why I need to nest one while loop within a while(true); I tried commenting out the while(true){}, and the code compiled, but it didn't work.

Logically, everything in a script happens instantaneously except for Waitframe and Waitdraw, so anything that should happen over a period of time needs to be in a loop with a Waitframe. If you remove the outer loop, it'll get to the position check, see that it's not true, and move right past it - to the end of the script. You need the outer loop to tell it to keep running indefinitely and checking every frame.

In other words, you need the logic to be:

Repeat forever:
    If Link is pressing on the sign:
        (Do stuff)
    Wait a frame

4. As of 2.50.2, is there any way to get the screen's message string and make the script default to that if D0 is left empty?

Nope.
 

5. Is there anything else that I could have done differently?

Nothing major.
For that initial position correction, I'd suggest using GridX(this->X+8 ) so it can be off half a tile in either direction rather than checking the top-left corner.
Link->InputUp==true is redundant; you can just use Link->InputUp instead (true==true is true and false==true is false).



#8 Geoffrey

Geoffrey

    Chosen One

  • Members
  • Gender:Unspecified

Posted 01 November 2021 - 05:47 PM

snip

Thank you for your help again, Saffith. I do a lot better when I understand what's going on under the surface of something, so this is really helpful!


Edited by Geoffrey, 01 November 2021 - 05:49 PM.



0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users