MetalES / Project-Zelda

Project Zelda, made using Solarus
12 stars 1 forks source link

Make object movement time-synchronized #58

Closed MetalES closed 7 years ago

MetalES commented 7 years ago

Some Castle Town NPC's and other place's NPC have a predefined movement, mostly from point A to point B, however, some need to be synchronized with the current time of the day, I didn't take this as a consideration when making the Time system or designing the NPC themselves.

The PZE does this, yet I'm not sure about the real algorythm, which should be something like

speed = default_speed * something / time flow

Looks like speed = (default_speed * time_flow) / 1000 works

Diarandor commented 7 years ago

By curiosity, do some of these NPCs change to different maps depending on the hour or day? That is something to take into account. I give you some ideas; you can use them or not, as you prefer.

A very delicate detail: make sure that your NPCs, when created, do not overlap the hero, at least if they are not traversable. To avoid them appearing in some map entrance and making the hero be stuck, you could make them traversable when walking and then make them solid when necessary (use a timer to check if the hero is not overlapping them, and otherwise repeat the timer).

You could do the code in the following way, or in a similar one:

1-A function to get the exact moment for the time (I guess this could be the day, hour and minute, but I don't know which is exactly your time system). You could call it "game:get_time()" or something like that.

2-For each map script, a list of the NPC names that may appear in that map. This can be used to iterate for their creation.

3-For the map metatable, a general function that creates the NPCs in the correct position and state (i.e., doing the corresponding action, initialize the corresponding dialogs, etc). This could be called map_meta:create_npc_in_time(info), where info is a list with the necessary info to create it.

4-For each particular NPC, a particular function (lets call it here NPC_NAME:get_info()) that calculates all the info to recreate it in the corresponding state. This should return all the necessary info that needs to be used by the function "map_meta:create_npc_in_time(info)". For NPCs that move, you may need to parametrize positions/movements in terms of the time (use the function "get_time()" for this); if you have problems to recover this info, I will try to help with this (when I have free time). For the dialogs that depend on the time, just make one function per NPC, defined on the map script, that chooses the corresponding action or dialog, depending on the current time. The functions "NPC_NAME:get_info()" could return a list with the position, sprite name, animation, direction, movement to initialize (or maybe a particular callback function that initializes the movement/behavior of the NPC).

I don't know how your NPCs behave, but this may be quite complex to do, and a lot of work. I hope these ideas help you a bit.

Diarandor commented 7 years ago

To simplify the NPCs creation, maybe it will be better to create them always by default, and then use the functions "NPC_NAME:get_info()" and "map_meta:create_npc_in_time(info)" to display them correctly, or hide them when they leave the map temporarily (if you disable them when they leave the map, you may need a timer in the map script to enable them when they need to come back to the map).

MetalES commented 7 years ago

1 - The time is always updated and saved as a savegame value, but you are right, a function game:get_time() would be more convenient, and write the time in a savageme value only if saving the game I should use the multiple event register by Christopho and register game:save() in each ogf the script that modify and save a value each second, it would make the code more cleaner than it is right now

2 - Nice idea, an array that contain all time-synchronised NPC, movement, starting position, default speed would be a nice feature, especially for the idea 3

3 - This is connected to the solution number 2 I guess

4 - I already though about a map:reload_movements() since NPC will be stored in a array with all infos, and map:reload_movement called only when the map is created or when the time flow change (playing the sun's song, being in slow motion mode for some things)

Diarandor commented 7 years ago

Exactly, the list in 2) would be used to iterate for all NPCs in 3), for their initialization.

MetalES commented 7 years ago

There are a lot of parameters to consider, first, to synchronize an event, let's say

x = NPC / = Begin / End of movement

6:00 6:30 7:00 /----------------------x---------------------/

Assume that this movement is a path movement, if you stay on the same map from 6:00 to 7:00, no problem, the movement will be played, but reloading the map, repositionning the character to be at a certain position, the path movemet will be strange, maybe using a target movement will be better ?

Diarandor commented 7 years ago

The minimum movement changes you use, the better. I suggest avoiding path movements since these make a zigzag, so the movement is constantly changing. I would use a list of lists with the info for each node, i.e., the info for each position where the movement changes: leaving-time, position, speed and direction. You can use target movements from one of such points to the next one on the list; intermediate positions can be calculated easily for target movements if these are linear, so make sure these are so (or just use straight movements). You could use straight movements instead of target ones, if you prefer. To keep an NPC fixed, set the speed to 0 for the corresponding list.

Diarandor commented 7 years ago

Also, a function that converts time lenghts of your time system into seconds or milliseconds, and conversely, might be useful to code the parametrized trajectories: when you enter a map, to compute the position of a walking NPC, you will need the speed and time, both expressed with the same time unit.

MetalES commented 7 years ago

The movement speed is already done, the algorythm is above.

This is something from the Project Zelda Engine's Time System, it returns the current time in a week in minutes maybe this is what you are talking about ?

def get_prog_minute(minute, hour, day)
    #return the progressive minute
    return (minute - STARTING_MINUTE) + (hour - STARTING_HOUR) * 60 +
           (day - 1) * 1440
  end

where Minute, Hour, Day = the current time Starting minute / day = the very first initialisation of the time system (in my game 11:30AM)

So, as example, this is what it should looks like

function game:get_progressive_time()
  local hour, minute, day = game:get_time()

  return (minute - 30) + (hour - 11) * 60 + (day - 1) * 1440
end

Then, let's admit, we are on a map, take the example above, it is 6:25 and the NPC would have already do some movements.

So, on recreation time, to position this, should I use

npc[1][1]:set_position(npc[1][1]:get_position() + (something + get_progressive_time)) ?

MetalES commented 7 years ago

OOOOOH, I might have found what you are talking about !

You mean, spawn and place the character if a certain time (progressive time) is reached, so I should use map:on_progressive_time_updated(new_progressive_time) instead of map:on_time_update(hour, minute, day) ?

This looks the same, meh, I could still use get_progressive_time anyway

https://www.youtube.com/watch?v=lhntUnT5B2M

Diarandor commented 7 years ago

I would do it in a different way. Assume that we have all info in a list of "nodes" (just an abstraction, think of them like nodes in a circular graph), each node is a list of info with a position, the starting-time in that position, a walking speed, etc. Then we can compute the position as follows.

1) Get the current time and find the first node previous to that time (then we will also know the following node since the list of nodes is ordered). 2) We know the position of the Previous node (px, py) and its speed S, and the position of the Next node (nx, ny). I will assume that your movements are all straight withouth obstacles in the middle (being stopped in a position for a while is also doable with a "node" of zero speed). Call Pt and Nt the times of the previous and next node, T the current instant of time. The current position (cx, cy) is what we need to compute.

3)Then we can compute the position, approximately, in several different ways. One possible way is with the formulas:

*) Proportion of the travelled part between the two nodes, for the time: propT = (T - Pt) / (Nt - Pt)

*) Proportion of the travelled part between the two nodes, for the distance, has to be the same constant, so we get:

dX = propT (nx - px) dY = propT (ny - py)

*)The current position: cx = px + dX cy = py + dY

There are other ways to do this by computing the angle, etc, but I like more this way. After creating the NPC in the current position, just create the movement towards the next node with the speed S of the previous node.

Diarandor commented 7 years ago

"You mean, spawn and place the character if a certain time (progressive time) is reached, so I should use map:on_progressive_time_updated(new_progressive_time) instead of map:on_time_update(hour, minute, day) ?"

I am not sure if this is the same as I explained or not, but it could be similar. I don't know.

MetalES commented 7 years ago

There should be easier way to do it, this is how it is done in the RPG Maker script

`

===============================================================================

Game_Event

===============================================================================

class Game_Event < Game_Character attr_reader :time_event alias max_time_initialize_later initialize unless $@ alias max_time_stop_later update_stop unless $@ alias max_time_steps_later increase_steps unless $@ alias max_time_custom_later move_type_custom unless $@ def initialize(args) @time_stop = 0 @time_event = false max_time_initialize_later(args)

initialize time event flags

init_page = set_time_event_flag1 = set_time_event_flag2 = false
#calculate the initialized page id
@event.pages.each_index do |i|
  if @page == @event.pages[i]
    init_page = i
    #flag the first time event flag
    set_time_event_flag1 = true
    break
  end
end
#if not erased or no page
unless @erased
  #loop through the pages
  @event.pages.each do |page|
    c = page.condition
    #if switch 1 condition is valid
    if c.switch1_valid
      #flag as a time event if the switch is a time system switch
      @time_event = true if PZE::SWITCHES.include?(c.switch1_id)
    end
    #if switch 1 condition is valid
    if c.switch2_valid
      #flag as a time event if the switch is a time system switch
      @time_event = true if PZE::SWITCHES.include?(c.switch2_id)
    end
    #if the variable condition is valid
    if c.variable_valid
      #if the varaible id is a time system variable
      if PZE::VARIABLES.include?(c.variable_id)
        @time_event = true 
        #change to progressive minute if progressive hour
        if c.variable_id == PZE::PROG_HOUR
          c.variable_id = PZE::PROG_MINUTE
          c.variable_value *= 60
        end
        #flag the seccond time event flag
        set_time_event_flag2 = true if c.variable_id == PZE::PROG_MINUTE
      end
    end
  end
  #move into place if both time event flags are activated
  set_time_event(init_page) if set_time_event_flag1 && set_time_event_flag2
end

end

def set_time_event(final_page_index)

return if there is no initialized page

return if final_page_index.nil?
#loop through each page until the initialized page
(final_page_index + 1).times do |page_index|
  # Open the events page
  pg = @event.pages[page_index]
  # if valid
  if pg.condition.variable_id == PZE::PROG_MINUTE && pg.move_type == 3 && 
     pg.condition.variable_valid
    # Load the movement data
    @move_type = pg.move_type
    @move_speed = pg.move_speed
    @move_frequency = pg.move_frequency
    @move_route = pg.move_route
    @move_route_index = 0
    @move_route_forcing = false
    @walk_anime = pg.walk_anime
    @step_anime = pg.step_anime
    @direction_fix = pg.direction_fix
    @through = pg.through
    @always_on_top = pg.always_on_top
    # Variables used for the time calculations
    frames_updated = 0 # time increment (in frames)
    last = (page_index == final_page_index) # Page set to go to
    sys_time_frames = $game_system.time.get_prog_frame
    frames_start_page = pg.condition.variable_value * PZE::FRAMES
    #loop through move route list
    while @move_route_index < @move_route.list.size - 1
      # Skip while playing a sound effect
      begin
        while [44].include?(@move_route.list[@move_route_index].code)
          @move_route_index += 1
        end
      rescue
        break
      end
      # Move the event
      last_move_index = @move_route_index
      max_time_custom_later
      @move_route_index = last_move_index + 1
      # Count how long the step took
      frames_to_wait = 1
      if @wait_count > 0
        frames_to_wait += @wait_count - 1
        @wait_count = 0
      end
      if @jump_peak > 0 or @jump_count > 0
        frames_to_wait += @jump_count - 1
        @jump_peak = 0
        @jump_count = 0
      end
      if (@y * 128 != @real_y) or (@x * 128 != @real_x)
        frames_per_square = 128 / (2 ** @move_speed)
        wait_x = (((@x * 128 - @real_x).abs + 127) / 128) *frames_per_square
        wait_y = (((@y * 128 - @real_y).abs + 127) / 128) *frames_per_square
        frames_to_wait += [wait_x, wait_y].max
        frames_to_wait -= 1
        @real_x = @x * 128
        @real_y = @y * 128
      end
      frames_updated += frames_to_wait
      # If the movement is done, finish
      if last and frames_start_page + frames_updated >= sys_time_frames
        return
      end
    end
  end
end

end

def update_stop

if the event is a time event and move type is custom

if @time_event && @move_type == 3 && @character_name != '' 
  command = @move_route.list[@move_route_index]
  if command != nil && command.code > 0 && command.code < 15 && @wait_count == 0 
    #update the time stop
    @time_stop += 1
    #if time stop is equal to frame rate
    if @time_stop >= PZE::FRAMES
      #reset the time stop
      @time_stop = 0
      #loop through the pages
      @event.pages.each do |page|
        c = page.condition
        #if the variable condition is valid
        if c.variable_valid
          #if variable is a prog minute
          if c.variable_id == PZE::PROG_MINUTE
            #increment the minute condition
            c.variable_value += 1
          end
        end
      end
    end
  end
end
max_time_stop_later

end

def increase_steps @time_stop -= 1 if @time_event && @move_type == 3 && @character_name != '' max_time_steps_later end

def move_type_custom return if @time_event && $game_system.map_interpreter.running? && !@move_route_forcing max_time_custom_later end end `

Just need to decrypt how it is done, then I could have the right formula about character placement But, it can be your algorythm as well, as it is just comparing the start, arrival with the speed and the time flow

Diarandor commented 7 years ago

Some final comments in case you decide to use my method: -If you have some NPC like the post man that may leave the map, when they leave, put them in a position outside the map so that it is not visible. In this way, its timers will keep working, which may be necessary to make them reappear if you stay in the map all the time. -To each "node" for an NPC, you can also attach particular functions if necessary, for particular stuff. Nodes may be any type of action/function in general, and not only for movements. -Each node may also have an optional corresponding dialog_id (that may change depending on where the NPC is going to). -You will need to add a timer to each NPC that checks the time to start the action of the next node when necessary. This method is very flexible, but it may be long to code since you need the exact positions and time for each node. I don't think the other method will be much easier, since you have to obtain similar information somehow, and that info has to be stored somewhere in your map script and recovered from there with functions, I guess. If the actions of your NPCs are not so complex, then maybe you can use a simpler, but less general, time system.

EDIT: if you use my time system, I recommend to make NPCs traversable when they walk, to avoid the player blocking their path, which would mess up the actions of the NPC (things may happen where they should not happen).

MetalES commented 7 years ago

Yeah, it should be the best solution out there I'm gonna use your solution, the one used in the PZE is rather hard to decrypt

MetalES commented 7 years ago

Okay, the movement on object works, it is updated if the time flow change, then there is a limit on the engine, the speed of an object cannot exceed 1000 or else it will crash https://www.youtube.com/watch?v=duwB3X12vi0