Here's a 2.5D beat em up i did in 3D while learning godot 😉
Its extremely easy and turned out better than i would expect !!!

In case anyone wants to do anything simillar, or has any questions on how to make one let me know in here 🙂

project download link: --- godot version 3.4

.

i thought 2.5D was all 3D models viewed from a fixed sidescrolling (2D)camera perspective?

2.5D can mean anything that is not traditional 2D or 3D.

@cybereality said: 2.5D can mean anything that is not traditional 2D or 3D.

ok, didnt know that, tnx

5 days later

Alright, frist the most easier things.

to set all the folders to contain those small files

the sprites and scripts folders are the most important for now.

next creat a script in the scripts folder and call it functionsScript.gd

on the frist line type: class_name Entity extends KinematicBody

save the script ( ctrl+s )

now create 2 new scenes name it 3d_player1.tscn and the other 3d_enemy1.tscn

now to add a KinematicBody3D: In the search bar type Entity this will show a KinematicBody3D with the word Entity it is a subClass of the functions script, this makes things better in the end

every code ( functions ) you write on the functions script will be usable in the player and enemy code (script).

Now try to add the extra objects to the player scene.... CollisionShape and animatedSprite3D

the MeshInstance is just a rectancle that point left or right depending on were the player is facing the animation node is the AnimatedSprite3D . I havent tried the other animation so its better to go with this one for now.

Then try to do the same thing for the enemy ( collision shape and animation )

it uses the same nodes has the player.

To create the Areas ( hitBox, hurtBox, grabBox, grabbedBox ) it will be diferent they are individual scenes.

Create a new scene - choose other node and select Area

In this case the name it hitBox ( ctrl + s .. save ) add a collision shape newBox shape - All collision shapes will use the Box shape including the player and enemy KinematicBody collision shape.

the area hitBox is the only one that will have a node AudioStreamPlayer2D Use the same process to create all the other Areas ( hurtBox, grabBox, grabbedBox ) - ex: new scene - Area node - collisionShape node. I forgot to write here when saving the scenes save it to the respective folders

both player and enemy will use the same scenes
hitBox.tscn, hurtBox.tscn, grabBox.tscn, grabbedBox.tscn )

Once you have both the player and enemy all set up looking like the images above. let me know, i will continue

8 days later

@BeanJiang said: A good teaching

( tell me if you finish i will continue )

a month later

Finished! Please continue... :)

(Would it be better to export the project at various steps? That would guarantee no mistakes were made by anyone reading and trying to follow along...)

@Semaj31273 said: Finished! Please continue... :)

(Would it be better to export the project at various steps? That would guarantee no mistakes were made by anyone reading and trying to follow along...)

I could upload it, but i dont think it can be made using KOF sprites. Iam too lazy to draw stick man's... but the KOF sprites are better they have important details that give a better visual represetation of an arcade game.

I will upload it maybe to google drive, ( or the forum if i replace the sprites with a stick man )... Once godot 4 is out and i can continue with my previous game... Right now godot 4 alpha keeps crashing to the desktop...

In this case ive used the animated sprite3D node... ( i guess the animation player node could be used instead, and it freezes playing once the _physics_process exits, but this way made it simplier for me )

the frame speed needs to be set to ZERO in all animation, the loop can stay ON or OFF depending on the animation type like WALK or IDLE have the loop ON.

In animations that dont LOOP the attack2 anim for example the last frame is a copy of the previous frame ( frame 5 and 6 are equal )... So when frame == 6; state = "idle"

Next is the functionScript.gd and the player.gd ( scripts ) The main variables all stay in the functionScript.gd so they can be shared by the player and enemy and other entities.

For now the important ones are:

    var motion = Vector3();
    var walkSP = 2;
    var runSP = 0;
    var acceralation = 0.5;
    
    var gravity = -14.0;
    var JUMP_HEIGHT = 7.5;
    var RUN_JUMP_HEIGHT = 5.2;
    var health = 150;
    
    var my_direction = -1;
    
    #-------///-----//---------------
    
    var state = "st_idle";
    var previousState = "st_idle"; #---just a helpfull var not really important
    var isPaused = false;
    var pauseTimer = 0; # in seconds
    
    var frameDelay = 0.35;
    var frameTimer = 1;
    var delay_frame = 0;
    var nextFrame = false;

#--//--entity-animation--//--
onready var anim = 0;

functionScript.gd ( continue ) this is the function that will determine the animation speed

func _animationSpeed(_animName, _ddelta):
	if ( frameTimer > 0 ):
		frameTimer -= frameDelay * _ddelta;
		nextFrame = false;
	elif ( frameTimer <= 0  ):
		nextFrame = true;
		frameTimer = 1;
		var frame_count = _animName.frames.get_frame_count(_animName.animation);
		var frame_loop =  _animName.frames.get_animation_loop(_animName.animation);
		
		if ( delay_frame < frame_count -1 ):
			delay_frame += 1;
		elif ( frame_loop == true ):
			delay_frame = 0;

#------------//-------------//---------------------- This function is used to change the state the entity is on ( same var need to be on Comment for now )

func _changeState(stateName):
	if ( state != stateName ):
		#_attackBoxZero();
		frameTimer = 1;
		frameDelay = 0.001;
		delay_frame = 0;
		#attackOne = false;
		#keyComboEnable = false;
		previousState = state;
		state = stateName;

#------------//-------------//---------------------- #------------//-------------//---------------------- Next in the player.gd ( player script ) He only contains 3 functions that are not shared with other entities

the func _drawText() is not really important, it just shows the values on screen for debugging using a Label node.

The func _move() contains some variables that will be used later they need be on comment for now

func _move():
	#_attackBoxZero();
	
	if ( is_on_floor() ):
		if ( keyRIGHT ):
			motion.x += acceralation;
			motion.x = clamp(motion.x, 0 , + (walkSP+runSP) );
			
			#motion.x = + (walkSP+runSP);
			_facingRight();
		elif ( keyLEFT  ):
			motion.x -= acceralation;
			motion.x = clamp(motion.x, - (walkSP+runSP) , 0 );
			#motion.x = - (walkSP+runSP);
			_facingLeft();
		else:
			motion.x = 0;

		if ( keyUP ):
			motion.z -= acceralation;
			motion.z = clamp(motion.z, - (walkSP+runSP) , 0 )
			#motion.z = - (walkSP+runSP);
		elif ( keyDOWN ):
			motion.z += acceralation;
			motion.z = clamp(motion.z, 0 , + (walkSP+runSP) )
			#motion.z = + (walkSP+runSP);
		else:
			motion.z = 0;

		if (motion.x || motion.z != 0 ):
			if ( runSP != 0 ):
				_changeState("st_run");
			else:
				_changeState("st_walk");
		else:
			_changeState("st_idle");

#		#----//--charge--//---------------
#		if ( keyATTACK2 ):
#			_stopHor();
#			_changeState("st_charge");
#		elif ( keySpecial ):
#			blockEnable = true;
#			_stopHor();
#			_changeState("st_block");
#		else:
#			blockEnable = false;
#
#	if ( is_on_floor() || is_on_wall() ):
#		if ( keyJUMP ):
#			_changeState("st_jumpStart");
#			_snd_play(Global.snd_jump1);

Next in the player.gd set the func ready() and the func process() We will be using only the player normal keys... The combination specials ( aduken/oryouken ) will not be in use right now... And some vars in the ready function need to stay on comment for now.

func _ready():
	health = 200;
#	grabForce = 100;
	walkSP = 1.7;
	JUMP_HEIGHT = 5.7;
	RUN_JUMP_HEIGHT = 4.5;
	anim = $playerAnim;
#	animOffSet_X = anim.translation.x;
#	animOffSet_Y = anim.translation.y;
#	animOffSet_Z = anim.translation.z;

func _process(delta: float) -> void:
	
	#-------//--player-keys--//----------
	keyUP = int ( Input.is_action_pressed("ui_up") );
	keyDOWN = int ( Input.is_action_pressed("ui_down") );
	keyRIGHT = int ( Input.is_action_pressed("ui_right") );
	keyLEFT = int ( Input.is_action_pressed("ui_left") );

	keyJUMP = int ( Input.is_action_just_pressed("ui_X") );
	keySpecial = int ( Input.is_action_pressed("ui_C") );

	keyATTACK1 = int ( Input.is_action_just_pressed("ui_Z") );
	keyATTACK1Hold = int ( Input.is_action_pressed("ui_Z") );
	keyATTACK2 = int ( Input.is_action_pressed("ui_A") );
	keyATTACK3 = int ( Input.is_action_just_pressed("ui_S") );
	keyATTACK4 = int ( Input.is_action_just_pressed("ui_D") );

#------------//-------------//---------------------- Next is the physics_process in player.gd Set the pauseTimer, animationSpeed and movement ( motion ), Other functions like grabBoxZero() , _comboTime() , etc... wont be in use right now...

    func _physics_process(delta: float) -> void:
    
    #------//--attack-hit-pause--(exits-code)--//----------
    	if ( pauseTimer >= 0 ):
    		pauseTimer -= 0.05;
    		pauseTimer = clamp(pauseTimer, 0, 100);
    		if  ( pauseTimer != 0 ):
    			return;
    	
    	_animationSpeed(anim, delta);
    	
    	anim.frame = delay_frame;
    	
    	motion.y += gravity * delta;
    	motion = move_and_slide(motion, Vector3.UP, true, 4);

Next set the player sates "idle" and "walk" Most variables and functions will be commented right now We will only be focusing on frameDelay , anim.play , move() , and changeState()

	match state:
		"st_idle":
			runSP = 0;
#			keyComboEnable = true;
#			_resetVars();
#			_bodyBox1( 0, 0.45, 0.8, 2.2, 0.3 );
#			_grabBox1( 0, 0.06, 0.5, 0.5, 0.3 );
#			_grabbedBox1( 0, 0.06, 0.4, 0.4, 0.2 );
#			_attackBoxZero();
#			_attackDropZERO();
			_move();
#			_attackChain();
#			_specialAttackArea("st_specialArea");

			anim.play("idle");

			if ( anim.frame == 0 ):
				frameDelay = 25;
			elif ( anim.frame == 1 ):
				frameDelay = 8;
			elif ( anim.frame == 7 ):
				delay_frame = 1;

#			if ( ! is_on_floor()  && ! is_on_wall()):
#				_changeState("st_walkOff");
		
		"st_walk":
			runSP = 0;
#			keyComboEnable = true;
#			_attackBoxZero();
			_move();
#			_attackChain();
#			_specialAttackArea("st_specialArea");
			
			frameDelay = 12.5;
			anim.play("walk");
			
#			if ( ! is_on_floor()  && ! is_on_wall()):
#				_changeState("st_walkOff");

the player still wont be able to run because it uses the keyCombinations FOWARD FOWARD / BACK FOWARD / BACK BACK.. the combination works with a string based on were the player is facing. example: If he is facing right and the user presses key left the Combinations will check BACK. example: If he is facing right and the user presses key right the Combinations will check FOWARD.

For now the player is just moving and switching between the "idle" and "walk" sate

dont forget to click on the images and zoom.

4 days later

@jonSS said: I could upload it, but i dont think it can be made using KOF sprites. [...]

I do not think anyone would just use those assets in anything they released. As you said, they work well with what you are trying to teach. If you obtained them from an archive somewhere, perhaps providing a link to that location on your first BeatEmUp video on YouTube (if not allowed here), or are they one of the sets available from sites like Sprite Database or Spriter's Resource, etc.? (This presumes you do not go the Google Drive approach, of course.)

Right now godot 4 alpha keeps crashing to the desktop...

I had a problem with a desktop debug build of Godot 4 - one of the Vulkan pipeline functions crashed with a null pointer dereference after being called a few times. I used the nVidia Vulkan Beta Driver v473.11 instead of the General Release one and that problem went away. Maybe it will help you, too?

Either way, thanks much for what you have provided so far!

Peace!

To save time, I created PNG sprite sheets with transparency from the various different graphics including Ryo and StreetGuy1. The order of the sprites and the sizing is not always intuitive, but everything should work. The naming convention is SS<name><TileWidth>x<TileHeight><GridWidth>x<GridHeight>.png.

For example, SSStreetGuy1_180x180_4x7.png is the sprite sheet for StreetGuy1, and the individual tile sizes are 180x180 pixels, and there are 4 images across (X) and 7 images down (Y) in the sprite sheet. In order to make the tiling script work, I needed to add blank tiles to some of the image sets.

Here is an example of the 1Robert sprite sheet:

The files will be located here and I will update the folder with additional content as I get around to it or if anyone needs a specific sprite sheet done, please just ask and I will see what I can do.

Peace!

@Semaj31273 said: To save time, I created PNG sprite sheets with transparency from the various different graphics including Ryo and StreetGuy1. The order of the sprites and the sizing is not always intuitive, but everything should work. The naming convention is SS<name><TileWidth>x<TileHeight><GridWidth>x<GridHeight>.png.

lol, i dont know if its just good idea, the player sprites for example take about 200 or 300 images... You could be waisting time on making idividual sprite sheets...

The process is very strait foward... There are just a few things to keep in mind, to maybe keep the confusion to a minimum....

the hitBox checks all enemies / pains all enemies... When the player is grabbing the enemie he only hits(pains) one enemie... var areaOne = areaAll[0]; #----( the 1st enemie in the enemies list array [0] ).

	attacker.myOpponent = areaOne.get_owner();
	
	print(attacker.myOpponent);

3d_enemy1:[KinematicBody:2058] 3d_enemy1:[KinematicBody:2058] --- Debugging process stopped ---

[KinematicBody:2058] - this is the name of the current enemy its hiting ---> print(attacker.myOpponent);

Next in the grabBox.tscn this area looks for the grabbedBox area ( basaclly it does the same than hitBox area does to the hurtBox area )

It puts the entity ( enemie[0] ) -> [KinematicBody:2058] has the attacker opponent, so the attacker can manage only this entity the way it wants

Ive made a few extra vars that might add confusion to the code...

	attacker.myOpponent = opponent;
	attacker.myGrabOpponent = opponent;
	attacker.the_Obj_Iam_Grabbing = opponent;
	
	opponent.myOpponent = opponent;
	#opponent.myGrabOpponent = attacker;
	opponent.the_Obj_Grabbing_Me = attacker;

i was doing this for future things in case anything would go wrong the_Obj_Grabbing_Me - > this is just so that the enemy knows who is grabbing him

and in the player GRAB this is basically the same thing attacker.myOpponent = opponent; attacker.myGrabOpponent = opponent; attacker.the_Obj_Iam_Grabbing = opponent;

the player GRAB state manadges this entity the_Obj_Iam_Grabbing -> is the [KinematicBody:2058]

The player Slam state ( he grabs the enemie and trows him )

the function bindGrabEntity( entity, state, x, y, z ): in the player grabState ---> OpponentSlam = _entity;

_bindGrabEntity makes the enemy (x, y, z) the same has the player ( x, y, z )

In the player slam state

the player transforms the -> enemy Animation NODE position (x, y, z, angle ) in the function --> _slamPos( 1, -0.02, 0, -0.01, 1, 1, 0 );

it also transforms its own animation NODE in --> _animTransform( anim, -0.6, 0.55, 1, 1, 90); to take some advantage of godot flexibility, and give more costumization to the slams, withOut having edit the sprite ( rotate the image sprite and save to png for example )

Just a few things to keep in mind... Theres already some confusion added to the code mostly with vars: myOpponent myGrabOpponent the_Obj_Iam_Grabbing

You can see other examples of confusion i made... OpponentSlam --> gets its value in the "grab state" it should be the entity has decribed above [KinematicBody:2058] In the player "slam state" the function _bindGrabEntity... its using the var OpponentSlam the main problem here... is that... ryo slam has a (roll) and then a (throw) this --> roll in the slam animation attack's the enemies has it goes by... has it attacks [KinematicBody:2058] becomes the entity its being attacked...and not the entity its being grabbed... its the case for the diferent extra var (OpponentSlam)

But it should very strait foward, its an easy process the code just gets bigger and looks confusing, has more stuff ( attacks, states, slams, grabAttacks ) are added...

One simple thing to keep in mind...

for playing sounds / projectiles / dusts...effects use for example if anim.frame== 3 && nextFrame == true

this way the projectile / sound will only play / spawn ONCE Godot original animation system has signals for animationFinished() and frameChange().... but for it might keep the code scatered and more dificult to read... has it puts the code in a diferent function... This is way is more strait foward ----> if (anim.frame== 3 && nextFrame == true ):

The learning process is very strait foward It should be very simple just by doing this tutorial for example... It explains very well hitBoxes

there are also other things i didnt mentioned, like alpha values ( flash entity ) Z position... the entity.tcsn scaleY ( transform Y ) its 1.4 once it gets put on level, so it doesnt look wide... etc... but if i mention everything this post becomes an encyclopedia

Ive uploaded the game here: by making a tutorial 1st you should understand better how the code works

download link: --- ( also added the link to the 1st page ) --- godot version 3.4 https://drive.google.com/file/d/1rixJJKgw3MXySc3PV4Tt1IrprjayNZhw/view?usp=sharing

a month later

Thanks much for adding the code. Good to see others' stuff sometime to see if you can learn anything from it or maybe help them out.

Peace!

7 months later

If you don't mind me asking which part of the code control the AI Idle movement? Like example if i wanted to have the enemy AI just stand in place until the player get close which part of in the GD script that controls?