My First 2D Platformer game

Hello everyone!
I’m new to the platform and excited to connect with this amazing community to learn and grow alongside all of you.

I’m currently developing a 2D platformer game, and I’ve made some progress so far. Here’s a quick overview of what I’ve implemented:

Players features :

  • Horizontal movement,
  • Jump,
  • Attack,
  • Wall slide,
  • Wall jump,
  • Pogo jump,
  • Respawn
  • Basic animations (idle, run, jump, attack, wall slide, fall)

enemy functions :

  • Patrolling along specified vector points (currently hardcoded),
  • Chasing when the player enters a detection area
  • Animations for walking, getting hurt, and death

Next Steps :
I plan to work on level design, aiming to create an open-world layout. The game is still very much in development, and I’d love to hear your thoughts or suggestions for improvement.

Feel free to point out any areas where I can enhance the game or approach things differently. Looking forward to your feedback!

9 Likes

Hello again, everyone!

I’m excited to share an update on my 2D platformer game after my previous post. The game is starting to take shape, though there’s still a lot to work on before it’s fully complete. I’m eager to hear feedback from the community!

What’s New?

Player Features:

  • Horizontal Movement, Jump, Attack, Wall Slide, Wall Jump, Respawn (as shared before)
  • Health System: The player now has a health bar (functional prototype is ready, proper textures not added yet)
  • Improved Controls: I’ve implemented on-screen D-pad controls for mobile play, with buttons for jump and attack. (functional prototype is ready, proper textures not added yet)
  • Camera Improvements: The camera now better frames the action, preventing it from moving into non-background areas, making the gameplay visually consistent.
  • Player Respawn Mechanic: If the player falls off the map or into a pit, he’ll respawn at a specific point.

Enemies:

  • Patrol and Chase: Enemies still patrol along set points and chase the player, with some tweaks to improve behavior.
  • Enemy Variants: I’ve added different enemy types (e.g., snakes, vultures, and hyenas)
  • Knockback Logic: Enemies react when hit by the player(still need some improvement)

World & Level Design:

  • Open-World Layout: I’m in the process of designing large areas, where players need specific abilities to access new zones, making exploration an essential part of the game.
  • Level Generation: I’ve created a system to generate levels based on symbols in a string array. This includes placing platforms, obstacles, and enemies based on predefined patterns.
  • UI elements: Added menus, background, and other UI elements.

Next Steps:
I’ll be focusing on refining level design, improving enemy AI, and adding more content like puzzles and new abilities for the player. I’m also planning to expand the visual assets and work on the sounds to create a more immersive experience.

As always, any feedback, suggestions, or tips would be greatly appreciated! Looking forward to hearing your thoughts.



I’ll be uploading the current project to itch.io soon, so stay tuned! Feel free to provide feedback and any suggestions for improvement.

NOTE: All visual assets and art belong to their respective owners.

Regards.

4 Likes

That’s an impressive feature set, I look forward to trying it on itch. It’s hard to give feedback until then, but I’ll be happy to once I’ve played it.

And a friendly reminder:

Blockquote
NOTE: All visual assets and art belong to their respective owners.

This is fine and all, but it’d be nice to credit these owners specifically. I’m assuming you do have permission to use their work (if not you should either switch it out for something you do have permission to use, or seek permission from these owners ASAP).

Nice work and good luck!

2 Likes

Thank you for your reply and consideration.

I plan to include the names of the asset creators and links to their work in the project description when I upload the game to itch.io. I’ll also double-check permissions before uploading the project on social platforms. I haven’t created an in-game credits section just yet, but I’m wondering if that’s required at this stage or if it can wait until the project is further along in development.

Currently, I’m focused on learning how to export the project for web and checking if there are any other things I need to take care of before uploading.

This will take few days to upload my project on itch.io

1 Like

The Prototype is Now Playable on Itch.io – Your Feedback is Welcome!

Hey everyone!

I’m excited to announce that I’ve uploaded the first playable prototype of my 2D platformer game on Itch.io! After sharing my initial progress here a while back, I’ve been working hard to improve the game, and now you can try it out directly in your browser.

You can check it out here : Knight Platformer by koliashwin

What’s Included in This Prototype?

  • Core Player Features: Movement mechanics like running, jumping, wall sliding, wall jumping, attacking, dashing, and respawning. The player also has a basic health system in place.
  • Basic Enemy AI: Enemies patrol and chase the player, with knockback reactions when hit. The current build includes a few different enemy types (vultures and hyenas) to challenge the player.
  • Level Design: The game is at an early stage of an open-world approach, where you can explore area, face obstacles, and defeat enemies. Future updates will feature an ability unlock system to access new areas.
  • UI Elements: Menus, background UI, and on-screen controls for mobile play (currently in the testing phase) are implemented.

What I Need Your Help With
This is still very much a work in progress, and I’d love to get your thoughts and feedback on what’s working, what feels off, and any suggestions you may have. I’m especially interested in your thoughts on:

  • The controls
  • The feel of the gameplay
  • Suggestions for improvements or new features

What’s Coming Next?
In the next phase, I’ll be focusing on:

  • Adding sound effects, background music.

Feel free to try out the game and share your feedback either on the Itch.io page or here in the forum. Your input will help shape the game’s future development!

Thanks again for all the support so far.

Hi there! Love the game and I’m super impressed by what you’ve got so far. I’ll address your questions as best I can.

The controls are smooth with the exception of walljumping endlessly upwards and the sluggish attack speed of the sword.

The gameplay felt decent. Not what I’d expect from a man in an iron suit with the doublejumps and such, but the speed was nice to kite the enemies around, although it might’ve made things too easy?

My suggestions probably dont mean much, but I’d slow the gameplay down a bit. It’d give it a more unique style rather than “It’s Sonic/Mario, but in armor”. Give the enemies some wind-up attack animations, maybe a new action to block instead of the dashing?

Overall, its got good bones. Just need to add some more meat. I’m sure whichever direction you take it, it’ll turn into something really cool.

1 Like

Thank you so much for playing the game and for the kind words—it means a lot to hear that you enjoyed it so far!

Regarding the sluggishness in the attacks, the player is programmed to perform upper and lower attacks for pogo jumps, but I haven’t found the right character assets yet to fully match all the abilities I’m envisioning. I plan to shorten the attack animations and introduce a cooldown period between attacks to balance things out.

I’ll also be considering your suggestion about slowing down the gameplay and will test it to see how it feels.

These tweaks and adjustments fall into my improvement category. Right now, I’m focused on finishing the core components of the game, like the mechanics (with a few tweaks needed) and basic level design, which is mostly done. Next, I’ll be working on collectibles, NPC interactions, and sound. Once all the foundational elements are in place, I’ll revisit these tweaks and start expanding the levels.

Please note that some character designs, including the player and enemies, may change in future prototypes.

I really appreciate the detailed feedback. Your suggestions mean a lot, and I’ll definitely be considering them as I continue refining the game. It’s still early days, but feedback like yours is helping shape the direction I want to go. Thanks again for the support, and I hope you’ll check out future updates as the game continues to evolve!

Oh, and also for background music, you can implement this by adding multiple AudioStreamPlayer nodes (not AudioStreamPlayer2D nodes) for each specific scene you want to be in throughout the game, and then drag your desired music over to the AudioStreamPlayer node, set it to “Autoplay”, and make a new Audio Bus titled “Music”, and change the volume for the overall music.

Oh, and for each object the player can interact with, you can make AudioStreamPlayer nodes that play once the player interacts with them. You really don’t have to do any code for any of this. It’s really that easy! Wish the best of luck on your game!

1 Like

Really Nice ! Try to make the Movement better.. So far so good

1 Like

This was kind of like my practice project. at that time I had run into major wall and that is I wasn’t able to find assets as per my requirement. so I scrape this idea and started learning about creating assets on my own. so far I can create small frame by frame animations, characters and some tiles and properties. I still need to learn how work with multiple animation at same time, its honestly like more you know something the more it feel like you don’t know lot for things about it. its specially true with game development. it’s really hard to keep the scope down and save yourself from burning out in the process. now I’m working on another project inspired from this and my other jam projects. you’ve already commented on that one and know that I appreciate it very much.

NOTE FOR EVERYONE : This project has been closed. I’m no longer working on it. I’m working on a Metroid Vania game with hand-drawn art you can find updates related to it in this link
since I’m working solo and wish to try out things on my own this project may take lot of time and I intent to complete it no matter how long it takes.

thank you

2 Likes

here is the basic flow for knock back system. I’ll share the whole script as well, it’ll be a bit messy so here is the flow for your understanding. (also know that this may not be the efficient but this worked for me.)

knock back for enemies:
I have like 3 files or references for the knock back.

  1. enemy_base.gd : this will be common file for enemies will have some common logic for things like enemy health, death, sprite flip and some common behaveiors like knockback etc.
    about the knock back logic, I’ve defined some variables related to knock back.
# knockback related variables
var is_knocked_back: bool = false
var knockback_duration: float = 0.2
var knockback_timer: float = 0.0
var knockback_force: Vector2 = Vector2(200, 0)

after this whenever your want to apply the knockback you set an is_knocked_back flag to true and set the knockback_duration (this define how much longer force should apply), get the knockback direction and add velocity in opposite direction (you’ll need the get player position for this. im calling damage function form the player script via area2d_body_entered signal and passing the player position in it)

after done setting the values now we need a logic to change the is_knocked_back flage again. that logic is there in second function. (you can experiment it by removing the timer completely and apply force one time only.)

# common function for all enemies
func apply_knockback(hit_from: Vector2) -> void:
	is_knocked_back = true
	knockback_timer = knockback_duration
	var knockback_dir = (global_position - hit_from).normalized()
	velocity = knockback_dir * knockback_force

# common function for all enemies
func knockback_logic(delta: float) -> void:
	knockback_timer -= delta
	if knockback_timer <= 0:
		is_knocked_back = false
	move_and_slide()

func apply_damage(hit_from: Vector2, damage: int) -> void:
	# push the enemy backwards
	apply_knockback(hit_from)
	# reduce the health
	enemy_health -= damage
	# update the visuals (use teh blink shader)
	var tween = self.create_tween()
	tween.tween_method(setShader_BlinkIntensity, 1.0, 0.0, 0.5)
	# if health reaches 0 remove the enemy
	death()

this was the main logic for knockback, now we just need to club this together and take the necessary input form other nodes.

  1. enemy_script : I have multiple enemy scripts which are inheriting the common behavior from enemy_base script. (depending on your scope and requirement you can skip this step and have enemy behaviour in one file only).
    in this file im actively checking if enemy is in knockback state (is_knocked_back flag) if so then call the knockback_logic funcion
func _physics_process(delta: float) -> void:
	if is_knocked_back:
		knockback_logic(delta)
		return

    # rest of the code
  1. player.gd: I wanted to apply the knock back when player will hit the target and in direction opposit to the player so we need player position for that im calling the damage enemy function via area2d_body_entered signal and passing player position and attack power into it. global position will be used for knockback logic and attack power will be used for reducing the enemy health.
func _on_attack_area_body_entered(body: Node2D) -> void:
	print(body.name , " is attacked")
	if body.is_in_group('enemies'):
		if Input.is_action_pressed("look_down"):
			pogo_jump()
		body.apply_damage(global_position, ATTACK_POWER)

below is the whole script for your reference (I tried uploading the zip file but it did not work so sharing he it like this)

enemy_base.gd :

extends CharacterBody2D

# common variables
var speed: int = 200
var dir: int = 1
var enemy_health: int = 10

# patrol related variables
var patrol_timer: float = 0.0
var patrol_start_pos: Vector2
var patrol_end_pos: Vector2
var delay_at_end: float = 0.5
var move_direction: Vector2

# knockback related variables
var is_knocked_back: bool = false
var knockback_duration: float = 0.2
var knockback_timer: float = 0.0
var knockback_force: Vector2 = Vector2(200, 0)

# attack related variables
var can_attack: bool = true

# reference variables
var enemy_sprite: Sprite2D
var enemy_animation: AnimationPlayer
var detection_range: Area2D
var damage_area: Area2D
var player: CharacterBody2D
var projectile: PackedScene
var attack_cooldown: Timer


func patrol(delta: float) -> void:
	# stop for a moment when reached at end of patrol point
	if patrol_timer > 0:
		patrol_timer -= delta
		velocity.x = 0
		return
	
	# switch target pos based on dir
	var target_pos: Vector2 = patrol_end_pos if dir == 1 else patrol_start_pos
	
	# switch the dir and start the delay timer when reached at one of the end points
	if global_position.distance_to(target_pos) < 5:
		dir *= -1
		patrol_timer = delay_at_end
	
	move_direction = (target_pos - global_position).normalized()
	# below is the move behaviour(for now it just straight line movment)
	patrol_pattern()

# the following function can be overridden for differnt patroll behaviours
func patrol_pattern() -> void:
	velocity.x = speed * move_direction.x

func chase(player_pos: Vector2, speed_multiple: float = 2) -> void:
	# get the player direction and execute the chase behaviour
	var player_direction = (player_pos - global_position).normalized()
	# below is the chase behaviur(for now it just straight line movment)
	velocity.x = player_direction.x * speed * speed_multiple

func shoot_projectile(projectile_type: int = 1) -> void:
	# set can_shoot flag to false so that enemy will not be able to spam projectiles
	can_attack = false
	# create the instance of the projectile and add it to the parent scene
	var projectile_instance = projectile.instantiate()
	projectile_instance.global_position = position
	
	# below is the projectile set up specific for projectile type. logic is in the projectile script
	# projectile type : {1: straight line, 2: tracking player}
	projectile_instance.type = projectile_type
	# following line is for straight projectile
	projectile_instance.dir = Vector2(dir,0)
	# followwin line is for traking player
	projectile_instance.target_body = player
	get_parent().add_child(projectile_instance)
	
	# start the cooldown timer, when the timer is up enemy can shoot again
	attack_cooldown.start()

# common function for all enemies
func apply_damage(hit_from: Vector2, damage: int) -> void:
	# push the enemy backwards
	apply_knockback(hit_from)
	# reduce the health
	enemy_health -= damage
	# update the visuals (use teh blink shader)
	var tween = self.create_tween()
	tween.tween_method(setShader_BlinkIntensity, 1.0, 0.0, 0.5)
	# if health reaches 0 remove the enemy
	death()

# common function for all enemies
func apply_knockback(hit_from: Vector2) -> void:
	is_knocked_back = true
	knockback_timer = knockback_duration
	var knockback_dir = (global_position - hit_from).normalized()
	velocity = knockback_dir * knockback_force

# common function for all enemies
func knockback_logic(delta: float) -> void:
	knockback_timer -= delta
	if knockback_timer <= 0:
		is_knocked_back = false
	move_and_slide()

# common function for all 
func death() -> void:
	# this is just a place holeder func (update this when animations are added)
	if enemy_health <= 0:
		queue_free()

# common function for all enemies
func setShader_BlinkIntensity(newValue: float) -> void:
	enemy_sprite.material.set_shader_parameter("blink_intensity", newValue)

# common function for all enemies
func flip_enemy() -> void:
	if velocity.x != 0:
		enemy_sprite.flip_h = velocity.x < 0 
		
		# remove the below code if enemies do not have the detection_range as area2d node
		if detection_range:
			if velocity.x > 0:
				detection_range.scale.x = 1
			elif velocity.x < 0:
				detection_range.scale.x = -1

#----------------------------------SIGNALS---------------------------------------------
# common signal for all the enemies
# player gets hurt when touched by enemy
func _on_damage_area_body_entered(body: Node2D) -> void:
	if body.is_in_group('players'):
		body.take_damage(global_position)

# common signal for chase logic
func _on_detection_range_body_entered(body: Node2D) -> void:
	if body.is_in_group('players'):
		player = body

# common signal for chase logic
func _on_detection_range_body_exited(body: Node2D) -> void:
	if body.is_in_group('players'):
		player = null

func _on_attack_cooldown_timeout() -> void:
	can_attack = true

petrolling_enemy.gd :

extends "res://scripts/enemies/enemy_base.gd"

@export var PATROL_DIST: int = 128

func _ready() -> void:
	# add enemy to the group and setup nessery nodes
	add_to_group('enemies')
	enemy_sprite = $Sprite2D
	damage_area = $damage_area
	
	# calculate and store patrol points
	patrol_start_pos = Vector2(global_position.x - PATROL_DIST, global_position.y)
	patrol_end_pos = Vector2(global_position.x + PATROL_DIST, global_position.y)
	
	# connet the signals to the base class methoths(can use the custom signals too)
	damage_area.body_entered.connect(_on_damage_area_body_entered)

func _physics_process(delta: float) -> void:
	if is_knocked_back:
		knockback_logic(delta)
		return
	
	patrol(delta)
	
	flip_enemy()
	move_and_slide()

Player.gd :

extends CharacterBody2D

# define some constants params (i'll be using var for development purpose)
@export var SPEED: int = 300
@export var ACCELERATION: int = 900
@export var FRICTION: int = 1800
@export var GRAVITY: int = 980
@export var ATTACK_POWER: int = 2

# jump params
@export var JUMP_FORCE: int = 800
@export var coyote_time: float = 0.1
@export var jump_buffer_time: float = 0.1
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
var can_double_jump: bool = true

# dash params
@export var DASH_SPEED: int = 900
@export var DASH_DURATION: float = 0.2
@export var DASH_COOLDOWN: float = 1.0
var is_dashing: bool = false
var can_dash: bool = true
var dash_timer: float = 0.0
var dash_cooldown_timer: float = 0.0
var dash_direcction: int = 1

#knockback params
@export var KNOCKBACK_FORCE: Vector2 = Vector2(0,-400)
@export var INVINCIBILITY_DURATION: float = 1.0
var is_invincible: bool = false

#attack params
@export var ATTACK_COOLDOWN: float = 0.3
var is_attacking: bool = false
var can_attack: bool = true

#player states
enum State {IDLE, RUN, JUMP, FALL}
var curr_state = State.IDLE

#nodre references
var player_sprite: Sprite2D
var player_animation: AnimationPlayer
var attack_area: Area2D
var weapon_sprite: AnimatedSprite2D

func _ready() -> void:
	add_to_group('players')
	
	player_sprite = $Sprite2D
	player_animation = $AnimationPlayer
	attack_area = $attack_area
	weapon_sprite = $attack_area/AnimatedSprite2D
	
	weapon_sprite.visible = false

func _physics_process(delta: float) -> void:
	var direction: int = Input.get_axis("move_left", "move_right")
	horizontal_movment(direction, delta)
	jump(delta)
	dash(delta)
	attack()
	apply_gravity(delta)
	
	handle_flip()
	handle_state_logic()
	move_and_slide()

func horizontal_movment(direction: int, delta: float) -> void:
	# horizontal movment with firction and acceleration
	if direction:
		velocity.x = move_toward(velocity.x, SPEED * direction, ACCELERATION * delta)
	else:
		velocity.x = move_toward(velocity.x, 0, FRICTION * delta)

func apply_gravity(delta: float) -> void:
	# apply gravity
	if !is_on_floor():
		var fall_multiplyer: float = 2 if velocity.y > 0 else 1
		#velocity.y += GRAVITY * delta * fall_multiplyer
		velocity.y = move_toward(velocity.y, GRAVITY , 1500 * delta * fall_multiplyer)
		coyote_timer -= delta			#gradually decrease coyote timer when not on floor
		#velocity.y += clamp(velocity.y, 0, 1500)
	else:
		coyote_timer = coyote_time		#reset coyote timer when on the floor
		can_double_jump = true

func jump(delta: float) -> void:
	if Input.is_action_just_pressed("jump"):
		jump_buffer_timer = jump_buffer_time		#on jump input start jump buffer time to record the jump for fraction of second
		
		#double jump logic
		if can_double_jump and !is_on_floor():						#while in air check if is able to jump again
			#velocity.y = 0
			velocity.y = -JUMP_FORCE			#if is able to jump, change the y height
			if coyote_timer < 0 :				#make sure to check coyote timer first before deactivating double jump
				can_double_jump = false
	else:
		jump_buffer_timer -= delta
	
	# jump buffer logic (incase player tries to jump just before touching or leaving the floor)
	if jump_buffer_timer > 0 and coyote_timer > 0:
		velocity.y = -JUMP_FORCE
		jump_buffer_timer = 0.0
		coyote_timer = 0.0
	
	# variable jump height logic
	if Input.is_action_just_released("jump") and velocity.y < 0:
		velocity.y /= 5			#the value 5 is just for experimental purpose

func dash(delta: float) -> void:
	if Input.is_action_just_pressed("dash") and can_dash and !is_dashing:
		#setup some dahs flags and variables
		is_dashing = true
		can_dash = false
		dash_timer = DASH_DURATION
		dash_cooldown_timer = DASH_COOLDOWN
		dash_direcction = -1 if player_sprite.flip_h else 1		# set dash direction based on flip_h property of sprite
		velocity.x = DASH_SPEED * dash_direcction
		#here (need to add) the code to change the collision layers and mask
	
	if !can_dash:
		dash_cooldown_timer -= delta
		if dash_cooldown_timer <= 0:
			can_dash = true
	
	if is_dashing:
		velocity.y *= 0.1
		dash_timer -= delta
		if dash_timer < 0:
			#velocity.x = 0
			is_dashing = false

func attack() -> void:
	if Input.is_action_just_pressed("attack") and can_attack:
		#attack flags
		weapon_sprite.visible = true
		weapon_sprite.play('slash')
		is_attacking = true
		can_attack = false
		
		attack_area.set_deferred('monitoring', true)
		
		#playe animation here
		#below code will go into attack animation finish func
		await get_tree().create_timer(ATTACK_COOLDOWN).timeout
		can_attack = true
		is_attacking = false
		attack_area.set_deferred('monitoring', false)
		weapon_sprite.visible = false

func take_damage(hit_source: Vector2) -> void:
	if is_invincible:
		# skip if already invincible
		return
	
	# apply knockback in opp direction
	var knockback_direction = sign(global_position.x - hit_source.x)
	velocity.x = knockback_direction * KNOCKBACK_FORCE.x
	velocity.y = KNOCKBACK_FORCE.y
	
	# start invincibility state
	is_invincible = true
	start_blinking()
	await get_tree().create_timer(INVINCIBILITY_DURATION).timeout
	is_invincible = false
	player_sprite.modulate.a = 1

func start_blinking() -> void:
	var blink_timer = INVINCIBILITY_DURATION/10
	for i in range(5):
		player_sprite.modulate.a = 0.5 if i % 2 == 0 else 0.8
		await get_tree().create_timer(blink_timer).timeout

func handle_flip() -> void:
	var flip_factor: int = 1
	if velocity.x != 0:
		player_sprite.flip_h = velocity.x < 0
	
	if player_sprite.flip_h:
		attack_area.scale.x = -1
		flip_factor = 1
	else:
		attack_area.scale.x = 1
		flip_factor = -1
	
	if Input.is_action_pressed("look_up"):
		attack_area.rotation_degrees = 90 * flip_factor
	elif Input.is_action_pressed("look_down"):
		attack_area.rotation_degrees = -90 * flip_factor
	else:
		attack_area.rotation_degrees = 0

func pogo_jump(pogo_multiple: float = 1) -> void:
	print("pogo activated")
	#velocity.y = 0
	velocity.y = -JUMP_FORCE * pogo_multiple
	can_double_jump = true

func change_state(new_state: State) -> void:
	if curr_state == new_state:
		return
	curr_state = new_state
	update_animations(curr_state)

func update_animations(state: State) -> void:
	var anim_name = ''
	
	match state:
		State.IDLE: 
			anim_name = 'dash'
		State.RUN:
			anim_name = 'run'
		State.FALL:
			anim_name = 'fall'
		State.JUMP:
			anim_name = 'jump'
	
	if player_animation.name != anim_name:
		player_animation.play(anim_name)

func handle_state_logic() -> void:
	if is_on_floor():
		if velocity.x != 0:
			change_state(State.RUN)
		else:
			change_state(State.IDLE)
	else:
		if velocity.y > 0:
			change_state(State.FALL)
		else:
			change_state(State.JUMP)

func _on_attack_area_body_entered(body: Node2D) -> void:
	print(body.name , " is attacked")
	if body.is_in_group('enemies'):
		if Input.is_action_pressed("look_down"):
			pogo_jump()
		body.apply_damage(global_position, ATTACK_POWER)

Hope this helps. also about the health. I went through your devlog and it appeared like you may have already implemented it. so i haven’t added that code. if you required that too feel free to ask (to be honest my own health system was just the dummy so it may not be of that much help)

2 Likes