Jump to content
  • entries
    15
  • comments
    50
  • views
    25,703

Ladders are evil, ugh...


Einlander

3,442 views

 Share

Ladders, Some people like them, some people don't. I like them, but I don't like making them. I just spent 9 straight hours after work to get the semblance of a half working ladder. The challenges were mind boggling for my sleep addled brain.

 

I initially started a few days ago. I wanted ladders for the game I had planned. Specifically with functionality similar to Counterstrike: Source and Left 4 Dead. So being the lazy good dev I am, I went to Youtube to see how people using Unity were accomplishing it. The first immediate thing I noticed was that they all used triggers/colliders to accomplish this goal. They used 3 features of their trigger/collider: OnEnter, OnStay, and OnExit.

 

I was shown a script where all of the actions were implemented, but I realized from the time I spent making sourcemod scripts that I would need to control every entity that touched my trigger, especially if I ever did multi-player. So I wrote a heavily modified collision trigger. OnEnter was not that hard, all that was is the very first instance that an entity collided with the trigger I call a function once, and add it to a list so it would not be called again. OnStay is just a duplicate of the Collision function in Leadwerks. OnExit was the spawn of satan. On exit tells you when an entity leaves the trigger. Well in Leadwerks when something leaves a trigger it's gone. You can have a list of entites that you populated but since the collision trigger can't tell you whats missing the list becomes useless. So I ended up creating a a list paging system in order to track what entity has left.

 

So I created 2 lists: old and new

and a timer : Timetillupdate

 

When an entity collides for the first time I set it's state to onenter.

When it collides a second time i change it's state to onstay

 

when timetillupdate runs out i check the old list against the new. If there is something missing in the new list, i set that entity to onexit. Then i page the new list into the old list, then empty the old

 

Seems simple enough, but it's my first time doing this, and Leadwerks is my first game engine where I have coded anything of note, so I probably went the long way round, and messed something up, nevertheless it functioned properly.

 

Now the ladder. After watching about 3 videos on YouTube,

This one showed that once you enter the trigger, you takeover the controls then transform the player up the ladder, simple enough. In Leadwerks I used translate to move the player up the ladder. It mostly worked, except that the player couldnt get to the top of the platform. After all sorts of unholy buggery I settled for a dirty trick I read a few places. When they enter the ladder zone, turn off the players gravity, when they get off the ladder, turn it back on. This solved my ladder issue for the most part.

 

When I get more time/experience I will fix the ladder so there are no gravity tricks,and the player can be able to stop mid climb and not slide back down. Or I could simply cop out and force people to press the use button to mount the ladder, and press use again to dismount.

 

Here is the result of my sleepless night:

 

  • Upvote 4
 Share

5 Comments


Recommended Comments

I haven't tested this but this is the idea of how you can handle multiple entities hitting a trigger:

 

-- this will store all entities that collide with this trigger so we can manage them
Script.entities = {}

function Script:UpdatePhysics()
  for i = 0, #self.entities do
     if self.entities[i].entered then
        if self.entities[i].hadCollision == false then
           if self.entities[i].exited == false then
              -- remove this entity from the table because they left the trigger. we can do whatever with the entity here also
              table.remove(self.entities, i)
              i = i + 1
           end
        end
     end

     self.entities[i].hadCollision = false
  end
end

function Script:FindEntity(e)
  for i = 0, #self.entities do
     if self.entities[i].entity == e then
        return self.entities[i].entity
     end
  end

  return nil
end

function Script:CreateEntityObject(e)
  local eObject = {}
  eObject.entity = e
  eObject.entered = false
  eObject.hadCollision = true
  eObject.exited = false

  return eObject
end

function Script:Collision(entity, position, normal, speed)
  -- see if this entity is already actively colliding and if so get it from our list
  local e = self:FindEntity(entity)

  -- this is a new entity if we can't find it in our list so add it to our list with it's vars
  if e == nil then
     e = entity

     -- this is a new entity so add it to our list
     self.entities[#self.entities + 1] = self:CreateEntityObject(entity)
  end

  -- check the entity that was passed in here which either exists already or is new and we just added it
  e.hadCollision = true

  if e.entered == false then
     e.entered = true
     e.exited = false
  end
end

  • Upvote 1
Link to comment

Hi Einlander,

 

This is working in C++ for my trigger class and gives me an onenter and onexit. The trigger saves every collided item to a list.

 

If the item isn't in the list it is onEnter then it is added to the list.

 

In the Physics update checks a value has been set on all objects in the list every loop, if it's not there it's onExit.

 

There is a lot of **** in my code which is not necessary as I did a straight copy paste, but hope it may give you an idea.

 

Cheers!

 

 

Trigger.h

 

struct CollisionItem
{
std::string   address = "";
bool    hasCollision = false;
};
class Trigger : public GameObject
{
public:

  std::list<CollisionItem*>   collisionList;
  Trigger(long type);
  ~Trigger();
  void EnterWorld(void);
  void Update(void);
  std::list<Trigger*>::iterator it;
  static std::list<Trigger*> List;

  static void UpdateEach(void);
  void AddCollision(std::string address);
  void RemoveCollision(std::string address);
  void Activate(BaseObject *trigger, BaseObject *activator);
  void Save(pugi::xml_node *xmlNode);

  void Load(pugi::xml_node *xmlNode);
};

 

 

 

Trigger.cpp

 

std::list<Trigger*> Trigger::List;
void TriggerCollision(Entity *entity0, Entity *entity1, float *position, float *normal, float *speed)
{
GameObject* object = (GameObject*)entity0->GetUserData();
if(object->GetBaseType() == kObjectTrigger)
{
 Trigger *trigger = static_cast<Trigger*>(object);
 if((trigger) && !trigger->IsDisabled())
 {
  //Find what the hit target is
  GameObject* hitObject = (GameObject*)entity1->GetUserData();
  bool found = false;
  //Check if item exists
  std::list<CollisionItem*>::iterator it;
  for (it = trigger->collisionList.begin(); it != trigger->collisionList.end(); it++)
  {
   CollisionItem *object = (CollisionItem*)(*it);
   if(object->address.compare(hitObject->GetAddress()) == 0)
   {
 object->hasCollision = true;
 found = true;
   }
  }
  //We havent found the hit object so call Entered
  if (!found)
  {
   trigger->AddCollision(hitObject->GetAddress());
   hitObject->OnEnter(trigger);
  }
  if(hitObject)
  {
   switch(trigger->GetType())
   {
 case kObjectDeathZoneTrigger:
 {
  trigger->Activate(trigger, hitObject);
  break;
 }
 case kObjectFoodZoneTrigger:
 {
  //trigger->Activate(trigger, hitObject);
  break;
 }
 case kObjectLevelLoadTrigger:
 {
   if(hitObject->GetBaseType() == kObjectCharacter)
  {
   GameCharacter *character = static_cast<GameCharacter*>(hitObject);
   if(character->GetGamePlayer() == TheGame->GetLocalPlayer())
   {
    trigger->Activate(trigger, hitObject);
   }
  }
  break;
 }
 default:
 {
  trigger->Activate(trigger, hitObject);
  trigger->Disable();
 }
   }
  }
 }
}
}
void TriggerUpdatePhysics(Entity *entity0, Entity *entity1, float *position, float *normal, float *speed)
{
GameObject* object = (GameObject*)entity0->GetUserData();
if (object->GetBaseType() == kObjectTrigger)
{
 Trigger *trigger = static_cast<Trigger*>(object);
 if ((trigger) && !trigger->IsDisabled())
 {
  //Check if item exists
  std::list<CollisionItem*>::iterator it;
  for (it = trigger->collisionList.begin(); it != trigger->collisionList.end()
  {
   CollisionItem *collItem = (CollisionItem*)(*it);
   if (!collItem->hasCollision)
   {
 GameObject *object = (GameObject*)TheGameWorld->FindObjectByAddress(collItem->address);
 object->OnExit(trigger);
 delete collItem;
 collItem = NULL;

 it = trigger->collisionList.erase(it);
   }
   else
   {
 collItem->hasCollision = false;
 ++it;
   }
  }
 }
}
}
Trigger::Trigger(long type) : GameObject(type)
{
   SetBaseType(kObjectTrigger);

List.push_front(this);
it = List.begin();
}
Trigger::~Trigger()
{
std::list<CollisionItem*>::iterator collit;
for (collit = collisionList.begin(); collit != collisionList.end(); collit++)
{
 CollisionItem *object = (CollisionItem*)(*collit);
 delete object;
 object = NULL;
}
collisionList.clear();
List.erase(it);
}
void Trigger::EnterWorld()
{
GameObject::EnterWorld();
//Material *material = Material::Load("Materials/Game/Trigger/triggerAlpha.mat");
//entity->SetMaterial(material);

entity->AddHook(Entity::CollisionHook, (void*)TriggerCollision);
entity->AddHook(Entity::UpdatePhysicsHook, (void*)TriggerUpdatePhysics);
}
void Trigger::Update()
{
GameObject::Update();
}
void Trigger::UpdateEach()
{
std::list<Trigger*>::iterator it;
for (it=List.begin(); it!=List.end(); it++)
{
 Trigger *object = (Trigger*)(*it);

 if(!(object->IsDisabled()))
  object->Update();
}
}
void Trigger::AddCollision(std::string address)
{
CollisionItem *item = new CollisionItem;
item->address = address;
item->hasCollision = true;
collisionList.push_front(item);
}
void Trigger::RemoveCollision(std::string address)
{
CollisionItem *item = new CollisionItem;
item->address = address;
collisionList.push_front(item);
}
void Trigger::Activate(BaseObject *trigger, BaseObject *activator)
{
GameObject::Activate(trigger, activator);
}
void Trigger::Save(pugi::xml_node *xmlNode)
{
GameObject::Save(xmlNode);

short index = 0;

std::list<CollisionItem*>::iterator it;
for (it = collisionList.begin(); it != collisionList.end(); it++)
{
 CollisionItem *object = (CollisionItem*)(*it);
 std::ostringstream oss;
 oss << "coll" << index;
 xmlNode->append_attribute(oss.str().c_str()) = object->address.c_str();
 oss << "has";
 xmlNode->append_attribute(oss.str().c_str()) = Utill::BoolToString(object->hasCollision).c_str();
 index++;
}
xmlNode->append_attribute("collInd") = index;
}
void Trigger::Load(pugi::xml_node *xmlNode)
{
GameObject::Load(xmlNode);
//rot.x = atof(xmlNode->attribute("rotx").value());
int index = atoi(xmlNode->attribute("collInd").value());

//Load link addresses
for (unsigned int i = 0; i < index; i++)
{
 ostringstream oss;
 oss << "coll" << i;
 CollisionItem *item = new CollisionItem();
 item->address = xmlNode->attribute(oss.str().c_str()).value();
 oss << "has";
 item->hasCollision = Utill::StringToBool(xmlNode->attribute(oss.str().c_str()).value());
}
}

  • Upvote 1
Link to comment

Thanks for the help, both your solutions are shorter and more elegant than what I came up with. Also less computationally intensive.

 

I used Ricks code. Here are the changes that i made:

 

-- this will store all entities that collide with this trigger so we can manage them
function Script:Start()
self.entities = {}
end
function Script:UpdatePhysics()

  for i = 1, #self.entities do --lua indexes start at 1 not 0 - einlander
         if self.entities[i].entered==true then
               if self.entities[i].hadCollision == false then   
                       if self.entities[i].exited == false then  
                          -- remove this entity from the table because they left the trigger. we can do whatever with the entity here also
     self:CollisionOnExit(self.entities[i].entity) --raise event [einlander]
                          table.remove(self.entities, i)
     break --[einlander for sanity reasons, and not to have to manually incriment the for next loop, finish it on the next run through the function]
                         -- i = i + 1 [commented out - einlander]
                       end
              end
         end
         self.entities[i].hadCollision = false
  end
end

function Script:FindEntity(e)
  for i = 1, #self.entities do --lua indexes start at 1 not 0 - einlander
         if self.entities[i].entity == e then
                return self.entities[i].entity
         end
  end
  return nil
end

function Script:CreateEntityObject(e)
  local eObject = {}
  eObject.entity = e
  eObject.entered = false
  eObject.hadCollision = true
  eObject.exited = false
  return eObject
end
function Script:Collision(entity, position, normal, speed)
  -- see if this entity is already actively colliding and if so get it from our list
  local e = self:FindEntity(entity)
  -- this is a new entity if we can't find it in our list so add it to our list with it's vars
  if e == nil then
         e = entity
         -- this is a new entity so add it to our list
         self.entities[#self.entities + 1] = self:CreateEntityObject(entity)
  self:CollisionOnEnter(entity, position, normal, speed) -- raise event   
  end
--[[ For what ever reason, this section did not work, I to assumed that lua passed things by reference, the internet supports this and documentation. Well not happening here, see next section.
  e.hadCollision = true
  if e.entered == false then
         e.entered = true
         e.exited = false
  end
]]--

-- check the entity that was passed in here which either exists already or is new and we just added it
for i=1 , #self.entities do -- manually find the object we created and set it's attributes [einlander]
 if self.entities[i].entity==entity then
  self.entities[i].hadCollision = true
  self:CollisionOnStay(entity, position, normal, speed) -- raise event
    if self.entities[i].entered == false then
   self.entities[i].entered = true
   self.entities[i].exited = false
    end
 end
end

end
-- Called ONCE when something touches trigger for the first time [einlander]
function Script:CollisionOnEnter(entity, position, normal, speed)
self.component:CallOutputs("CollisionOnEnter")
end
-- Called CONTINUOUSLY when something is in the trigger [einlander]
function Script:CollisionOnStay(entity, position, normal, speed)
self.component:CallOutputs("CollisionOnStay")
end
-- Called OONCE AFTER something leaves the trigger [einlander]
function Script:CollisionOnExit(entity)
self.component:CallOutputs("CollisionOnExit")
end

Link to comment

The collision stuff should be simple enough, my last post has the events needed to code actions for each situation. Ladders on the other hand, imo would require a more robust navmesh. The AI can move around and up and down slopes. But iirc, they cant bridge between separated navmesh areas such as a roof and the ground, unless the slope is gentle enough. So an AI controller would need to be made in conjunction with a compatible ladder script.

 

So making it work with AI is doable, but you will end up needing to replicate parts and systems more mature engines have.

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...