Custom Entity: Companion

Started by Satoh, January 21, 2015, 06:32:23 AM

Previous topic - Next topic
Ok, so I think a custom entity is what I need for this.

I want to make an object that follows and orbits the hero across multiple maps.
In this case something like a fairy or a spirit orb or the like.

There are a few problems I have, mostly because I'm not sure how the engine does them (if it does them).

The first thing is blending modes: Add and Multiply.
The SNES could overlay sprites as transparent objects that would add to the brightness of pixels beneath them (this is how they made sunbeams and torch lights work for example), but this is Solarus, not SNES. I'm not sure if it supports this currently. (the method for implementing it isn't hard if you can modify pixel arrays. In case you need to add it, the general logic is like this:

foreach spritepixel //we only need to mess with the sprite
{
  final_R = Math.Min(screen_R + sprite_R, 255); //If the result is bigger than a byte, we clamp it.
  final_G = Math.Min(screen_G + sprite_G, 255);
  final_B = Math.Min(screen_B + sprite_B, 255);
  //Additive sprites usually don't need alpha since black + any color = no change.
  //so we'll ignore the alpha channel.
  //Obviously this is not in C++ syntax, but I'm sure you're able to fix that easily.
}

I don't think I can do it directly from Lua, judging by how solarus handles drawing, and there's no mention of blending modes. If it's not in the engine, consider this a feature request.

Moving on.
I know exactly how I want the orbit to work, but I'm not certain if it works in Solarus.
local entity = ...
local hero = map:get_entity("hero")
local acceleration = 0.2
local maxspeed = 4.0
local xspeed = 0.0
local yspeed = 0.0
local sprite2 = nil
local x = 0.0
local y = 0.0

function entity:on_created()
  self:create_sprite("entities/faeglow")
  self:set_size(8, 8)
  self:set_origin(4, 4)
  x, y = self:get_position()
  self:set_position(x, y, 2)
  self:get_sprite():set_animation("faeglow")
  sprite2 = sol.sprite.create("entities/faewings")
  sprite2:set_animation("faewings")
end

function entity:on_update()
  local hero_x, hero_y = hero:get_position()
  x, y = self:get_position()
  --Each direction gets a unique check because the entity accelerates each way separately.
  --This creates a nice simple sinusoidal motion and allows the object to follow a fluid organic
  --path to and orbit around its destination, instead of stopping.
  if hero_x > x and xspeed < maxspeed then
    xspeed += acceleration
  end
  if hero_y > y and yspeed < maxspeed then
    yspeed += acceleration
  end
  if hero_x <= x and xspeed > -maxspeed then
    xspeed -= acceleration
  end
  if hero_y <= y and yspeed > -maxspeed then
    yspeed -= acceleration
  end
  --now we actually change the position.
  self:set_position(x+xspeed, y+yspeed)
end

function entity:on_pre_draw()
  self:get_map():draw_sprite(sprite2, x, y)
end


I believe this should work, but I'm not entirely certain if the engine can handle decimal positions, and I'm not sure if this is all I need.

Lastly, I don't know how to make this be a global entity. I've considered making it some kind of item that's simply always in use... but I'm not sure if that would work either.

So, can I get some advice?
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

Hi,
Direct pixel manipulation is not possible yet from Lua, but this is a planned feature for a next version. In the meantime you can make your image semi-transparent so that it blends when it is drawn. All drawing operations are alpha-blended.

You code should work but there are a few issues:
- The position of an entity is always integer numbers. The engine truncates the coordinates when you call set_position(). Maybe this is simply okay in your case.
- entity:on_update() is called at each frame, and it is good practice to be independent from the frame rate. So you can replace on_update() by a repeated timer called with a delay that you control. (Another solution is to create a movement object instead but I don't think that any built-in movement provides the kind of acceleration that you need. Or maybe a pixel movement whose trajectory is something that you compute initially and a speed that you change over time?)

About your last question (making it global), a custom entity cannot survive accross maps, but this is not a problem. You can easily re-create a new one when entering any map. For example in game:on_map_changed(). It is even possible to keep the state but I don't think you need that (but if you do, I can explain :))

January 21, 2015, 08:17:01 PM #2 Last Edit: January 21, 2015, 08:31:45 PM by Satoh
What object should I attach my game:on_map_changed() to?

Where should I put my timer, if not on_update()?
I'm assuming scripts only get updated when something tells them to update, so I'm not sure how to make a timer in Lua that wouldn't be frame-rate dependent.

EDIT:
Ah, ok, I forgot sol.timer.start(...)
I should put that in on_created() right? Then it should call the function for movement, and restart its timer...
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

In game:on_map_changed(map), create the custom entity with map:create_custom_entity(........) with the appropriate parameters : in particular, the model will be the name of your custom entity script.

And yes, sol.timer.start would be done in entity:on_created().
Note that you can make a timer repeat itself automatically by returning true in its callback.

I can't seem to get my entity to be created.
I've tried placing it on a map directly,
I've tried adding game:on_map_changed(map) to it...
I tried creating it in a map script event I can trigger manually...

I added a sound effect in the on_created() so I'd at least know it was there even if it was invisible.

Nothing I do makes the entity work.

This is the whole script. Sorry I can't shorten it for convenience.
local entity = ...
local hero = map:get_entity("hero")
local acceleration = 1
local maxspeed = 4
local xspeed = 0
local yspeed = 0
local sprite2 = nil
local x = 0
local y = 0

function game:on_map_changed(map)
  map:create_custom_entity{
      name = "navy",
      layer = 2,
      x = 0,
      y = 0,
      direction = 3,
      --model = "entities/fae"
      model = "fae"
    }
end

function entity:on_created()
  self:create_sprite("entities/faeglow")
  self:set_size(8, 8)
  self:set_origin(4, 4)
  x, y = hero:get_position()
  self:set_position(x, y, 2)
  self:get_sprite():set_animation("glow")
  sprite2 = sol.sprite.create("entities/faewing")
  sprite2:set_animation("flap")
  sol.timer.start(33, moving)
  sol.audio.play_sound("secret")

end

local function moving()
  local hero_x, hero_y = hero:get_position()
  --x, y = self:get_position()
  --Each direction gets a unique check because the entity accelerates each way separately.
  --This creates a nice simple sinusoidal motion and allows the object to follow a fluid organic
  --path to and orbit around its destination, instead of stopping.
  if hero_x > x and xspeed < maxspeed then
    xspeed += acceleration
  end
  if hero_y > y and yspeed < maxspeed then
    yspeed += acceleration
  end
  if hero_x <= x and xspeed > -maxspeed then
    xspeed -= acceleration
  end
  if hero_y <= y and yspeed > -maxspeed then
    yspeed -= acceleration
  end
  x+=xspeed
  y+=yspeed
  --now we actually change the position.
  self:set_position(x, y)
  --self:set_position(x+xspeed, y+yspeed)
  return true --restarts timer
end

function entity:on_pre_draw()
  self:get_map():draw_sprite(sprite2, x+xspeed, y+yspeed)
end


I can't figure out what I've done wrong.
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

Your entity script is called when the entity is created, so the map:create_custom_entity() has to be done from outside. I was recommending to do it from game:on_map_changed() so that you have it on all maps. And game:on_map_changed() is a function that you have to define wherever the game is initialize (in game_manager.lua if you follow my tutorials/examples).

As a first step, if you have trouble do it on all maps from game:on_map_changed(), you can start by creating the entity from the map editor on a specific map.

Several hours of trial and error and I have finally discovered why it wasn't working.
I was trying to get references too early in the script, and thus the coordinates I was supplying, didn't exist, so neither did the entity.

After rearranging some things, it does exactly what I thought it should when I first wrote it.

You mentioned 'keeping its state' before.
Its not strictly necessary for this, but I've been considering some more complex followers, that may need to remember 'where they are' in relation to the hero when changing maps, so they can return there when created fresh. (and maybe other things like equipment or something)

Anyway, in short, how do I store that type of information to be reloaded on the next map?
My instinct is "create a save game variable" like with items, but that seems a bit messy for something that could get written frequently. (If the player changes maps really fast)
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

You can set a structure on your game object : the game will remember these as long as it runs. When you stop a game, you can write all these values on a save file using the file api (or in savegame variables if you prefer).

Exactly. If you don't need to save it, just use the fact that it is possible to add anything to any object: not only functions but also tables or any datatype. In your case, the game since you want it to persist accross maps.
Something like this:

game.companion_info = {
  x_from_hero = 0,
  y_from_hero = 0,
}


It will also work by using savegame variables (game:get/set_value()), but this is overkill if you don't actually need to save the information.

Ah! Ok, I'm used to C type languages where everything is immutable after it's created!

I didn't realize I could modify the structure of an object at runtime. That makes things a lot easier.
I got it working now, thanks to all your help.

Knowing what I do now, I might even be able to create a crude party system... only question is, what do I do with this knowledge... so many possibilities.
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

Hi all,
I have been working on implementing code like this into my solarus game
though it works and the companions spawn I am having trouble getting
the companions to spawn near the hero.
The typical methods such as altering the x and y have not worked.
Any suggestions.

Code sample attached.
  local hero = self:get_map():get_entity("hero")
  local x, y, layer = hero:get_position()
  local direction = hero:get_direction()

  if game:get_value("a1000") and game:get_value("a1006")
  then
  ally1 = map:create_custom_entity{
      name = "a_butterfly",
      x = x,
      y = x,
      layer = layer,
      direction = direction,
      sprite = "allies/a_butterfly",
      model = "ally_butterfly"
    }
  self.created = true
  local ally1 = true
  local allynumber = 1
  end

@polyglot762, you have a mistake in your code. Write "y = y" instead of "y = x".
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

 :o Yes... yes. I do...  :-\ gonna go fix it. thanks.