Jump to content

Photo
* * * * * 6 votes

[Scripting/2.50] ghost.zh Tutorial


  • Please log in to reply
4 replies to this topic

#1 Lejes

Lejes

    Seeker of Runes

  • Members
  • Location:Flying High Above Monsteropolis

Posted 19 June 2015 - 11:26 PM

GHOST.ZH TUTORIAL

===========================================
HEADER 1 - GLOBAL SCRIPT SETUP
===========================================

Did you write import "ghost.zh" at the top of your script file? Good. Doing that automatically imports a bunch of stuff, including the ghost.zh global script.
If it's your only global script, you just need to put "GhostZHActiveScript" in your Active global slot. If you're using others, use this handy guide to combine them!
 
import "std.zh"
import "string.zh"
import "ghost.zh"

global script GhostAndOtherStuff
{
	void run()
	{
		int dumbvariable;
		float someotherdumbvariable;
		bool dumboolean;
		int dumbarray[10];
		
		StartGhostZH();
		
		while (true)
		{
			UpdateGhostZH1();
			DumbFunction();
			Waitdraw();
			Z3Scrolling();
			UpdateGhostZH2();
			Waitframe();
		}
	}
}
The important things to look at here are the start of the while loop, Waitdraw(), and Waitframe(). The three global ghost functions need to be placed in relation to these.
As long as they're in the proper order, everything should work fine.

===========================================
HEADER 2 - INITIALIZATION FUNCTIONS AND YOU
===========================================

With the global out of the way, we'll get to the meat of what gives your ghosted enemy its unique identity: its FFC script. The syntax for these is like any other FFC script.
 
ffc script CoolAssMoblin
{
	void run()
	{
		// Enemy script goes here.
	}
}
Like other FFC scripts, these will have to be loaded into slots when you import and compile your main script file. These scripts will generally run as soon as the enemy appears,
and quit when it dies. So how do you start a ghosted enemy script? With an initialization function of course. ghost.zh provides six of these, but for simplicity, we'll be focusing
on two of them.
 
npc ghost = Ghost_InitCreate(this, enemyID);
This initialization function will create an enemy with the given ID at the location of the FFC running this script and return a pointer to it. This enemy will thereafter be
controlled by the FFC script. The pointer the function returns will be used a lot, so give it a good name! I like using "ghost". As the function name implies, the enemy will
be created by this function, so you don't have to place it in ZQuest. Just make sure the ID you're using has all the right stats with the enemy editor. Since it will spawn on top
of the FFC, the FFC's location is effectively the enemy's spawn point. Spawn flags are irrelevant. The FFC's CSet will also be the enemy's CSet.

VxmTAY5.png
 
npc ghost = Ghost_InitAutoGhost(this, enemyID)
This is another initialization function, AutoGhost. It's my personal favorite of the bunch. It allows you to simply place your enemy on screen, without ever touching the FFC
properties in ZQuest. AutoGhost has a special feature to make placing enemies easier.
 
void run(int enemyID)
The arguments to run() are normally set in ZQuest in the FFC's Arguments tab. AutoGhost, however, can automate this for you. By starting your FFC script with that line, you'll
be able to use any enemy ID. You might set up your enemy as ID 177, but someone else could import this script into their quest and use ID 201 without editing the script at all.

Using AutoGhost does require a bit more setup, but more on that in the next section. It's worth the effort for how much easier it makes things later.


===========================================
HEADER 3 - ENEMY EDITOR BLUES
===========================================

You thought you could jump straight into using your enemy now? WRONG. You have to open ZQuest, like some kind of caveperson. Each ghosted enemy will have its own entry
in the enemy list, like a normal enemy. You have to edit this enemy's properties so ghost.zh can work its magic. If you're using Ghost_InitCreate, this is easy.
That function will create any existing enemy. There are two important things to remember. First, you need to set its HP to a non-zero number! If you don't, your
poor creature will immediately die, questioning why it was given life in the first place! Second, you need to make sure its type is anything other than "(None)".
If you don't set a type, the enemy won't spawn at all. To start out with, let's select "Other" type.

aC8kiNB.png

It's the type used by the flames surrounding Zelda. It takes no knockback and doesn't move, so it's a perfect canvas for script magic.

AUTOGHOST
Remember when I said AutoGhost required a little extra setup in ZQuest? It's happening now! On top of setting the enemy's HP and type, there are two more important properties
you must set for AutoGhost to work: Miscellaneous Attributes 11 and 12. These normally aren't used for anything, but scripts can use them, and that's exactly what AutoGhost does.

Miscellaneous Attribute 11 is used for the enemy's graphic. Setting this number to -1 will use the enemy's normal tile. Setting it to a positive number will use the same number
combo from your quest file. These combos can be animated. Leaving this attribute at 0 means the enemy won't work at all, so don't forget! When using a combo, its CSet will be
decided by the CSet of whatever tile you chose for the enemy. The original tile will show up briefly when you first enter the room, even if the enemy is set to use a combo, so
use a blank tile with the right CSet for those.

Miscellaneous Attribute 12 is the script slot number that will be used for this enemy. You can check this at any time by compiling your scripts. AutoGhost has a special feature.
If you set this attribute to -1, ghost.zh will attempt to find the right script based on the enemy's name. By default, it uses @ as a marker to delineate the script name.
As an example, let's use that script titled "CoolAssMoblin" I started above. Set attribute 12 to -1, and name it like so:
 

Enemy177IGUESS@CoolAssMoblin

You can put whatever you want before the @, but the text after the @ must match the script name exactly, otherwise it won't work! By not specifying a script slot number,
your enemy will be easier to transfer between quests. As with attribute 11, your entire scripting adventure will fail if you leave attribute 12 as zero.

TXcNo2R.png

===========================================
HEADER 4 - THE TRUE GHOST.ZH STARTS HERE
===========================================

You're finally here. It's time to open Notepad++ and unleash your monstrosity upon the world. By now, you've done all the legwork in ZQuest for your ghosted enemy to take advantage of.
Let's take a look at that skeleton of a monster script from before.
 
ffc script CoolAssMoblin
{
	void run(int enemyID)
	{
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			// Cool ass Moblin stuff goes here.
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
You may have noticed several new elements here. Let's break it down.

First is the script type and name. It's an FFC script with the name "CoolAssMoblin".

Second is the run() function. This is where all your cool stuff will go. Arguments to run (the stuff in parentheses) must be declared like new variables, and set in ZQuest (unless
you're using AutoGhost magic). In this case, D0 of the FFC you're using for this enemy will be its enemy ID.

Third is the initialization function. This lets the ghost global functions know they should start doing their thing to this enemy. This particular initialization function returns a
pointer to your enemy, which is very useful because you'll be using it a lot.

Fourth is the while loop. You generally want an infinite loop for this. That way, it can run through the code within the loop over and over, doing cool ass Moblin things until the
end of time. Or until Link leaves the room or kills the enemy. Whichever comes first.

Fifth is something you may not have seen before, Ghost_Waitframe. You probably already know that every infinite loop needs a Waitframe if you want to avoid locking up ZC. Ghosted enemies,
however, need their own special Waitframe for reasons. Do not ever use plain Waitframe in a ghosted enemy script! Not even before the initialization function! If you do, the enemy won't
work. Let's take a closer look at the four arguments to Ghost_Waitframe. "this" is an FFC pointer, and simply refers to the FFC currently running this script. This will never change, so
don't worry about it. "ghost" is the NPC pointer you defined earlier with the initialization function. See why it was so important? It doesn't have to be named "ghost", but I feel
it's easier to keep track of if it is. The first "true" is the value of the clearOnDeath variable. If it's true, the enemy's graphic is cleared when it dies. The second "true"
is the quitOnDeath variable. If it's true, the script calls Quit() once the enemy dies. Otherwise it'll keep going, even if the enemy isn't there. You generally want both of these
arguments to be true, unless you want to do something fancy when the enemy dies. A Ghost_Waitframes variant also exists, if you want your enemy to sit around doing nothing for a while.

===========================================
HEADER 5 - MOVEMENT MYSTERIES
===========================================

The example code above will actually compile, and when you include it with the global ghost functions and ZQuest setup, will produce a fully functional ghosted enemy.
An enemy that does precisely nothing. You would like your enemy to do slightly more than nothing, so let's get started on that. We'll begin with the most basic enemy function: movement.
You could technically create your own movement function that directly adjusts the enemy's X and Y coordinates, dragging it around like the demented puppetmaster you are, but the entire
reason you're using ghost.zh is to make that kind of thing easier. To that end, ghost.zh has a number of presets and generalized movement functions. Let's look at one befitting a Moblin.
 
float Ghost_HaltingWalk4(int counter, int step, int rate, int homing, int hunger, int haltRate, int haltTime)
This function recreates the four way movement used by Moblins and various other enemies in the original game. Most of the arguments correspond to values you can see in the enemy editor,
but two are unfamiliar: counter and haltTime. haltTime is obvious from the name. It's how long, in frames, an enemy is stopped whenever it halts. Original flavor Moblins use 48 for this.
counter is a little more involved. It is a value used internally by this function. In order to work, it must be set to -1 to start with, then set to the function's return value every frame.
 
ffc script CoolAssMoblinHaltingWalk
{
	void run(int enemyID)
	{
		float counter = -1; // Our counter variable for Ghost_HaltingWalk. Starts at -1.
		
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			counter = Ghost_HaltingWalk4(counter, ghost->Step, ghost->Rate, ghost->Homing, ghost->Hunger, ghost->haltRate, 48);
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
This example showcases something important about ghost movement functions: they must be called every frame. You can't just call it once and expect the enemy to keep on chugging!
If you stop using the movement function, the enemy will stop moving! So now we've got an enemy that walks around, occasionally stopping for 48 frames. It could even conceivably walk into
Link and damage him!

Another type of movement function is Ghost_MoveAtAngle. It makes an enemy move at an angle! Wow! This movement function doesn't use a mysterious counter variable, so you can do what it
says on the tin!
 
Ghost_MoveAtAngle(float angle, float step, int imprecision)
Angle is in degrees, our favorite unit of measurement! Step speed, however, is a nasty trap! You might think you can give your enemy a step speed of 150 or something, like normal. But if you do that, your enemy will bug out into the nether realm! Spooky! The reason is that this function uses a different unit of measurement for no discernible reason. In order to get the right speed, just divide normal step speed by 100. So our 150 guy will actually be 1.5. HaltingWalk4, however, did use normal step speed. These functions can be the worst sometimes. Imprecision looks confusing, but in practice it isn't. It just gives your enemy some extra pixels of leeway for collision detection with walls, so they don't get stuck. I usually set it at 2.
 
ffc script CoolAssMoblinMoveAtAngle
{
	void run(int enemyID)
	{
		float angle; // The angle our dude will move at.
		int angle_change; // Generic variable to use as timer for changing the angle.
		
		npc ghost = Ghost_InitCreate(this, enemyID);
		float step = ghost->Step * 0.01; // Step speed must be divided by 100, otherwise Ghost_MoveAtAngle moves the enemy at supersonic speed!
		
		while (true)
		{
			if (angle_change == 0)
			{
				angle = Rand(361);
			}
			Ghost_MoveAtAngle(angle, step, 2);
			
			angle_change = (angle_change + 1) % 60; // Counts up from 0, then resets back to 0 when it hits 60.
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
Now our guy moves at a random angle that changes once per second.

The movement functions are nice and all, but there are other ways you can move your enemy. One is to directly adjust its position. This movement can be as complex as you want it to be, since you're the one writing the function for it. As an example, let's create a movement type that doesn't exist in the presets: circular movement.
 
ffc script CoolAssMoblinCircle
{
	void run(int enemyID)
	{
		float angle; // Where in the circle our dude is at.
		int radius = 64; // The radius of the circle in pixels.
		
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			Ghost_X = (Link->X + 8) + (radius * Cos(angle));
			Ghost_Y = (Link->Y + 8) + (radius * Sin(angle));
			
			angle = (angle + 1) % 360; // Go up by 1 every frame, then reset to 0 at 360.
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
With the power of math, the Moblin now moves in a circle around Link. Note how the special variables Ghost_X and Ghost_Y are the ones changed, rather than ghost->X and ghost->Y. You should generally try to use the Ghost variables when working with ghosted enemies, rather than directly using the ones available under the NPC pointer. It'll probably be fine if you do, though. Probably. Another way to move your enemy is to manipulate its velocity and acceleration. You might be thinking, "But enemies don't have velocity and acceleration variables!". And you'd be right...if not for ghost magic! With ghost.zh, you can give enemies velocity and acceleration values, and the header will handle the movement from there. As an example, let's look at an enemy that goes back and forth between two points.
 
ffc script CoolAssMoblinVelocity
{
	void run(int enemyID)
	{
		npc ghost = Ghost_InitAutoGhost(this, enemyID);
		
		int min_y = 32; // Starts going down again at this y.
		int max_y = 112; // Starts going up again at this y.
		float speed = 1; // Normal movement speed.
		float accel = 0.05; // Acceleration while changing direction. Should be a very small value!
		
		while (true)
		{
			// Changing direction at the bottom of the screen.
			if (Ghost_Y > max_y && Ghost_Vy > 0)
			{
				Ghost_Ay = -accel;
				while (Ghost_Vy > -speed)
				{
					Ghost_Waitframe(this, ghost, true, true);
				}
				Ghost_Ay = 0;
				Ghost_Vy = -speed;
			}
				
			// Changing direction at the top of the screen.
			if (Ghost_Y < min_y && Ghost_Vy < 0)
			{
				Ghost_Ay = accel;
				while (Ghost_Vy < speed)
				{
					Ghost_Waitframe(this, ghost, true, true);
				}
				Ghost_Ay = 0;
				Ghost_Vy = speed;
			}
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
It would have been easy to just make our enemy change velocity at certain points, but that would make for very abrupt changes in speed at the min and max y values. So we use an acceleration value to make these changes more gradual. If you use acceleration values, make sure to be very careful with them! Acceleration is the change in velocity over time, and ZC's unit of time is frames, so velocity can grow unchecked very quickly unless you make sure to cap it or manage the acceleration value.

===========================================
HEADER 6 - BIG EWEAPON, 3 A.M.
===========================================

Now you can make your enemy dance like a ballerina, but aren't you forgetting something? Trying to body slam directly into the player is a classic video game staple, but enemies also tend to be smart enough to use basic tools (or just breath fire, as the case may be).
 
eweapon FireEWeapon(int weaponID, int x, int y, float angle, int step, int damage, int sprite, int sound, int flags)
This is the basic function for firing an eweapon. You'll notice that there's no argument for an enemy pointer. Ghost.zh's eweapon functions can be used entirely independently of any enemy! You can have a script that does nothing but fire 100 rocks per second in random directions. That is a thing you can do. For now, we'll limit ourselves to using this inside an enemy script, though.
 
ffc script CoolAssMoblinFireball
{
	void run(int enemyID)
	{
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			FireEWeapon(EW_FIREBALL, Ghost_X, Ghost_Y, Randf(PI2), 200, ghost->WeaponDamage, -1, -1, EWF_UNBLOCKABLE);
			Ghost_Waitframes(this, ghost, true, true, 60);
		}
	}
}
Our immobile once more Moblin now fires a fireball once per second. Take note that the angle argument is in radians, hence the need for Randf and pi (2 * PI is the number of radians in a circle!). Step is 200 and damage can use the set-in-editor weapon damage. Sprite and sound effect are both set to -1, meaning they'll use the default sprite and sound for that weapon type. Speaking of weapon type, check it out:



There you go, that's all the weapon types. Most of the non-script types have their own inherent, annoying properties, like bombs exploding and fires disappearing after a short time. Use them at your own peril! Then there's the flags. These are properties unique to eweapons controlled by ghost.zh.
 
 * EWF_UNBLOCKABLE
 *    The weapon is unblockable.
 *
 * EWF_ROTATE
 *    The weapon's sprite will be rotated and flipped according to the
 *    weapon's direction.
 *
 * EWF_ROTATE_360
 *    The weapon will be drawn using Screen->DrawTile(), allowing the sprite
 *    to rotate in whichever direction the weapon is moving. The base sprite
 *    set in Quest > Graphics > Sprites > Weapons/Misc should be pointing
 *    to the right.
 *
 * EWF_SHADOW
 *    The weapon will cast a shadow if its Z position is greater than 0.
 * 
 * EWF_FLICKER
 *    The weapon will be invisible every other frame.
 * 
 * EWF_NO_COLLISION
 *    The weapon's collision detection will be disabled.
I only used the unblockable property for the fireballs up there, but what if you want to use more than one?
 
FireEWeapon(EW_FIREBALL, Ghost_X, Ghost_Y, Randf(PI2), 200, ghost->WeaponDamage, -1, -1, EWF_UNBLOCKABLE | EWF_ROTATE | EWF_SHADOW | EWF_FLICKER | EWF_NO_COLLISION);
All of the eweapon flags are actually just binary flags, so they can be ORed together like this. If you don't know what bitwise operators are, don't worry about it. Just use the bar ( | ) to combine the flags. This eweapon will now be unblockable by shields, rotate in accordance with its direction, have a shadow, flicker, and be completely unable to damage Link. If you want to use no flags at all, just put 0 there.

Spray and pray is fine sometimes, but what if you want the enemy to aim directly at Link? You can do that!
 
eweapon FireAimedEWeapon(int weaponID, int x, int y, float angle, int step, int damage, int sprite, int sound, int flags)
ffc script CoolAssMoblinAimedFireball
{
	void run(int enemyID)
	{
		eweapon fireball;
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			fireball = FireAimedEWeapon(EW_FIREBALL, Ghost_X, Ghost_Y, 0, 200, ghost->WeaponDamage, -1, -1, EWF_UNBLOCKABLE);
			Ghost_Waitframes(this, ghost, true, true, 60);
		}
	}
}
Now the Moblin fires a shot directly at Link once per second. You might have noticed there's still an angle argument. How is that relevant, you ask? This function uses it for a different purpose. The angle is now an offset from whatever direction Link is in. If you put PI/4 there, the shot will be angled 45 degrees clockwise from Link's position. Using a random angle for this is a great way to add some unpredictability to your monster's shots, as directly aimed shots can be rather easy to dodge if there's nothing else going on. You also might have noticed that these functions return an eweapon pointer, but we haven't been using it. These functions create the eweapon from nothing, and don't need an existing one to work. However, you might still need the pointer for stuff later, so you can store that in an eweapon pointer variable you define yourself. What will you use that pointer for? LET'S FIND OUT RIGHT THE HELL NOW!
 
void SetEWeaponMovement(eweapon wpn, int type, float arg, float arg2)
void SetEWeaponLifespan(eweapon wpn, int type, int arg)
void SetEWeaponDeathEffect(eweapon wpn, int type, int arg)
These functions have no return values, and they need an existing eweapon pointer to work. What do they do? Our good friend CoolAssMoblin will show us!
 
ffc script CoolAssMoblinWeaponEffects
{
	void run(int enemyID)
	{
		eweapon fireball;
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			fireball = FireAimedEWeapon(EW_FIREBALL, Ghost_X, Ghost_Y, 0, 200, ghost->WeaponDamage, -1, -1, EWF_UNBLOCKABLE);
			SetEWeaponMovement(fireball, EWM_SINE_WAVE, 32, 12);
			SetEWeaponLifespan(fireball, EWL_NEAR_LINK, 32);
			SetEWeaponDeathEffect(fireball, EWD_8_FIREBALLS, 17);
			
			Ghost_Waitframes(this, ghost, true, true, 60);
		}
	}
}
Now Friendmoblin shoots a fireball that travels in a sine wave pattern that explodes into 8 straight fireballs once it gets within 32 pixels of Link. If you're wondering what all the movement, lifespan, and death effect types are, here they are.

MOVEMENT


LIFESPAN


DEATH EFFECT


You don't necessarily have to use all 3 functions for every eweapon you create. Some of the movement types have lifespans built in and such. Be careful of eweapons sticking around when they're not wanted, though! The edge of the screen will annihilate any weapon that dares to come near it, but for anything that doesn't try to leave the screen, you might have to take things into your own hands. To be absolutely sure it's gone, you should use set EWD_VANISH (or some other effect) and use KillEWeapon(eweapon wpn) on anything you don't want around any more. Now that you know how to use the eweapon functions, have fun mixing and matching various effects! It's like playing dress-up! Just try not to break the player's face too much.

BONUS SCRIPT
 
ffc script CoolAssMoblinFlamethrower
{
	void run(int enemyID)
	{
		eweapon fire;
		float counter = -1;
		npc ghost = Ghost_InitCreate(this, enemyID);
		
		while (true)
		{
			counter = Ghost_HaltingWalk4(counter, ghost->Step, ghost->Rate, ghost->Homing, ghost->Hunger, ghost->haltRate, 48);
			
			// counter is equal to remaining halt time, so begin flamethrower after halted for 32 frames
			if (counter == 16)
			{
				for (int i = 0; i < 10; i++)
				{
					fire = FireAimedEWeapon(EW_FIRE, Ghost_X, Ghost_Y, Randf(-PI/4, PI/4), 150, ghost->WeaponDamage, -1, -1, EWF_UNBLOCKABLE);
					SetEWeaponLifespan(fire, EWL_TIMER, 119)
					SetEWeaponDeathEffect(fire, EWD_EXPLODE, ghost->WeaponDamage * 2);
					
					Ghost_Waitframes(this, ghost, true, true, 5);
				}
			}
			
			Ghost_Waitframe(this, ghost, true, true);
		}
	}
}
Yes, that's an exploding flamethrower. What of it?


And so ends this tutorial. For now. I'll be adding more later. Questions, comments, or complaints about how literally none of the example scripts work/compile are welcome.
  • Nimono, Demonlink, Hari and 2 others like this

#2 Logos

Logos

    Gnome Child

  • Members
  • Real Name:Kevin
  • Location:USA, North Carolina

Posted 19 June 2015 - 11:28 PM

Thanks for the dissertation! I'll check this out tomorrow when I'm not sleepy.

#3 Nimono

Nimono

    Ultra Miyoa Extraordinaire!

  • Members
  • Real Name:Matthew
  • Location:Static Void Kingdom

Posted 19 June 2015 - 11:36 PM

Very informative! With this tutorial, I actually feel I can start making my own ghost.zh enemies now, where before I was just too confused by everything to be able to. Thank you!



#4 Demonlink

Demonlink

    Lurking in the shadows...

  • Members
  • Real Name:Miguel
  • Location:Wouldn't you like to know?

Posted 19 June 2015 - 11:39 PM

HOLY CRAP!  :omg:  :omg:  :omg:

 

Dude, you have just made probably one of the most revolutionary contributions in the History of ZScript! Man, this really makes me want to give not just one round of applause, but three rounds of applause!   :clap:  :clap:  :clap:

 

I'll check this detail by detail tomorrow when I'm not sleepy like Logos. Thanks for such a tutorial! :D BTW:

			Z3Scrolling();

 

Finally!



#5 Taco Chopper

Taco Chopper

    protector of the darn forum

  • Moderators
  • Pronouns:He / Him
  • Location:South Australia

Posted 05 September 2022 - 12:37 AM

hello from the future

 

just posting here to let people know that using Ghost_InitCreate may cause feedback loops, or infinite NPCs spawning, or at least it did for me

 

the way it may best work is by using Ghost_InitAutoGhost instead. 

 

as pointed out by ywkls: The first one creates the npc, which already exists; then the copy makes a copy ad infinitum.

 

so yeah, this tutorial is great but just keep this in mind if you're watching dozens upon dozens of enemies spawn




1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users