Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Bagu

#1
Yeah, I just picked up Aseprite. At $15, it feels stupid to overthink the decision. Even if I were to change my mind and get Pro Motion NG, $55 spent on the two programs is a drop in the bucked compared to the amount I've spent on real life paints, brushes, canvases, etc. as well as music or computer gear.
#2
General discussion / Re: Some Zelda-like games
July 19, 2018, 05:09:13 PM
Yeah, I picked up Sword Of Ditto last month on Steam, and I there's some really nice aspects to it, but I can see why it'd piss people off. It's trying to be a rogue-lite where you lose everything on death, but that aspect doesn't feel as well-executed as other similar games. When I die in Spelunky, Vagante, or Dead Cells, I usually want to try again and see if I do better... in Swords Of Ditto death just makes me feel like quitting out of boredom.

The game has potential, but I think they needed to spend more time refining the balance of the permadeath mechanic and which things you need to collect all over again on each run.

By the way, Elysis has a tumblr now where the latest progress is being shown off. It's looking really good:

https://blankthevidya.tumblr.com/tagged/elysis
#3
Just to clarify what I'm asking about, it's programs which do the following:




There's a usable example of it here.

After trying a couple demo versions these were my observations as far as free-hand optimization:

Aseprite: The pixel-perfect option works like the example above and really gives a lot more precision when free-handing small areas. The "contour" tool is also a very handy way to fill in large areas quickly. My only complaint is there's no support for tablet pen pressure, so you can't create line-width variation on the fly.

Pro Motion NG: The trial version doesn't allow you to try the pixel-perfect mode, which is pretty annoying. However, Pro Motion does support pen pressure, so assuming the pixel perfect mode works just as well as Aseprite's, it's the better choice to accomplish everything in one program. It's simply annoying that you can't evaluate all the functionality before purchasing (the $40 asking price is a lot compared to other popular pixel art tools).

One other factor in Aseprite's favor IMO is the simple GUI; it's a pleasant break from the clutter of mini-windows plastered all over Photoshop. Conversely Pro Motion NG feels pretty much just as bad as Photoshop in that regard.

These are not the only programs with a pixel-perfect option, just the one's I've tried so far.
#4
What is the best pixel art tool for freehand drawing?

Up to now I've been using Photoshop for pixel art, simply because I have decades of familiarity using it for other tasks, but lately things seem to go seriously wrong when I try to freehand draw at small sizes, and then I finally realized it was primarily due to the pencil tool's lack of a pixel perfect functionality.

I understand Asprite and a few other programs have pixel-perfect options, but which one do you guys prefer/recommend? Is there one which has a better pixel-perfect algorithm than others, or has any other advantages for drawing freehand with a tablet/stylus rather than a mouse?
#5
Development / Re: function vs. function:enemy
June 11, 2018, 07:10:32 PM
Indeed, thanks for these explanations llamazing, it's been really helpful!
#6
Yes, it works perfectly now, thanks! I also sorted out movement:on_finished for closing the jump function.
#7
Development / Re: function vs. function:enemy
June 10, 2018, 06:48:27 PM
Ok, that pretty much explains everything. In theory if I know an enemy script won't have its functions references by any other scripts I could use local functions for everything, but if that changes it would be extra work to convert it back to enemy:functions.

One other question I have: what is the difference of calling i.e. self:jump() instead of enemy:jump()? I've seen both used and they seem to work interchangeably, but I'm sure I'm missing some distinction there.
#8
llamazing, your version looks awesome, but when I try it, self[random_choice](self) is calling a nil value. Maybe I did something wrong, but I didn't think I could mess much up with what little I added to it:

Code ( lua) Select
local enemy = ...

function enemy:on_created()
  enemy:set_life(3)
  enemy:set_damage(2)
  enemy:create_sprite("enemies/" .. enemy:get_breed())
end

local choices = {
  pause = 25,
  jump = 75,
}

-- these are functions the enemy will choose from randomly proportional to the value associated with each one
   
local random_max = 0 --sum of all choice values, calculate once when script is first run (in this example, radom_max = 25 + 75 = 100)
for _,weight in pairs(choices) do random_max = random_max + weight end
   
--this function could be moved to a different script for common utility functions
local function random_choice()
local choice_value = math.random(1, random_max) --choose random number between 1 and random_max
       
--step through list of choices to find which choice correspondes to the randomly chosen value
local cumulative = 0 --sum of current choice entry plus all previous
for choice,weight in pairs(choices) do
  cumulative = cumulative + weight
  if choice_value <= cumulative then
    return choice --current entry corresponds to the randomly chosen value
   end
end
       
--should have returned a value before getting to this line, as contingency returns some choice (undefined which one)
--you may prefer to throw an error here instead
local choice = next(choices)
        return choice
end
   
function enemy:random_action()
  self[random_choice](self) --this line calls a random function from the "choices" table at the top
end
   
function enemy:on_restarted()
  self:random_action()
end

function enemy:pause()
  local sprite = enemy:get_sprite()
  --sprite:set_animation("idle")
  enemy:stop_movement()
  sol.timer.start(enemy, math.random(1, 2000), function()
    self:random_action()
  end)
end

function enemy:jump()
  local sprite = enemy:get_sprite()
  enemy:stop_movement()
  --sprite:set_animation("jumping")
  local movement = sol.movement.create("jump")
  movement:set_direction8(math.random(0, 7))
  movement:set_distance(math.random(24, 64))
  movement:start(enemy)
  sol.timer.start(enemy, 800, function()
    self:random_action()
  end)
end
#9
I'm not very good at lua yet either!  I tried to replicate several different methods of randomization I googled and failed utterly at anything beyond printing strings until llamazing fixed my code. However, if you're interested in investigating, here's a couple examples:

https://wiki.garrysmod.com/page/table/Random
https://forums.coronalabs.com/topic/39315-how-to-iteratepick-randomly-from-associative-arrays/


The main reason I used the weighting method I did is because I lifted it from other scripts of mine with many unique functions. I like starting with this one because if I decide to add more functions I can just replace some of the duplicates in the list.

Your suggested method looks like it would work, and could be an effective basic means of weighting things. When I tried to research various methods of weighting random selections in lua, most were extremely complex (here's a good example) so I gave up and stuck to what I had working.
#10
Your scripts / Classic Tektite Random Jumping AI
June 09, 2018, 08:21:21 PM
I always liked the random jumping of tektites in Zelda 1 and ALTTP, and was a little disappointed by the tektites in Return of the Hylian which simply walked at the player. Thus once I figured out how to set up random jumping for some other enemies I figured I'd share this extremely basic and simple example of Nintendo tektite behavior.

This also would work for replicating Bits/Bots from Zelda 2 or Zols and Gels from Zelda 1.

example video: https://youtu.be/HGkoJe9JqEc

Code ( lua) Select
local enemy = ...

local choices = {"pause","jump","jump","jump"}
-- these are functions the enemy will choose from randomly. I added extra copies of the jump function so he would move 75% of the time instead of 50/50. However, you could create more functions and place each one in the list only once.

function enemy:on_created()
  enemy:set_life(3)
  enemy:set_damage(2)
  enemy:create_sprite("enemies/" .. enemy:get_breed())
end

function enemy:on_restarted()
    enemy[ choices[math.random( 1, #choices )] ](enemy) --this line calls a random function from the "choices" table at the top
end

function enemy:pause()
  local sprite = enemy:get_sprite()
  --sprite:set_animation("idle")
-- uncomment the line above if you want to use a custom pause animation, otherwise it'll use "walking"
  enemy:stop_movement()
  sol.timer.start(enemy, math.random(1, 2000), function() --the timer lasts randomly between 1 and 2000 ms. You can change the minimum and maximum to taste.
    enemy[ choices[math.random( 1, #choices )] ](enemy) --again picks a random function from the choices table, you could also just call enemy:restart
  end)
end

function enemy:jump()
  local sprite = enemy:get_sprite()
  enemy:stop_movement()
  --sprite:set_animation("jumping")
-- uncomment the line above if you want to use a custom jump animation, otherwise it'll keep using "walking"
  local movement = sol.movement.create("jump")
  movement:set_direction8(math.random(0, 7)) -- picks a random direction to jump in
  movement:set_distance(math.random(24, 64)) -- sets a distance between 24 and 64px
  movement:start(enemy)
  sol.timer.start(enemy, 800, function()  -- 800ms is the typical time for the enemy to land from a 64px jump.
    enemy[ choices[math.random( 1, #choices )] ](enemy)
  end)


I probably should have used movement:on_finished() instead of a timer to close the jump function, because if you use distances greater than 64 the jump begins to get cut-off early and it looked bad.

Art-wise, if you use something like Return of the Hylian's tektite sprite, it'll look a little weird because there's a shadow drawn into the animations. Thus the shadow floats through the air with the character as he jumps, instead of staying on the ground. The easiest solution is to simply erase any shadows from your animation frames, though this will make it visually ambiguous whether the character is jumping through the air or sliding across the ground. For the best results, you'd want to use a custom entity or something to place a separate shadow sprite under the character.

Special thanks to llamazing for helping me sort out the random function selection call!
#11
Development / Re: Choosing a Function Randomly
June 09, 2018, 07:35:07 PM
llamazing, you're right - that's exactly what I needed and it works perfectly. Thanks!!!
#12
Development / function vs. function:enemy
June 09, 2018, 05:29:35 AM
This is super basic but I've looked around and not found an answer and it's really bugging me.

So I find the following to be much cleaner, neater, and easier to deal with than the alternative:

Code ( lua) Select
function pause()
  local sprite = enemy:get_sprite()
  sprite:set_animation("idle")
  enemy:stop_movement()
  sol.timer.start(enemy, math.random(1, 2000), jump)
end

function jump()
  local sprite = enemy:get_sprite()
  enemy:stop_movement()
  sprite:set_animation("jumping")
  local movement = sol.movement.create("jump")
  movement:set_direction8(math.random(0, 7))
  movement:set_distance(math.random(12, 64))
  movement:start(enemy)
  sol.timer.start(enemy, 800, pause)
end


Whereas if I use "function enemy:blahblahblah" I appear to accomplish the same thing, yet the syntax on things like timers is longer and more restrictive:

Code ( lua) Select
function enemy:pause()
  local sprite = enemy:get_sprite()
  sprite:set_animation("idle")
  enemy:stop_movement()
  sol.timer.start(enemy, math.random(1, 2000), function()
    enemy:jump()
  end)
end

function enemy:jump()
  local sprite = enemy:get_sprite()
  enemy:stop_movement()
  sprite:set_animation("jumping")
  local movement = sol.movement.create("jump")
  movement:set_direction8(math.random(0, 7))
  movement:set_distance(math.random(12, 64))
  movement:start(enemy)
  sol.timer.start(enemy, 800, function()
    enemy:pause()
  end)
end


Then if I start mixing both in the same script, it gets much worse because I'll tend to forget and mismatch the syntaxes to the wrong functions and I spend extra time correcting stupid mistakes to clean up all the error messages.

I see the latter method used in a lot of scripts in enemies by the Solaris team, so there must be some advantage to it, or some reason why it's bad to avoid it. What am I missing? Is there a reason not to use the first approach for all my enemy scripts?

Sorry to ask about such basic stuff all the time!
#13
Development / Choosing a Function Randomly
June 09, 2018, 03:03:23 AM
I've looked at a few different methods of making a random selection from a table, as a means to make an enemy AI choose a function as its next action. For example:

Code ( lua) Select
choices = {"enemy:go_hero()","enemy:random_pause()","enemy:jump_pause()","enemy:go_random()"}

  sol.timer.start(enemy, math.random(1, 3000), function()
    choices[math.random( 1, #choices )]
end)


Code ( lua) Select
function enemy:pickaction()
math.randomseed(os.time())
local a = {"go_hero","random_pause","jump_pause","go_random"}
a[math.random(1,#a)]
end

  sol.timer.start(enemy, math.random(1, 3000), function()
    enemy:pickaction()
  end)


I've tried about 20 different variations of this approach and always wind up with an error message like "= expected near end" or "function arguments expected near [".

I can get it to print the desired results to the console but the engine doesn't seem to want to accept them as anything remotely resembling a function. What am I doing wrong?
#14
Your scripts / Re: Dodge/Dash/Roll Action
June 03, 2018, 11:08:48 PM
Quote from: Max on June 03, 2018, 10:05:28 PM
That looks great! I like your big sprites, how's that going?

Thanks! The big sprites are fun but there's lot of hit detection bugs to work out. I haven't set correct sizes for enemies yet, and I haven't figured out how to change the true launch "height" for hero projectiles like arrows and boomerangs, so I faked it by lowering the sprite origin, so of course weird stuff happens. Still though, there are fewer problem than you'd think considering how much of the engine is built around 16x16.

Quote from: Max on June 03, 2018, 10:05:28 PMI think whether or not you have the dash as its own item would have a lot to do with how you intend the player to use it in combat. Is combat designed around the player being able to dodge enemy attacks, or is this just an add-on? I've found (to me) that Link to the Past's combat isn't amazing. I image it was great for the time, and it's functional if enemies have a wide variation of different movements, attacks, patterns, weaknesses, but it's never great (maybe some bosses might reach that level). Hopefully, with abilities like dodging, an active shield (like Link's Awakening or Minish Cap), and different enemy behaviors, we can make combat more engaging for a modern audience. Side note, I think the game Blossom Tales went a long way toward more engaging combat just by not freezing the hero when they're attacking.

Yeah I totally agree. I loved ALTTP when I was a kid, but as I grew older I began to feel like it's a little too simple and forgiving... even combat in Zelda 1 and 2 is a lot more challenging IMO. There's a lot of different ways I'd like to build on the formula, though I know it will take time to implement everything. That's one of the reasons I wanted to at least implement a cooldown ASAP... until my AI is as challenging as i.e. Hyper Light Drifter, being able to dash constantly will likely feel OP and make things too easy.

I love the fact in Link's Awakening that almost everything is an assignable item, including stuff like the shield, gauntlets, and boots which were previously passive abilities. Being able to assign and "use" an item is inherently more fun and makes you appreciate it more, and it forces the player to make tough decisions about which items they want equipped at a given time. Your equipment menu also pretty much becomes your keybinding menu at that point, which potentially streamlines the configuration process.

That being said though, as you say it all depends on the style of game you're going for. For example if your game requires a ton of jumping, then at some point having a dedicated jump button as in Beyond Oasis or Landstalker is preferable to an assignable item like Zelda's feather. At that point being able to unassign such an important command could become a frustrating n00b trap rather than an interesting equipment choice. The same would be true of a crouch command if you need to frequently duck to avoid high attacks or kill short enemies, and of course if the player needs to dash away from enemy attacks constantly, it may not make sense to let players unassign their dash.

However for the moment, I haven't made any progress creating an assignable equipment menu either, so it's kind of a moot point... my dash command will definitely be a dedicated key for the foreseeable future.  ;)
#15
Your scripts / Re: Dodge/Dash/Roll Action
June 03, 2018, 09:21:09 PM
I got this working in my project over the weekend and it feels pretty nice: https://youtu.be/pLQduAnrbgI

One side effect I noticed is that the shield sprite appears to stay in the walking animation during the dash, instead of using an alternate dash animation. I added a blank dashing animation to my shield sprite and it definitely wasn't being called. Not the end of the world, but it's something people might have to work out depending on the poses in their hero animations.

I later added a cooldown timer to mine by changing a few lines, just so you can't just spam the dash constantly. It's pretty basic but here's what I did if it's helpful to anyone:

Code ( lua) Select

-- Add this somewhere OUTSIDE the "game:on_key_pressed" function (I put it right under game:start):

local can_dash = true

-- Then add it to the initial if/then statement:

if game:has_item("dandelion_charm") and effect == nil and state == "free" and game:is_suspended() == false and can_dash == true then

-- add this to the m:start like so:

      m:start(hero, function()
        hero:unfreeze()
        can_dash = false
        sol.timer.start(hero, 500, function()
          can_dash = true
        end)
      end)

-- and do the same to your on_obstacle_reached function:

function m:on_obstacle_reached()
        hero:unfreeze()
        sol.timer.start(hero, 500, function()
          can_dash = true
        end)
      end

-- obviously you can change "500" to any timing you want. You could also make the timing longer or shorter for on_obstacle_reached to either punish or forgive the player for slamming into a wall.


Eventually I'd like to make it use MP, convert it to an assignable item, and polish up the animations... but either way, it's great to have dash functionality in Solarus!