Help starting out

MHXGMEX

New member
Hey, guys, hope you're doing well.
I'm the guy doing the Maverick Hunter X: Giga Mission EX project. Initially I wanted to build my own beat 'em up engine, but because of matters with Corona and employment, I found time is one thing I'm short on.

https://twitter.com/gigamissionx/status/1288158091134013443

A pretty barebones video, but something I managed to put together in my free time from assignments in a couple of days.

I figured I could look into porting most of the progress on OpenBOR since it'd help in development with some fundamentals, but as expected I'm running in a few snags. I'm having difficulty getting enemies to palette swap - I tried to apply the same palette via Photoshop batches, but apparently I'm doing something wrong. I tried both remap and alternatepal and I'm not having much luck with it.

4D0oESB.png
ptgk7Wj.png
y5FES2K.gif

The one on the left is the image used to get the palette, the one on the middle has the purple gem swapped out for green, and the one on the right is an example of a sprite that goes pitch black after using the one on the left to spread the palette. It's weird that even so the one on the middle doesn't manage to remap the enemy. I did try changing the color depth to 32 bit as well.

I did look into Fighter's Factory (most recent version) but it's crashing on me every time I try to start a new character, so I don't know if I'm doing something wrong or stumbled on a legit bug.

Besides that, what are some programs suggested for more indepth editing? I saw the tool Piccolo created, but I don't know whether BoRStats is more stable/recommended over it. And is there any way to skip the title screen straight into character select or the engine for debugging?

I imagine these questions have been answered time and again but I'm just having a tough time balancing my free time besides reading the manuals and trying to filter through trial and error what I'm doing wrong to manage to read the documentation and threads as they would warrant.

Thanks very much in advance.
 
Kratus said:
...Is there a variable that is accessible by script and allows me to change the current highlighted player (or model)?

Kratus,

No, but yes. There isn't any kind of global variable that tracks your selection, script or otherwise. Believe it or not, all the engine does when you select players is perform a model swap - the same one you can do with the changentityproperty(<entity>, "model", <modelname>) function in script. That's it! Here's what the selection loop looks like, it may give you an idea how to proceed. I recently documented it too, so that should help. :)

If you want to see the whole function, look for int selectplayer(int *players, char *filename, int useSavedGame) in openbor.c.

Code:
// Stay in selection until escape or exit.
	// 
	// exit = all players ready (selected) and exit delay expired.
	// escape = Escape key pressed.
	while (!(exit || escape))
	{
		players_busy = 0;
		players_ready = 0;

		// Loop through players.
		for (i = 0; i < set->maxplayers; i++)
		{
			// Current player index not yet selected?
			if (!ready[i])
			{
				
				// This is where we present player selections. The logic is long
				// and a little messy, so buckle up! Basically, we want to spawn
				// an example entity to get started, and that example entity
				// is what player sees on the selection screen. Then we switch
				// its model/color/animation based on the situation and player
				// input.
				//
				// 1. If an example entity exists and is playing a transition 
				//  then we...
				//
				// a) do nothing if the animation isn't finished.
				//
				// b) If it IS finished...
				//
				// -- 1. If the animation is ANI_SELECTIN, then play ANI_SELECT.
				// 
				// -- 2. If aniamton is ANI_SELECTOUT, then we switch to new 
				// model (if available).
				//
				// 2. If player hasn't played yet, has some credits or 
				// can draw from credit pool and pressed any action button,
				// then we'll deal with their credit pool and spawn the
				// first example (selectable model preview). Having an 
				// example spawned also tells us the player has completed 
				// this step, and so it's OK to run actions from any of 
				// the others.
				//
				// 3. If the player pressed Left or Right instead and there's
				// an example spawned, then we find the previous/next 
				// character in line, and record it to a variable. Then we
				// see if example has ANI_SELECTIN. if it doesn't we switch
				// to new model. If it does, play ANI_SELECT.
				//
				// 4. If the player presses Up or Down, we have an example 
				// spawned and colourselect is enabled, then cycle to the
				// model's previous/next color set choice.
				//
				// 5. If the player presses any action button and we have
				// an example spawned, then we mark the player's ready delay 
				// flag, and stalltime. See the parent logic block for 
				// selection delay & exit details. This is the player
				// making their selection choice.

				// Example exists and select transition animation?
				if (example[i] 
					&& (example[i]->animnum == ANI_SELECTIN || example[i]->animnum == ANI_SELECTOUT))
				{
					// If still animating than do nothing. Let the transition finish.
					if (example[i]->animating)
					{
					}
					else
					{
						// Transition to select animation.
						if (example[i]->animnum == ANI_SELECTIN)
						{
							ent_set_anim(example[i], ANI_SELECT, 0);
						}

						// Transition from select (player selected another model, and the
						// select out transition is now finished). Repeat of left/right key 
						// logic below and probably needs consolidation.
						if (example[i]->animnum == ANI_SELECTOUT && model_new)
						{
							// Apply new model.
							ent_set_model(example[i], model_new->name, 0);

							// Copy example model name to player name variable.
							strcpy(player[i].name, example[i]->model->name);

							// If colorselect is enabled and nosame 2 is enabled, skip to
							// start at next avaialble color cycle. Otherwise just start 
							// with default color set (0).
							if (colourselect && (set->nosame & 2))
							{
								player[i].colourmap = nextcolourmapn(example[i]->model, -1, i);
							}
							else
							{
								player[i].colourmap = 0;
							}

							//  Apply color set.
							ent_set_colourmap(example[i], player[i].colourmap);
						}
					}
				}
				else if (!player[i].hasplayed
					&& (noshare || credits > 0)
					&& (player[i].newkeys & FLAG_ANYBUTTON))
				{

					//  Now this player has played.
					players[i] = player[i].hasplayed = 1;
					//printf("%d %d %d\n", i, player[i].lives, immediate[i]);

					// Noshare means each player has their own credit pool.
					if (noshare)
					{
						player[i].credits = CONTINUES;
					}

					// Credits cheat = infinite credits. If that's not enabled,
					// then deduct a credit.
					if (!creditscheat)
					{
						if (noshare)
						{
							--player[i].credits;
						}
						else
						{
							--credits;
						}
					}

					// Give player default number of lives, spawn
					// example model and cancel the key flag.
					player[i].lives = PLAYER_LIVES;
					example[i] = spawnexample(i);
					player[i].playkeys = 0;

					// Play sound effect.
					if (SAMPLE_BEEP >= 0)
					{
						sound_play_sample(SAMPLE_BEEP, 0, savedata.effectvol, savedata.effectvol, 100);
					}
				}
				else if (player[i].newkeys & (FLAG_MOVELEFT | FLAG_MOVERIGHT) && example[i])
				{
					// Give player a feedback sound.
					if (SAMPLE_BEEP >= 0)
					{
						sound_play_sample(SAMPLE_BEEP, 0, savedata.effectvol, savedata.effectvol, 100);
					}

					// Get model in use right now.
					model_old = example[i]->model;

					// Let's get the new model. Left key = previous model 
					// in cycle. Right key = next.
					if ((player[i].newkeys & FLAG_MOVELEFT))
					{
						model_new = prevplayermodeln(model_old, i);
					}
					else
					{
						model_new = nextplayermodeln(model_old, i);
					}

					// Do we have a select out transition? If so play it here. 
					// Otherwise switch to new model. 
					if (validanim(example[i], ANI_SELECTOUT))
					{						
						ent_set_anim(example[i], ANI_SELECTOUT, 0);				
					}
					else
					{
						// Apply new model.
						ent_set_model(example[i], model_new->name, 0);

						// Copy example model name to player name variable.
						strcpy(player[i].name, example[i]->model->name);

						// If colorselect is enabled and nosame 2 is enabled, skip to
						// start at next avaialble color cycle. Otherwise just start 
						// with default color set (0).
						if (colourselect && (set->nosame & 2))
						{
							player[i].colourmap = nextcolourmapn(example[i]->model, -1, i);
						}
						else
						{
							player[i].colourmap = 0;
						}

						//  Apply color set.
						ent_set_colourmap(example[i], player[i].colourmap);
					}					
				}
				else if (player[i].newkeys & (FLAG_MOVEUP | FLAG_MOVEDOWN) && colourselect && example[i])
				{
					player[i].colourmap = ((player[i].newkeys & FLAG_MOVEUP) ? nextcolourmapn : prevcolourmapn)(example[i]->model, player[i].colourmap, i);
					ent_set_colourmap(example[i], player[i].colourmap);
				}
				else if ((player[i].newkeys & FLAG_ANYBUTTON) && example[i])
				{
					if (SAMPLE_BEEP2 >= 0)
					{
						sound_play_sample(SAMPLE_BEEP2, 0, savedata.effectvol, savedata.effectvol, 100);
					}
					// yay you picked me!
					if (validanim(example[i], ANI_PICK))
					{
						ent_set_anim(example[i], ANI_PICK, 0);
					}
					example[i]->stalltime = _time + GAME_SPEED * 2;
					ready[i] = 1;
				}
		
			}
			else if (ready[i] == 1)
			{
				if (((!validanim(example[i], ANI_PICK) || example[i]->modeldata.animation[ANI_PICK]->loop.mode) && _time > example[i]->stalltime) || !example[i]->animating)
				{
					ready[i] = 2;
					exitdelay = _time + GAME_SPEED;
				}
			}
			else if (ready[i] == 2)
			{
				font_printf(psmenu[i][2], psmenu[i][3], 0, 0, Tr("Ready!"));
			}

			if (example[i] != NULL)
			{
				players_busy++;
			}
			if (ready[i] == 2)
			{
				players_ready++;
			}
		}

		if (players_busy && players_busy == players_ready && exitdelay && _time > exitdelay)
		{
			exit = 1;
		}
		update(0, 0);

		if (bothnewkeys & FLAG_ESC)
		{
			escape = 1;
		}
	}

For reference, here's how the entity property model change works so you can compare it against the model switch in player select.

Code:
 case _ep_model:
    {
        if(varlist[2]->vt != VT_STR)
        {
            printf("You must give a string value for model name.\n");
            goto changeentityproperty_error;
        }
        tempstr = (char *)StrCache_Get(varlist[2]->strVal);
        if(paramCount > 3 && SUCCEEDED(ScriptVariant_IntegerValue(varlist[3], &ltemp)))
        {
            set_model_ex(ent, tempstr, -1, NULL, (LONG)ltemp);
            if(!ent->weapent)
            {
                ent->weapent = ent;
            }
        }
        break;
    }

And the support function script is calling.
Code:
//change model, anim_flag 1: reset animation 0: use original animation
void set_model_ex(entity *ent, char *modelname, int index, s_model *newmodel, int anim_flag)
{
    s_model *model = NULL;
    entity tempe;
    s_defense *dfs = NULL;
    float *ofs = NULL;
    int   i;
    int   type = ent->modeldata.type;

    model = ent->model;
    tempe.exists = 0;

    if(!newmodel)
    {
        if(index >= 0)
        {
            newmodel = model_cache[index].model;
        }
        else
        {
            newmodel = findmodel(modelname);
        }
    }
    if(!newmodel)
    {
        borShutdown(1, "Can't set model for entity '%s', model not found.\n", ent->name);
    }
    if(newmodel == model)
    {
        return;
    }

    if(!(newmodel->model_flag & MODEL_NO_COPY))
    {
        if(!newmodel->speed.x)
        {
            newmodel->speed.x = model->speed.x;
        }
        if(!newmodel->runspeed)
        {
            newmodel->runspeed = model->runspeed;
            newmodel->runjumpheight = model->runjumpheight;
            newmodel->runjumpdist = model->runjumpdist;
            newmodel->runupdown = model->runupdown;
            newmodel->runhold = model->runhold;
        }
        if(newmodel->icon.def           <   0)
        {
            newmodel->icon.def          = model->icon.def;
        }
        if(newmodel->icon.pain       <   0)
        {
            newmodel->icon.pain      = model->icon.pain;
        }
        if(newmodel->icon.get        <   0)
        {
            newmodel->icon.get       = model->icon.get;
        }
        if(newmodel->icon.die        <   0)
        {
            newmodel->icon.die       = model->icon.die;
        }
        if(newmodel->shadow         <   0)
        {
            newmodel->shadow        = model->shadow;
        }
        if(newmodel->knife          <   0)
        {
            newmodel->knife         = model->knife;
        }
        if(newmodel->pshotno        <   0)
        {
            newmodel->pshotno       = model->pshotno;
        }
        if(newmodel->bomb           <   0)
        {
            newmodel->bomb          = model->bomb;
        }
        if(newmodel->star           <   0)
        {
            newmodel->star          = model->star;
        }
        if(newmodel->flash          <   0)
        {
            newmodel->flash         = model->flash;
        }
        if(newmodel->bflash         <   0)
        {
            newmodel->bflash        = model->bflash;
        }
        if(newmodel->dust.fall_land        <   0)
        {
            newmodel->dust.fall_land       = model->dust.fall_land;
        }
        if(newmodel->dust.jump_land  <   0)
        {
            newmodel->dust.jump_land       = model->dust.jump_land;
        }
        if(newmodel->diesound       <   0)
        {
            newmodel->diesound      = model->diesound;
        }

        for(i = 0; i < max_animations; i++)
        {
            if(!newmodel->animation[i] && model->animation[i] && model->animation[i]->numframes > 0)
            {
                newmodel->animation[i] = model->animation[i];
            }
        }
		//normalize_anim_models(model, newmodel);

        // copy the weapon list if model flag is not set to use its own weapon list
        if(!(newmodel->model_flag & MODEL_NO_WEAPON_COPY))
        {
            newmodel->weapnum = model->weapnum;
            if(!newmodel->weapon)
            {
                newmodel->weapon = model->weapon;
                newmodel->numweapons = model->numweapons;
            }
        }
    }

    //Make a shallow copy of old entity values, not safe but easy.
    //Also copy offense and defense because they are more likely be used by weapons,
    //other references are left alone for now
    if(Script_IsInitialized(newmodel->scripts->onmodelcopy_script))
    {
        tempe = *ent;
        dfs = malloc(sizeof(*dfs) * max_attack_types);
        ofs = malloc(sizeof(*ofs) * max_attack_types);
        memcpy(dfs, ent->defense, sizeof(*dfs)*max_attack_types);
        memcpy(ofs, ent->offense_factors, sizeof(*ofs)*max_attack_types);
        tempe.defense = dfs;
        tempe.offense_factors = ofs;
    }

    ent_set_model(ent, newmodel->name, anim_flag);

    ent->modeldata.type = type;

    if((newmodel->model_flag & MODEL_NO_SCRIPT_COPY))
    {
        clear_all_scripts(ent->scripts, 0);
    }

    copy_all_scripts(newmodel->scripts, ent->scripts, 0);
    memcpy(ent->defense, ent->modeldata.defense, sizeof(*ent->defense)*max_attack_types);
    memcpy(ent->offense_factors, ent->modeldata.offense_factors, sizeof(*ent->offense_factors)*max_attack_types);

    ent_set_colourmap(ent, ent->map);
    if(Script_IsInitialized(ent->scripts->onmodelcopy_script))
    {
        execute_onmodelcopy_script(ent, &tempe);
        if(ofs)
        {
            free(ofs);
        }
        if(dfs)
        {
            free(dfs);
        }
    }
}

HTH,
DC
 
I had downloaded the "Base" template (which comes with the Darkstalkers Set/Truck level set/etc) when preparing to try to do my own stuff so I managed to get an animated title screen done no problem since the script is just very easy:
DOu2uFv.gif


However, I'm having trouble now managing to have reflection apply to a single spot in the level using just the mirror 1 function.

ROzdyxj.png


I think O Ilusionista used just that function and layered his level accordingly for his Avengers mod (fantastic play so far btw - I didn't have much time to spend on it but my brother and I flexed our retro muscles playing the original Data East Avengers for nostalgia's sake and yours definitely feels like the game brought to speed for modern times). I tried to follow that lead but apparently everytime I use "layer" to use my background image, that layers reflect what's in the background as if I used it with the background command instead. Is there a particularly more effective way to enforce it like you did it, or did you have to use scripts when it came down to it?

Thanks again.
 
Damon Caskey
No, but yes. There isn't any kind of global variable that tracks your selection, script or otherwise. Believe it or not, all the engine does when you select players is perform a model swap - the same one you can do with the changentityproperty(<entity>, "model", <modelname>) function in script. That's it! Here's what the selection loop looks like, it may give you an idea how to proceed. I recently documented it too, so that should help. :)

If you want to see the whole function, look for int selectplayer(int *players, char *filename, int useSavedGame) in openbor.c.
Great! Thanks for send me the select screen code. Now I can understand better how it works. I will take a look and make some tests
 
Hit a new snag - apparently the hitbox for one attack, or maybe something else entirely, isn't working as intended:
The heavy attack which knocks enemies, the fourth one, many times just don't seem to register. Although fiddling with animation parameters - like making some frames take longer or not - helps with it, it's never fixed 100% and the attack just "goes through" like in many points in that video. Curiously, it seems like key pressing tempo has to do with it - if I press the keys less fast it seems to register more successfuly, but I'd like to just let the players be able to buttonmash to their heart's content instead of trying to find the right rhythm.

I don't recall any other modules I played feeling off like this so it's definitely something I'm doing wrong. I tried adding a 10 depth to the frame that lasts longer on openborstats but didn't seem to change much either. Maybe it needs to be applied to all other frames?

Here's the code:
Code:
anim	attack4
	bbox	20 37 24 53
	sound	data/sounds/Ann_att1.wav
	delay	8
	attack	3 32 16 6 1 0 0 0 0 0
	offset	33 87
	frame	data/chars/mary/annette_25_12.png
	delay	12
	movez	0
	movea	0
	move	0
	attack	26 21 40 53 10 1 0 0 0 0
	bbox	8 37 24 53
	offset	20 87
	frame	data/chars/mary/annette_25_14.png
	movez	0
	movea	0
	move	0
	delay	6
	attack	34 66 31 15 10 1 0 0 0 5
	frame	data/chars/mary/annette_25_15.png
	attack	34 66 31 15 10 1 0 0 0 5
	delay	4
	frame	data/chars/mary/annette_25_15b.png
	delay	2
	offset	16 59
	bbox	8 6 20 54
	bbox	8 6 20 54
	attack	0 0 0 0 0 0 0 0 0 0
	frame	data/chars/mary/annette_25_01.png

Thanks in advance again.
 
Curiously, it seems like key pressing tempo has to do with it - if I press the keys less fast it seems to register more successfuly,
yes, it does. But its more than that.

First, if you want an attack to hit many times, you have to set the "fastattack 1" and you need an "attack 0" between the frames, as it is stated at the manual:

fastattack {bi}

Normally, in order for an attack to hit entities multiple times, the attack boxes must be separated by at least one frame with an empty attack box (one set to all 0) and must also be separated by a brief delay.
If this is set to 1, this animation's attack boxes are not restricted by the delay (it will still need an empty attack box between frames, though).

Second,  the engine will apply a "cooldown" to avoid the same move to hit over and over - or the same attack would hit 200 times per second :) - so it knows which attack id had connected with what and prevents it to hit again.

I've discovered that if you make a chain combo with the same attack with short delays, the engine won't register those hits thanks to that cool down.
You can try to bypass it (I need to test this myself too) with this:

ignoreattackid {int}

Animation header.
0, Default, you can't use consecutive attack frames. You need to alternate attack ... with attack 0
1, You can use all attack frames without limits!

And the attacks should connect.  Don't worry - the same attack won't hit 200 times per second :)
 
MHXGMEX I was looking for a solution for this  - since I have the same problem - and looks like I've found it.
Plus, I think I understood how the engine handles it: the hit pause and the frame delay will have change the hit behaviour.

On my Avengers game, I have some characters which attacks very fast, but I often see that if I use the same attack 2 times in attackchain (or even different attacks) that are very fast, not all 4 attacks will connect, only 3. Then I tried to input the combo a little slower and it registers fine.

I had an attack (important numbers in red) which was failing a lot (and I use it two times on my attack chain) which had a fast delay (4).
Even I setting FASTATTACK 1, the second time I use the same attack... it fails.


anim attack2
loop 0
delay 4
offset 245 270
bbox 229 216 39 55
hitflash flash2
jugglecost 4
fastattack 1
frame data/chars/vision/att200.gif
attack 245 218 52 18 10 0 0 0 5 10
frame data/chars/vision/att202.gif
attack 0 0 0 0 0 0 0 0 0 0
frame data/chars/vision/att201.gif

Then I tried to use "ignoreattackid 1" and the move connects... but the punch hits two times, very fast:

anim attack2
loop 0
delay 4
offset 245 270
bbox 229 216 39 55
hitflash flash2
jugglecost 4
fastattack 1
ignoreattackid 1

frame data/chars/vision/att200.gif
attack 245 218 52 18 10 0 0 0 5 10
frame data/chars/vision/att202.gif
attack 0 0 0 0 0 0 0 0 0 0
frame data/chars/vision/att201.gif

So what I had discovered? Using IGNOREATTACKID 1, your hit pause needs to be faster than your frame delay.
Since my frame delay is 4, if I set my attack pause to 4 and beyond, the move hits 2 times per attack. But If I set my hitpause to 3, only one hit is registered by the same attack, and the attack won't fail anymore during combos.

I just had to set the hitpause to 3 and it works

anim attack2
loop 0
delay 4
offset 245 270
bbox 229 216 39 55
hitflash flash2
jugglecost 4
fastattack 1
ignoreattackid 1

frame data/chars/vision/att200.gif
attack 245 218 52 18 10 0 0 0 3 10
frame data/chars/vision/att202.gif
attack 0 0 0 0 0 0 0 0 0 0
frame data/chars/vision/att201.gif


 
Oh, hey man, nice to hear you found a foolproof fix! The fastattack bit you mentioned had worked already since Annette's moves weren't that fast, but it's very good to know anything else that could come from faster moves has been sorted out.

Was just busy with life to reply sooner to the thread, but it made everything real stable. In the WIP video I made to show some friends it clearly comes through.

Thanks for the help again!
 
Oh the game is pretty cool already - I like how you split the layers.
I was going to say that I have some content ripped from that game (like the statue), but you already have it. I think I have some stuff ripped, like a big titan, if you want

At 1:19, we can see that some statues (both in the background and in the front) doesn't shake when the screen shake. You can change this using "noquake", because the statues should shake with the stage. For the fglayer, you can use a quake setting to control it.


EDIT: another point I noticed - her attacking sprites are misaligned. Pay attention to her feet and how they move when she attacks - she should have the feet at the same position just rotating the postion because they work as an achor point.

This is how it should look - pay attention to her feet.
The back foot is her anchor, holding all her body weight - this is why it never changes. The front leg can shift a bit because she shares the body weight with that leg in some frames, in some not.
8uz9hAc.gif


I've made a tutorial about anchor point here, maybe it will be useful to you http://www.chronocrash.com/forum/index.php?topic=4300.msg59966#msg59966
 
Yeah, the anchors are probably all over the place because when I first noticed the issue with the hitboxes (way back in September) I was trying everything under the sun to get it fixed before we figured out the fastattack bit. Her slashes were very slow as a stopgap fix, so I need to get them sorted out next time I edit the engine.

Thanks for all the tips on the statues as well - I kinda like the big statues in the foreground staying how they are, but the knight statues that double for the little trap definitely needed fixing. I'll get to it too. Glad to hear the stage's looking good so far as well.

I think I have some stuff ripped, like a big titan, if you want
Absolutely, any and all help greatly appreciated. If it's the first stage boss I have some rips of my own, but all I did manage was just rip most of stage 1 before I had to spend pretty much over a month without touching the engine due to real life issues.

dmoVNj5.png
 
yeah, the big statues in front of the stage are optional if they shake or not (because they are away from where the quake happens, but I do prefer to let mine shaking as well), but the ones over the floor (behind the player) needs to shake for sure.

And yeah, that guy :)
 
Back
Top Bottom