Elegantly coded entity behaviour

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
Garbeld
Prole
Posts: 10
Joined: Sun May 05, 2013 3:36 pm

Elegantly coded entity behaviour

Post by Garbeld »

I'm working on a platformer.

I've got it working, so far - you can run and jump around, and in one (long gone) iteration, you could hit stuff with a sword and be hit back.
My problem is that my current code is a rather painful-to-modify mess, both in lua and c++ - I'm afraid to implement anything new, because I don't know how much of the existing behaviour code is going to have to change just to add, say, attacking. I've thought about it, and read a lot about programming in general, but I've yet to understand how I can better organize the logic.

For reference, here's the code for my main character - it allows Pip to run animations, walk, jump, fall in four directions, and collide with objects, but not much else
(I've also copied it fairly directly into c++, aside from having implemented almost identical behaviour about eight times in the past, with minor variances, already)

pip.lua

Code: Select all

require "physo"
--phys[ics]o[bject], common behaviour for gravity- and inertia-influenced objects
require "animation"
--too lazy to learn preexisting animation libraries, don't know if they support the features I want

Pip = {}

Pip.po = po.new_physo()

Pip.po.siz_x = 6-1
Pip.po.siz_y = 8-1
-- subtract 1 from sizes to get expected behaviour from pos+siz collision detection
Pip.off_x = 13
Pip.off_y = 24
-- off[set]; I don't align my sprites to the top-left of their grids in my spritesheets, so they need to be drawn at an offset

Pip.flip = false

Pip.po.terminal_vel = 3.75
Pip.po.gravity = 0.125
Pip.po.gravity_dir = 'd'
--gravity_dir[ection]; so things can fall u[p], r[ight], d[own] or l[eft]
Pip.po.a_friction = 0.125
Pip.po.g_friction = 0.25

Pip.def_physics = po.new_physo(Pip.po)
--def[ault], essentially a set of const values for Pip's physics
Pip.jump_gravity = 1/12
Pip.wall_gravity = 0.25
--I think I was going to implement wall-grabbing, not sure what this is for
Pip.jump_strength = 2.25
Pip.a_accel = 0.15625
Pip.g_accel = 0.25
Pip.run_max = 1.875

Pip.state = {}
Pip.state.jumping = false
Pip.state.moving = false
--used to make sure that if you're holding both direction keys, he'll only move in the direction of the last one hit
Pip.state.grounded = false

Pip.anim_frame = 1
Pip.draw_frame = 1

Pip.anim = {}
Pip.anim.idle = a.new_animation({1})
Pip.anim.run = a.new_animation({1,1,1,2,2,2,2,2,2,2,1,1,1,3,3,3,3,3,3,3})
Pip.anim.jump = a.new_animation({17})

Pip.cur_anim = Pip.anim.idle

Pip.update = function()
  --po:is_blocked(dir): moves physo one pixel in the specified direction, tests for collisions, unmoves, returns test result
  -- state end tests
  if Pip.po:is_blocked('u') then
    Pip.state.jumping = false
  elseif Pip.po:is_blocked('d') then
    Pip.state.jumping = false
  end

  --engine.key_pressed/engine.key_released: keeps track of which keys were only pressed/released on that specific frame
  -- key presses
  if engine.key_pressed['z'] then
    if Pip.po:is_blocked('d') then
      Pip.po.vel_y = -Pip.jump_strength
      Pip.state.jumping = true
    end
  end
  if engine.key_pressed['left'] then
    Pip.state.moving = true
    Pip.flip = true
  end
  if engine.key_pressed['right'] then
    Pip.state.moving = true
    Pip.flip = false
  end

  -- key releases
  if engine.key_released['left'] then
    if love.keyboard.isDown('right') then
      Pip.flip = false
    else
      Pip.state.moving = false
    end
  end
  if engine.key_released['right'] then
    if love.keyboard.isDown('left') then
      Pip.flip = true
    else
      Pip.state.moving = false
    end
  end

  -- key helds
  if love.keyboard.isDown('z') then
  else
    Pip.state.jumping = false
  end

  -- state effects
  if Pip.state.moving then
    if Pip.po:is_blocked('d') then
      Pip.po.g_friction = 0
      if Pip.flip then
        if Pip.po.vel_x > -Pip.run_max then
          Pip.po.vel_x = Pip.po.vel_x-Pip.g_accel
          if Pip.po.vel_x < -Pip.run_max then
            Pip.po.vel_x = -Pip.run_max
          end
        end
      else
        if Pip.po.vel_x < Pip.run_max then
          Pip.po.vel_x = Pip.po.vel_x+Pip.g_accel
          if Pip.po.vel_x > Pip.run_max then
            Pip.po.vel_x = Pip.run_max
          end
        end
      end
    else
      Pip.po.a_friction = 0
      if Pip.flip then
        if Pip.po.vel_x > -Pip.run_max then
          Pip.po.vel_x = Pip.po.vel_x-Pip.a_accel
          if Pip.po.vel_x < -Pip.run_max then
            Pip.po.vel_x = -Pip.run_max
          end
        end
      else
        if Pip.po.vel_x < Pip.run_max then
          Pip.po.vel_x = Pip.po.vel_x+Pip.a_accel
          if Pip.po.vel_x > Pip.run_max then
            Pip.po.vel_x = Pip.run_max
          end
        end
      end
    end
  else
    Pip.po.g_friction = Pip.def_physics.g_friction
    Pip.po.a_friction = Pip.def_physics.a_friction
  end
  if Pip.state.jumping then
    Pip.po.gravity = Pip.jump_gravity
    if Pip.po.siz_y==8-1 then
      Pip.po:resize('d',-1)
    end
  else
    Pip.po.gravity = Pip.def_physics.gravity
    if Pip.po.siz_y==8-1-1 then
      Pip.po:resize('u',1)
    end
  end

  -- animation setting
  if Pip.state.moving then
    if Pip.po:is_blocked('d') then
      if Pip.cur_anim ~= Pip.anim.run then
        Pip.cur_anim = Pip.anim.run
        Pip.anim_frame = 1
      end
    else
      if Pip.cur_anim == Pip.anim.run then
        Pip.cur_anim = Pip.anim.idle
        Pip.anim_frame = 1
      end
    end
  else
    Pip.cur_anim = Pip.anim.idle
  end
  if Pip.state.jumping then
    if Pip.cur_anim ~= Pip.anim.jump then
      Pip.cur_anim = Pip.anim.jump
      Pip.anim_frame = 1
    end
  else
    if Pip.cur_anim == Pip.anim.jump then
      Pip.cur_anim = Pip.anim.idle
    end
  end

  -- non-ai
  Pip.anim_frame = Pip.anim_frame+1
  if Pip.anim_frame > Pip.cur_anim.length then
    Pip.cur_anim = Pip.cur_anim.next_anim
    Pip.anim_frame = 1
  end
  Pip.draw_frame = Pip.cur_anim.frames[Pip.anim_frame].i
  Pip.po:update()
end
physo.lua
(features remnants of an unusual commenting scheme that I dropped during later changes; it made code easier to understand, I think, but was itself very tedious to maintain, not to mention very inconsistent in how I handled indentation, branching, etc.)

Code: Select all

po = {}
PhysO = {}
PhysO.__index = PhysO

function po.new_physo(ref)
  local physo = {}
  setmetatable(physo,PhysO)

  if ref then
    physo.pos_x = ref.pos_x
    physo.pos_y = ref.pos_y
    physo.vel_x = ref.vel_x
    physo.vel_y = ref.vel_y
    physo.siz_x = ref.siz_x
    physo.siz_y = ref.siz_y

    physo.terminal_vel = ref.terminal_vel
    physo.gravity = ref.gravity
    physo.gravity_dir = ref.gravity_dir

    physo.a_friction = ref.a_friction
    physo.g_friction = ref.g_friction
  else
    physo.pos_x = 0
    physo.pos_y = 0
    physo.vel_x = 0
    physo.vel_y = 0
    physo.siz_x = 0
    physo.siz_y = 0

    physo.terminal_vel = 0
    physo.gravity = 0
    physo.gravity_dir = 'd'

    physo.a_friction = 0
    physo.g_friction = 0
  end

  return physo
end

function PhysO:update()
  if engine.is_colliding(self) then
    return
  end

  self.vel_x = self.vel_x+(self.gravity*po.block_ref[self.gravity_dir].x)
  self.vel_y = self.vel_y+(self.gravity*po.block_ref[self.gravity_dir].y)
  --block_ref can be found further down
  --  it's a shortcut so that I can refer to directions (left,leftup,up,upright,...), and turn them into numerical values (left is -1x,0y, leftup is -1x,-1y, up is 0x,-1y, ...)
  if self.gravity_dir=='l' then
    if self.vel_x < -self.terminal_vel then
      self.vel_x = -self.terminal_vel
    end
  elseif self.gravity_dir=='u' then
    if self.vel_y < -self.terminal_vel then
      self.vel_y = -self.terminal_vel
    end
  elseif self.gravity_dir=='r' then
    if self.vel_x > self.terminal_vel then
      self.vel_x = self.terminal_vel
    end
  elseif self.gravity_dir=='d' then
    if self.vel_y > self.terminal_vel then
      self.vel_y = self.terminal_vel
    end
  end

  --TODO
  --  test for gravity_dir; do not apply friction unless vel > terminal_vel

  --apply friction (horizontal)
  --  test if touching wall (0), determines ground_friction vs air_friction (g/a)
  --    test if vel_y is negative/positive (1,n/p)
  --    apply g_friction (2)
  --      ensure velocity does not pass 0 (3)
  --  else apply a_friction (2)
  --    ensure velocity does not pass 0 (3)
--if self:is_blocked(self.vel_x,0) then         -- 0g
--  if self.vel_y < 0 then                      -- 1gn
--    self.vel_y = self.vel_y + self.g_friction -- 2gn
--    if self.vel_y > 0 then                    -- 3gn
--      self.vel_y = 0                          -- 3gn
--    end
--  elseif self.vel_y > 0 then                  -- 1gp
--    self.vel_y = self.vel_y - self.g_friction -- 2gp
--    if self.vel_y < 0 then                    -- 3gp
--      self.vel_y = 0                          -- 3gp
--    end
--  end
--elseif self.vel_y < 0 then                    -- 1an
--  self.vel_y = self.vel_y + self.a_friction   -- 2an
--  if self.vel_y > 0 then                      -- 3an
--    self.vel_y = 0                            -- 3an
--  end
--elseif self.vel_y > 0 then                    -- 1ap
--  self.vel_y = self.vel_y - self.a_friction   -- 2ap
--  if self.vel_y < 0 then                      -- 3ap
--    self.vel_y = 0                            -- 3ap
--  end
--end

  --apply friction (vertical)
  --  see above
  if self:is_blocked(0,self.vel_y) then         -- 0g
    if self.vel_x < 0 then                      -- 1gn
      self.vel_x = self.vel_x + self.g_friction -- 2gn
      if self.vel_x > 0 then                    -- 3gn
        self.vel_x = 0                          -- 3gn
      end
    elseif self.vel_x > 0 then                  -- 1gp
      self.vel_x = self.vel_x - self.g_friction -- 2gp
      if self.vel_x < 0 then                    -- 3gp
        self.vel_x = 0                          -- 3gp
      end
    end
  elseif self.vel_x < 0 then                    -- 1an
    self.vel_x = self.vel_x + self.a_friction   -- 2an
    if self.vel_x > 0 then                      -- 3an
      self.vel_x = 0                            -- 3an
    end
  elseif self.vel_x > 0 then                    -- 1ap
    self.vel_x = self.vel_x - self.a_friction   -- 2ap
    if self.vel_x < 0 then                      -- 3ap
      self.vel_x = 0                            -- 3ap
    end
  end

  --apply velocity (horizontal)
  --  moves object according to x velocity, checks for collisions, and, if necessary, moves it backwards 1 pixel at a time until it is no longer colliding
  self.pos_x = self.pos_x + self.vel_x
  if engine.is_colliding(self) then
    if self.vel_x < 0 then
      while engine.is_colliding(self) do
        self.pos_x = self.pos_x + 1
      end
    elseif self.vel_x > 0 then
      while engine.is_colliding(self) do
        self.pos_x = self.pos_x - 1
      end
    end
    self.vel_x = 0
  end

  --apply velocity (vertical)
  --  see above
  self.pos_y = self.pos_y + self.vel_y
  if engine.is_colliding(self) then
    if self.vel_y < 0 then
      while engine.is_colliding(self) do
        self.pos_y = self.pos_y + 1
      end
    elseif self.vel_y > 0 then
      while engine.is_colliding(self) do
        self.pos_y = self.pos_y - 1
      end
    end
    self.vel_y = 0
  end

end

po.block_ref = {l ={x=-1,y= 0},
                ul={x=-1,y=-1},
                u ={x= 0,y=-1},
                ur={x= 1,y=-1},
                r ={x= 1,y= 0},
                dr={x= 1,y= 1},
                d ={x= 0,y= 1},
                dl={x=-1,y= 1}}

function PhysO:is_blocked(x,y)
  if type(x)=='number' then
    if x < 0 then
      if y < 0 then
        dir = 'ul'
      elseif y > 0 then
        dir = 'dl'
      else
        dir = 'l'
      end
    elseif x > 0 then
      if y < 0 then
        dir = 'ur'
      elseif y > 0 then
        dir = 'dr'
      else
        dir = 'r'
      end
    else
      if y < 0 then
        dur = 'u'
      elseif y > 0 then
        dir = 'd'
      else
        return false
      end
    end
  else
    dir = x
  end

  self.pos_x = self.pos_x + po.block_ref[dir].x
  self.pos_y = self.pos_y + po.block_ref[dir].y
  if engine.is_colliding(self) then
    self.pos_x = self.pos_x - po.block_ref[dir].x
    self.pos_y = self.pos_y - po.block_ref[dir].y
    return true
  else
    self.pos_x = self.pos_x - po.block_ref[dir].x
    self.pos_y = self.pos_y - po.block_ref[dir].y
    return false
  end
end

function PhysO:resize(dir,change)
  if dir=='l' then
    self.pos_x = self.pos_x-change
    self.siz_x = self.siz_x+change
    if change > 0 then
      while engine.is_colliding(self) do
        self.pos_x = self.pos_x+1
      end
    end
  elseif dir=='u' then
    self.pos_y = self.pos_y-change
    self.siz_y = self.siz_y+change
    if change > 0 then
      while engine.is_colliding(self) do
        self.pos_y = self.pos_y+1
      end
    end
  elseif dir=='r' then
    self.pos_x = self.pos_x+change
    self.siz_x = self.siz_x+change
    if change > 0 then
      while engine.is_colliding(self) do
        self.pos_x = self.pos_x-1
      end
    end
  else
    self.pos_y = self.pos_y+change
    self.siz_y = self.siz_y+change
    if change > 0 then
      while engine.is_colliding(self) do
        self.pos_y = self.pos_y-1
      end
    end
  end
end
...performing empirical studies on selection bias.
User avatar
ivan
Party member
Posts: 1915
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Elegantly coded entity behaviour

Post by ivan »

Hi! I assume you are programming a platformer game.
I don't consider myself a Lua "expert" but I've been programming for a few years so I can tell that you're coming from another language by looking at your code.
There are two things I would like to point out:

Firstly, your code is very strongly coupled in a sense that you're doing too many different things in "pip.lua".
What I mean is you have physics code in there, input code, states, etc...
A better approach (in my opinion) is to separate those different domains in different .lua files.

Secondly, I think it would be very helpful if you looked and tried to understand other people's Lua code.
For example, the "physio.lua" file is very hard to read.
In Lua you can do things like:

Code: Select all

    physo.pos_x = ref.pos_x or 0
Little things like that can make your code much shorter and more clear.
"local" variables can also make code easier to read and faster to execute.
Garbeld
Prole
Posts: 10
Joined: Sun May 05, 2013 3:36 pm

Re: Elegantly coded entity behaviour

Post by Garbeld »

I assume you are programming a platformer game.
Kind of explicitly said so. In my first sentence. :awesome:
I don't consider myself a Lua "expert" but I've been programming for a few years so I can tell that you're coming from another language by looking at your code.
I find that interesting (if not terribly surprising). Is there much to it beyond the couple syntactical details you pointed out?
Firstly, your code is very strongly coupled in a sense that you're doing too many different things in "pip.lua".
What I mean is you have physics code in there, input code, states, etc...
A better approach (in my opinion) is to separate those different domains in different .lua files.
Unfortunately, I'm not really sure how to do that. Right now, it's about as separated as I could find any order to, given that states and physics are both very mutually sensitive, and both are sensitive to input.
Secondly, I think it would be very helpful if you looked and tried to understand other people's Lua code.
Oh, I certainly do. After personal experimentation, it's my primary method of learning - I read lots of books and articles and discussions, yet tend to take away little from them but trivia.
That said, a lot of open source projects I run into, I have either failed entirely to comprehend, or are for games and projects which simply don't work (or don't work well) to begin with.

I thank you muchly for the response.

On an unrelated note, I believe that I shall try making a small series of practice games of simpler sorts than platforming.
...performing empirical studies on selection bias.
User avatar
ivan
Party member
Posts: 1915
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Elegantly coded entity behaviour

Post by ivan »

Garbeld wrote:Kind of explicitly said so. In my first sentence. :awesome:
Oops sorry I must have missed it.
Garbeld wrote:I find that interesting (if not terribly surprising). Is there much to it beyond the couple syntactical details you pointed out?
Sure. I think understanding tables is crucial because the table is the only available data structure. OO programming is also done in a very distinct way too. In terms of optimization, it's helpful to know how strings and the garbage collector work so that you don't create intermediate objects all the time. People will probably point out other peculiarities too, but generally speaking I feel that it's more of a question of understanding HOW the language works.
Garbeld wrote:Oh, I certainly do. After personal experimentation, it's my primary method of learning - I read lots of books and articles and discussions, yet tend to take away little from them but trivia.
That said, a lot of open source projects I run into, I have either failed entirely to comprehend, or are for games and projects which simply don't work (or don't work well) to begin with.
Cool. There's many libs out there that you can look at. kikito's "bump.lua" library is fairly simple and may give you some ideas about platformer physics.
Garbeld
Prole
Posts: 10
Joined: Sun May 05, 2013 3:36 pm

Re: Elegantly coded entity behaviour

Post by Garbeld »

Sure. I think understanding tables is crucial because the table is the only available data structure.
I'm thinking my question might have been misinterpreted - I was referring to your statement that "I can tell you're coming from another language by looking at your code".
Cool. There's many libs out there that you can look at. kikito's "bump.lua" library is fairly simple and may give you some ideas about platformer physics.
Well, physics themselves are of little concern to me. Mine work to a satisfactory level already.
I'll keep it in mind whenever I get back to coding (I've been rather lazy, lately) - I definitely need to work on separating my internal code and public interfaces, which is one thing I immediately notice bump does better.

I should note that I'm more comfortable with coding in c++ than in lua, and, in general, am looking for advice that applies to both languages (like the internal/public separation mentioned above).
...performing empirical studies on selection bias.
User avatar
ivan
Party member
Posts: 1915
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Elegantly coded entity behaviour

Post by ivan »

Garbeld wrote:I'm thinking my question might have been misinterpreted - I was referring to your statement that "I can tell you're coming from another language by looking at your code".
My bad. I think one obvious thing is that you don't use local variables in your code.
In C++ statements like object.index.foo are resolved to a hardware address at compile time.
Whereas in Lua the "." operator means a runtime table lookup by the virtual machine which is a little bit slower and makes the code longer.
Garbeld
Prole
Posts: 10
Joined: Sun May 05, 2013 3:36 pm

Re: Elegantly coded entity behaviour

Post by Garbeld »

My bad. I think one obvious thing is that you don't use local variables in your code.
Could you give some examples of where they might be relevant to my code?
As it is, in my posted code, I very rarely declare any variables, and with the exception of pip.po, similarly rarely perform table lookups more than one table deep (which I can't, off the top of my head, think of any way to optimize further - I never coded in lua much to begin with, I must admit).
...performing empirical studies on selection bias.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 2 guests