Skip to content

Commit

Permalink
Stable animation system
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtemkaKun authored Aug 21, 2023
2 parents d2bdfd2 + 2b1c82e commit 3b5cd8a
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 39 deletions.
16 changes: 11 additions & 5 deletions src/chicken/systems.v
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import gg
// They were adjusted manually to achieve the desired jump behavior.
// Don't change them unless you know what you are doing.
const (
jump_velocity_x = 0.45
jump_velocity_y = -1
jump_velocity_x = 0.6
jump_velocity_y = -1.25
)

// spawn_chicken creates a new chicken entity and adds it to the world.
pub fn spawn_chicken(mut ecs_world ecs.World, screen_size gg.Size, chicken_idle_image gg.Image, image_scale int, time_step_seconds f64) !&ecs.Entity {
polygon_convex_parts := common.load_polygon_and_get_convex_parts(chicken_idle_image.path,
pub fn spawn_chicken(mut ecs_world ecs.World, screen_size gg.Size, chicken_animation_frames []gg.Image, image_scale int, time_step_seconds f64) !&ecs.Entity {
polygon_convex_parts := common.load_polygon_and_get_convex_parts(chicken_animation_frames[0].path,
image_scale)!

polygon_width := collision.calculate_polygon_collider_width(polygon_convex_parts)
Expand All @@ -27,9 +27,15 @@ pub fn spawn_chicken(mut ecs_world ecs.World, screen_size gg.Size, chicken_idle_
y: screen_size.height / 2
},
ecs.RenderData{
image_id: chicken_idle_image.id
image_id: chicken_animation_frames[0].id
orientation: common.Orientation.right
},
ecs.Animation{
frames: chicken_animation_frames
time_between_frames_ms: 77
current_frame_id: 0
next_frame_id: 1
},
GravityInfluence{
force: 2 * time_step_seconds
},
Expand Down
13 changes: 12 additions & 1 deletion src/ecs/common_components.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module ecs

import artemkakun.trnsfrm2d as transform
import common
import gg

// Position component represents the spatial location of an entity within the game world.
// It embeds the `Position` struct from the `transform` module.
Expand All @@ -25,6 +26,16 @@ pub:
orientation common.Orientation
}

pub struct Animation {
pub mut:
frames []gg.Image
time_between_frames_ms int
is_playing bool
time_left_to_next_frame_ms int
current_frame_id int
next_frame_id int
}

// HACK: This function is a workaround to a limitation in V's interface implementation.
// In V, a struct automatically implements an interface if it satisfies all of the interface's methods and fields.
// However, for our empty interface for components, no struct can satisfy it as there are no methods or fields to implement.
Expand All @@ -34,5 +45,5 @@ pub:
// The function uses an array to accommodate multiple components, thereby preventing code duplication.
// This hack should be removed when interface for component will have methods or fields.
fn component_interface_hack() []Component {
return [Position{}, Velocity{}, RenderData{}]
return [Position{}, Velocity{}, RenderData{}, Animation{}]
}
29 changes: 29 additions & 0 deletions src/ecs/common_systems.v
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,32 @@ pub fn movement_system(velocity_component &Velocity, mut position_component Posi
y: position_component.y + velocity_component.y
}
}

pub fn animation_system(mut animation_component Animation, mut render_data_component RenderData, delta_time_milliseconds int) {
if animation_component.is_playing {
if animation_component.time_left_to_next_frame_ms > 0 {
animation_component.time_left_to_next_frame_ms -= delta_time_milliseconds
} else {
if animation_component.next_frame_id == -1 {
animation_component.is_playing = false
animation_component.current_frame_id = 0
animation_component.next_frame_id = 1
} else {
next_frame_id := if animation_component.next_frame_id + 1 >= animation_component.frames.len {
-1
} else {
animation_component.next_frame_id + 1
}

animation_component.current_frame_id = animation_component.next_frame_id
animation_component.next_frame_id = next_frame_id
animation_component.time_left_to_next_frame_ms = animation_component.time_between_frames_ms
}
}

render_data_component = &RenderData{
...render_data_component
image_id: animation_component.frames[animation_component.current_frame_id].id
}
}
}
11 changes: 9 additions & 2 deletions src/egg/egg_spawn.v
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import ecs
import common
import artemkakun.trnsfrm2d
import collision
import gg

// spawn_egg adds a new egg entity into the ECS world
pub fn spawn_egg(mut ecs_world ecs.World, egg_x_position int, egg_image_height int, egg_image_id int, obstacle_move_vector trnsfrm2d.Vector, polygon_convex_parts [][]trnsfrm2d.Position, polygon_width f64, polygon_height f64) ! {
pub fn spawn_egg(mut ecs_world ecs.World, egg_x_position int, egg_image_height int, egg_animation_frames []gg.Image, obstacle_move_vector trnsfrm2d.Vector, polygon_convex_parts [][]trnsfrm2d.Position, polygon_width f64, polygon_height f64) ! {
ecs.register_entity(mut ecs_world, [
ecs.Position{
x: egg_x_position
y: 0 - egg_image_height
},
ecs.RenderData{
image_id: egg_image_id
image_id: egg_animation_frames[0].id
orientation: common.Orientation.right
},
ecs.Animation{
frames: egg_animation_frames
time_between_frames_ms: 67
current_frame_id: 0
next_frame_id: 1
},
ecs.Velocity{
x: obstacle_move_vector.x
y: obstacle_move_vector.y
Expand Down
21 changes: 12 additions & 9 deletions src/graphics/app.v
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ mut:
is_quited bool
images_scale int

chicken_idle_image gg.Image
chicken_animation_frames []gg.Image

obstacle_section_right_image gg.Image
obstacle_endings_right_images []gg.Image
obstacle_image_id_to_y_offset map[int]int

egg_1_image gg.Image
egg_animation_frames []gg.Image

ecs_world &ecs.World
chicken_entity_id u64
Expand Down Expand Up @@ -187,8 +187,11 @@ fn react_on_input_event(event &gg.Event, mut app App) {
ecs.get_entity_component_by_entity_id[chicken.IsControlledByPlayerTag](app.ecs_world,
app.chicken_entity_id) or { return }

mut animation_component := ecs.get_entity_component_by_entity_id[ecs.Animation](app.ecs_world,
app.chicken_entity_id) or { return }

player_input.react_on_input_event(event, mut app.chicken_render_data_component, mut
app.chicken_velocity_component, get_screen_size(app).width)
app.chicken_velocity_component, mut animation_component, get_screen_size(app).width)
}

// start_app starts graphical app.
Expand Down Expand Up @@ -222,14 +225,14 @@ pub fn get_obstacle_section_right_image(app App) gg.Image {
return app.obstacle_section_right_image
}

// get_chicken_idle_image returns chicken idle image id.
pub fn get_chicken_idle_image(app App) gg.Image {
return app.chicken_idle_image
// get_chicken_animation_frames returns chicken animation frames.
pub fn get_chicken_animation_frames(app App) []gg.Image {
return app.chicken_animation_frames
}

// get_egg_1_image returns egg 1 image id.
pub fn get_egg_1_image(app App) gg.Image {
return app.egg_1_image
// get_egg_animation_frames returns egg animation frames.
pub fn get_egg_animation_frames(app App) []gg.Image {
return app.egg_animation_frames
}

// get_images_scale returns images scale.
Expand Down
24 changes: 19 additions & 5 deletions src/graphics/images_loading.v
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,28 @@ const (
}
)

const (
chicken_frames_count = 5
egg_frames_count = 6
)

fn load_assets(mut app App) ! {
right_obstacle_assets_path := common.get_platform_dependent_asset_path('obstacle/right')
chicken_idle_asset_path := common.get_platform_dependent_asset_path('chicken/chicken_idle.png')
egg_1_asset_path := common.get_platform_dependent_asset_path('egg/egg_1.png')

load_images_right_obstacle_images(mut app, right_obstacle_assets_path)!
app.chicken_idle_image = load_image(mut app, chicken_idle_asset_path)!
app.egg_1_image = load_image(mut app, egg_1_asset_path)!

for chicken_frame_count in 0 .. graphics.chicken_frames_count {
chicken_frame_asset_path := common.get_platform_dependent_asset_path('chicken/chicken_flight_${
chicken_frame_count + 1}.png')

app.chicken_animation_frames << load_image(mut app, chicken_frame_asset_path)!
}

for egg_frame_count in 0 .. graphics.egg_frames_count {
egg_frame_asset_path := common.get_platform_dependent_asset_path('egg/egg_${
egg_frame_count + 1}.png')

app.egg_animation_frames << load_image(mut app, egg_frame_asset_path)!
}
}

fn load_images_right_obstacle_images(mut app App, root_path string) ! {
Expand Down
66 changes: 50 additions & 16 deletions src/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import collision
import common

const (
target_fps = 144.0 // NOTE: 144.0 is a target value for my phone. Most phones should have 60.0 I think.
time_step_seconds = 1.0 / target_fps // NOTE: This should be used for all game logic. Analog of delta time is some engines.
time_step_nanoseconds = i64(time_step_seconds * 1e9) // NOTE: This is used only for game loop sleep.
target_fps = 60.0 // NOTE: 144.0 is a target value for my phone. Most phones should have 60.0 I think.
time_step_seconds = 1.0 / target_fps // NOTE: This should be used for all game logic. Analog of delta time is some engines.
time_step_nanoseconds = i64(time_step_seconds * 1e9) // NOTE: This is used only for game loop sleep.
time_step_milliseconds = int(time_step_seconds * 1e3) // NOTE: This is used only for game loop sleep.
)

const (
Expand Down Expand Up @@ -43,7 +44,7 @@ fn start_main_game_loop(mut app graphics.App, mut ecs_world ecs.World) {
screen_size := graphics.get_screen_size(app)
images_scale := graphics.get_images_scale(app)

chicken_entity := chicken.spawn_chicken(mut ecs_world, screen_size, graphics.get_chicken_idle_image(app),
chicken_entity := chicken.spawn_chicken(mut ecs_world, screen_size, graphics.get_chicken_animation_frames(app),
images_scale, time_step_seconds) or { panic("Can't spawn chicken - ${err}") }

graphics.set_chicken_data(mut app, chicken_entity)
Expand All @@ -61,7 +62,7 @@ fn start_main_game_loop(mut app graphics.App, mut ecs_world ecs.World) {
screen_width := screen_size.width
get_screen_pixels := get_all_x_pixels(screen_width)

egg_polygon_convex_parts := common.load_polygon_and_get_convex_parts(graphics.get_egg_1_image(app).path,
egg_polygon_convex_parts := common.load_polygon_and_get_convex_parts(graphics.get_egg_animation_frames(app)[0].path,
images_scale) or { panic("Can't load egg's polygon - ${err}") }

egg_polygon_width := collision.calculate_polygon_collider_width(egg_polygon_convex_parts)
Expand All @@ -75,6 +76,8 @@ fn start_main_game_loop(mut app graphics.App, mut ecs_world ecs.World) {
panic('Chicken entity does not have velocity component!')
}

mut egg_entities_to_remove_on_animation_end := []u64{}

for graphics.is_quited(app) == false {
obstacle_id = try_spawn_obstacle_and_egg(mut obstacle_spawner_stopwatch, mut ecs_world,
obstacles_render_data, screen_width, obstacle_id, mut app, get_screen_pixels,
Expand All @@ -89,10 +92,15 @@ fn start_main_game_loop(mut app graphics.App, mut ecs_world ecs.World) {

destroy_entities_below_screen(mut ecs_world, screen_size.height) or {}

handle_collision(mut ecs_world, chicken_entity) or {
handle_collision(mut ecs_world, chicken_entity, mut egg_entities_to_remove_on_animation_end) or {
println("Can't handle collision - ${err}")
}

play_animations(mut ecs_world) or { println("Can't play animations - ${err}") }

remove_egg_entities_on_animation_end(mut egg_entities_to_remove_on_animation_end, mut
ecs_world)

graphics.invoke_frame_draw(mut app)

time.sleep(time_step_nanoseconds * time.nanosecond)
Expand Down Expand Up @@ -138,6 +146,18 @@ fn spawn_obstacle(mut ecs_world ecs.World, obstacle_graphical_assets_metadata ob
obstacle_min_blocks_count, obstacle_move_vector, obstacle_id)!
}

fn spawn_egg(mut ecs_world ecs.World, mut app graphics.App, obstacle_id int, get_screen_pixels []int, polygon_convex_parts [][]transform.Position, polygon_width f64, polygon_height f64) ! {
egg_image_id := graphics.get_egg_animation_frames(app)[0].id
egg_image_width := graphics.get_image_width_by_id(mut app, egg_image_id)
egg_image_height := graphics.get_image_height_by_id(mut app, egg_image_id)

egg_x_position := egg.calculate_egg_x_position(ecs_world, egg_image_width, obstacle_id,
get_screen_pixels)

egg.spawn_egg(mut ecs_world, egg_x_position, egg_image_height, graphics.get_egg_animation_frames(app),
obstacle_move_vector, polygon_convex_parts, polygon_width, polygon_height)!
}

fn destroy_entities_below_screen(mut ecs_world ecs.World, screen_height int) ! {
query := ecs.check_if_entity_has_component[ecs.Position]
entities_to_check := ecs.get_entities_with_query(ecs_world, query)
Expand All @@ -149,7 +169,7 @@ fn destroy_entities_below_screen(mut ecs_world ecs.World, screen_height int) ! {
}
}

fn handle_collision(mut ecs_world ecs.World, chicken_entity ecs.Entity) ! {
fn handle_collision(mut ecs_world ecs.World, chicken_entity ecs.Entity, mut egg_entities []u64) ! {
if ecs.check_if_entity_has_component[collision.Collider](chicken_entity) == false {
return
}
Expand All @@ -160,7 +180,9 @@ fn handle_collision(mut ecs_world ecs.World, chicken_entity ecs.Entity) ! {
for entity in entities_to_check {
if collision.check_collision(chicken_entity, entity)! {
if ecs.check_if_entity_has_component[egg.IsEggTag](entity) == true {
ecs.remove_entity(mut ecs_world, entity.id)!
mut animation_component := ecs.get_entity_component[ecs.Animation](entity)!
animation_component.is_playing = true
egg_entities << entity.id
} else {
ecs.remove_component[chicken.IsControlledByPlayerTag](mut ecs_world, chicken_entity.id)!
ecs.remove_component[collision.Collider](mut ecs_world, chicken_entity.id)!
Expand All @@ -171,14 +193,26 @@ fn handle_collision(mut ecs_world ecs.World, chicken_entity ecs.Entity) ! {
}
}

fn spawn_egg(mut ecs_world ecs.World, mut app graphics.App, obstacle_id int, get_screen_pixels []int, polygon_convex_parts [][]transform.Position, polygon_width f64, polygon_height f64) ! {
egg_image_id := graphics.get_egg_1_image(app).id
egg_image_width := graphics.get_image_width_by_id(mut app, egg_image_id)
egg_image_height := graphics.get_image_height_by_id(mut app, egg_image_id)
fn play_animations(mut ecs_world ecs.World) ! {
query := ecs.query_for_two_components[ecs.Animation, ecs.RenderData]
entities := ecs.get_entities_with_query(ecs_world, query)

egg_x_position := egg.calculate_egg_x_position(ecs_world, egg_image_width, obstacle_id,
get_screen_pixels)
for entity in entities {
mut animation_component := ecs.get_entity_component[ecs.Animation](entity)!
mut render_data_component := ecs.get_entity_component[ecs.RenderData](entity)!

egg.spawn_egg(mut ecs_world, egg_x_position, egg_image_height, egg_image_id, obstacle_move_vector,
polygon_convex_parts, polygon_width, polygon_height)!
ecs.animation_system(mut animation_component, mut render_data_component, time_step_milliseconds)
}
}

fn remove_egg_entities_on_animation_end(mut egg_entities []u64, mut ecs_world ecs.World) {
for id in egg_entities {
mut animation_component := ecs.get_entity_component_by_entity_id[ecs.Animation](ecs_world,
id) or { continue }

if animation_component.is_playing == false {
ecs.remove_entity(mut ecs_world, id) or { continue }
egg_entities.delete(id)
}
}
}
14 changes: 13 additions & 1 deletion src/player_input/player_input.v
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ import ecs
// react_on_input_event handles the input event and triggers chicken jump.
// For Android platform, it checks if a touch event has occurred.
// For other platforms, it checks if an arrow key has been pressed.
pub fn react_on_input_event(event &gg.Event, mut rendering_metadata_component ecs.RenderData, mut velocity_component ecs.Velocity, screen_width int) {
pub fn react_on_input_event(event &gg.Event, mut rendering_metadata_component ecs.RenderData, mut velocity_component ecs.Velocity, mut animation_component ecs.Animation, screen_width int) {
$if android {
if event.typ == .touches_began && event.num_touches > 0 {
animation_component.current_frame_id = 0
animation_component.next_frame_id = 1
animation_component.time_left_to_next_frame_ms = 0

animation_component.is_playing = true

execute_chicken_jump_system(mut rendering_metadata_component, mut velocity_component,
event.touches[0].pos_x > screen_width / 2)
}
} $else {
if event.typ == .key_down && (event.key_code == .left || event.key_code == .right) {
animation_component.current_frame_id = 0
animation_component.next_frame_id = 1
animation_component.time_left_to_next_frame_ms = 0

animation_component.is_playing = true

execute_chicken_jump_system(mut rendering_metadata_component, mut velocity_component,
event.key_code == .right)
}
Expand Down

0 comments on commit 3b5cd8a

Please sign in to comment.