First playable advanced soccer ai
This commit is contained in:
parent
748051871c
commit
d2f26fe70d
@ -283,13 +283,13 @@ void AIBaseController::crashed(const Material *m)
|
|||||||
} // crashed(Material)
|
} // crashed(Material)
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
void AIBaseController::checkPosition(const Vec3 &point,
|
void AIBaseController::checkPosition(const Vec3 &point, posData *pos_data,
|
||||||
posData *pos_data,
|
Vec3 *lc, bool use_front_xyz) const
|
||||||
Vec3 *lc) const
|
|
||||||
{
|
{
|
||||||
// Convert to local coordinates from the point of view of current kart
|
// Convert to local coordinates from the point of view of current kart
|
||||||
btQuaternion q(btVector3(0, 1, 0), -m_kart->getHeading());
|
btQuaternion q(btVector3(0, 1, 0), -m_kart->getHeading());
|
||||||
Vec3 p = point - m_kart->getXYZ();
|
Vec3 p = point -
|
||||||
|
(use_front_xyz ? m_kart->getFrontXYZ() : m_kart->getXYZ());
|
||||||
Vec3 local_coordinates = quatRotate(q, p);
|
Vec3 local_coordinates = quatRotate(q, p);
|
||||||
|
|
||||||
// Save local coordinates for later use if needed
|
// Save local coordinates for later use if needed
|
||||||
|
@ -77,7 +77,9 @@ protected:
|
|||||||
/** This can be called to detect if the kart is stuck (i.e. repeatedly
|
/** This can be called to detect if the kart is stuck (i.e. repeatedly
|
||||||
* hitting part of the track). */
|
* hitting part of the track). */
|
||||||
bool isStuck() const { return m_stuck; }
|
bool isStuck() const { return m_stuck; }
|
||||||
void checkPosition(const Vec3&, posData*, Vec3* lc = NULL) const;
|
void checkPosition(const Vec3&, posData*,
|
||||||
|
Vec3* lc = NULL,
|
||||||
|
bool use_front_xyz = false) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AIBaseController(AbstractKart *kart);
|
AIBaseController(AbstractKart *kart);
|
||||||
|
@ -76,7 +76,10 @@ void ArenaAI::update(float dt)
|
|||||||
|
|
||||||
// Don't do anything if there is currently a kart animations shown.
|
// Don't do anything if there is currently a kart animations shown.
|
||||||
if (m_kart->getKartAnimation())
|
if (m_kart->getKartAnimation())
|
||||||
|
{
|
||||||
|
resetAfterStop();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isWaiting())
|
if (isWaiting())
|
||||||
{
|
{
|
||||||
@ -101,6 +104,7 @@ void ArenaAI::update(float dt)
|
|||||||
|
|
||||||
if (m_is_uturn)
|
if (m_is_uturn)
|
||||||
{
|
{
|
||||||
|
resetAfterStop();
|
||||||
handleArenaUTurn(dt);
|
handleArenaUTurn(dt);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -202,6 +206,7 @@ bool ArenaAI::handleArenaUnstuck(const float dt)
|
|||||||
{
|
{
|
||||||
if (!m_is_stuck || m_is_uturn) return false;
|
if (!m_is_stuck || m_is_uturn) return false;
|
||||||
|
|
||||||
|
resetAfterStop();
|
||||||
setSteering(0.0f, dt);
|
setSteering(0.0f, dt);
|
||||||
|
|
||||||
if (fabsf(m_kart->getSpeed()) >
|
if (fabsf(m_kart->getSpeed()) >
|
||||||
@ -263,7 +268,8 @@ void ArenaAI::handleArenaSteering(const float dt)
|
|||||||
|
|
||||||
checkPosition(m_target_point, &m_cur_kart_pos_data);
|
checkPosition(m_target_point, &m_cur_kart_pos_data);
|
||||||
#ifdef AI_DEBUG
|
#ifdef AI_DEBUG
|
||||||
if (m_path_corners.size() > 2)
|
m_debug_sphere->setPosition(m_path_corners[0].toIrrVector());
|
||||||
|
/*if (m_path_corners.size() > 2)
|
||||||
{
|
{
|
||||||
m_debug_sphere->setVisible(true);
|
m_debug_sphere->setVisible(true);
|
||||||
m_debug_sphere_next->setVisible(true);
|
m_debug_sphere_next->setVisible(true);
|
||||||
@ -274,7 +280,7 @@ void ArenaAI::handleArenaSteering(const float dt)
|
|||||||
{
|
{
|
||||||
m_debug_sphere->setVisible(false);
|
m_debug_sphere->setVisible(false);
|
||||||
m_debug_sphere_next->setVisible(false);
|
m_debug_sphere_next->setVisible(false);
|
||||||
}
|
}*/
|
||||||
#endif
|
#endif
|
||||||
if (m_cur_kart_pos_data.behind)
|
if (m_cur_kart_pos_data.behind)
|
||||||
{
|
{
|
||||||
|
@ -118,6 +118,7 @@ private:
|
|||||||
void stringPull(const Vec3&, const Vec3&);
|
void stringPull(const Vec3&, const Vec3&);
|
||||||
virtual int getCurrentNode() const = 0;
|
virtual int getCurrentNode() const = 0;
|
||||||
virtual bool isWaiting() const = 0;
|
virtual bool isWaiting() const = 0;
|
||||||
|
virtual void resetAfterStop() {};
|
||||||
virtual void findClosestKart(bool use_difficulty) = 0;
|
virtual void findClosestKart(bool use_difficulty) = 0;
|
||||||
virtual void findTarget() = 0;
|
virtual void findTarget() = 0;
|
||||||
virtual bool forceBraking() { return false; }
|
virtual bool forceBraking() { return false; }
|
||||||
|
@ -56,9 +56,9 @@ SoccerAI::SoccerAI(AbstractKart *kart)
|
|||||||
video::SColor red(128, 128, 0, 0);
|
video::SColor red(128, 128, 0, 0);
|
||||||
video::SColor blue(128, 0, 0, 128);
|
video::SColor blue(128, 0, 0, 128);
|
||||||
m_red_sphere = irr_driver->addSphere(1.0f, red);
|
m_red_sphere = irr_driver->addSphere(1.0f, red);
|
||||||
m_red_sphere->setVisible(true);
|
m_red_sphere->setVisible(false);
|
||||||
m_blue_sphere = irr_driver->addSphere(1.0f, blue);
|
m_blue_sphere = irr_driver->addSphere(1.0f, blue);
|
||||||
m_blue_sphere->setVisible(true);
|
m_blue_sphere->setVisible(false);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_world = dynamic_cast<SoccerWorld*>(World::getWorld());
|
m_world = dynamic_cast<SoccerWorld*>(World::getWorld());
|
||||||
@ -114,6 +114,7 @@ void SoccerAI::update(float dt)
|
|||||||
|
|
||||||
if (World::getWorld()->getPhase() == World::GOAL_PHASE)
|
if (World::getWorld()->getPhase() == World::GOAL_PHASE)
|
||||||
{
|
{
|
||||||
|
resetAfterStop();
|
||||||
m_controls->m_brake = false;
|
m_controls->m_brake = false;
|
||||||
m_controls->m_accel = 0.0f;
|
m_controls->m_accel = 0.0f;
|
||||||
AIBaseController::update(dt);
|
AIBaseController::update(dt);
|
||||||
@ -170,7 +171,10 @@ void SoccerAI::findTarget()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise do the same as in battle mode, attack other karts
|
// Always reset this flag,
|
||||||
|
// in case the ball chaser lost the ball somehow
|
||||||
|
m_overtake_ball = false;
|
||||||
|
|
||||||
if (m_kart->getPowerup()->getType() == PowerupManager::POWERUP_NOTHING &&
|
if (m_kart->getPowerup()->getType() == PowerupManager::POWERUP_NOTHING &&
|
||||||
m_kart->getAttachment()->getType() != Attachment::ATTACH_SWATTER)
|
m_kart->getAttachment()->getType() != Attachment::ATTACH_SWATTER)
|
||||||
{
|
{
|
||||||
@ -212,30 +216,80 @@ Vec3 SoccerAI::determineBallAimingPosition()
|
|||||||
posData aim_pos = {0};
|
posData aim_pos = {0};
|
||||||
Vec3 ball_lc;
|
Vec3 ball_lc;
|
||||||
Vec3 aim_lc;
|
Vec3 aim_lc;
|
||||||
checkPosition(orig_pos, &ball_pos);
|
checkPosition(orig_pos, &ball_pos, &ball_lc, true/*use_front_xyz*/);
|
||||||
checkPosition(orig_pos, &aim_pos, &aim_lc);
|
checkPosition(ball_aim_pos, &aim_pos, &aim_lc, true/*use_front_xyz*/);
|
||||||
|
|
||||||
// Too far from the ball,
|
// Too far from the ball,
|
||||||
// use path finding from arena ai to get close
|
// use path finding from arena ai to get close
|
||||||
if (ball_pos.distance > 6.0f) return ball_aim_pos;
|
// ie no extra braking is needed
|
||||||
|
if (aim_pos.distance > 6.0f) return ball_aim_pos;
|
||||||
|
|
||||||
const Vec3 dist_to_aim_point = m_kart->getFrontXYZ() - ball_aim_pos;
|
if (m_overtake_ball)
|
||||||
|
|
||||||
// Prevent lost control when steering with ball
|
|
||||||
const bool need_braking = ball_pos.angle > 0.1f &&
|
|
||||||
m_kart->getSpeed() > 9.0f && ball_pos.distance < 3.0f;
|
|
||||||
|
|
||||||
if (need_braking)
|
|
||||||
{
|
{
|
||||||
m_controls->m_brake = true;
|
// Check if the kart passed the ball already,
|
||||||
m_force_brake = true;
|
// If so aim the front side of ball
|
||||||
|
if (ball_pos.behind)
|
||||||
|
{
|
||||||
|
const Vec3& front_pos =
|
||||||
|
m_world->getBallAimPosition(m_opp_team, true/*reverse*/);
|
||||||
|
Vec3 d = front_pos - m_kart->getFrontXYZ();
|
||||||
|
if (d.length_2d() < (m_world->getBallDiameter() / 2))
|
||||||
|
{
|
||||||
|
// Almost arrive, reset
|
||||||
|
m_overtake_ball = false;
|
||||||
|
}
|
||||||
|
return front_pos;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Otherwise aim left/right depends on the side of ball
|
||||||
|
if (ball_pos.lhs)
|
||||||
|
{
|
||||||
|
return m_world->getBallTrans()
|
||||||
|
(Vec3(m_world->getBallDiameter(), 0, 0));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return m_world->getBallTrans()
|
||||||
|
(Vec3(-m_world->getBallDiameter(), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (dist_to_aim_point.length_2d() < 0.4f)
|
else
|
||||||
{
|
{
|
||||||
//Log::info("","%f",dist_to_aim_point.length_2d());
|
// Check whether the aim point is non-reachable
|
||||||
return m_world->getBallTrans()(Vec3(0, 0, 1));
|
// ie the ball is in front of the kart, which the aim position
|
||||||
|
// is behind the ball, in an almost straight line
|
||||||
|
// If so m_overtake_ball is true
|
||||||
|
if (aim_lc.z() > 0 && aim_lc.z() > ball_lc.z() &&
|
||||||
|
ball_pos.angle < 0.6f && aim_pos.angle < 0.2f)
|
||||||
|
{
|
||||||
|
m_overtake_ball = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use the aim position calculated by soccer world
|
||||||
|
// Prevent lost control when steering with ball
|
||||||
|
const bool need_braking = ball_pos.angle > 0.15f &&
|
||||||
|
m_kart->getSpeed() > 9.0f &&
|
||||||
|
ball_pos.distance < m_world->getBallDiameter();
|
||||||
|
|
||||||
|
if (need_braking)
|
||||||
|
{
|
||||||
|
m_controls->m_brake = true;
|
||||||
|
m_force_brake = true;
|
||||||
|
}
|
||||||
|
if (aim_pos.behind && aim_pos.distance <
|
||||||
|
(m_world->getBallDiameter() / 2))
|
||||||
|
{
|
||||||
|
// Reached aim point, aim forward
|
||||||
|
return m_world->getBallAimPosition(m_opp_team, true/*reverse*/);
|
||||||
|
}
|
||||||
|
return ball_aim_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make compiler happy
|
||||||
return ball_aim_pos;
|
return ball_aim_pos;
|
||||||
|
|
||||||
} // determineBallAimingPosition
|
} // determineBallAimingPosition
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
@ -57,6 +57,7 @@ private:
|
|||||||
|
|
||||||
virtual void findClosestKart(bool use_difficulty);
|
virtual void findClosestKart(bool use_difficulty);
|
||||||
virtual void findTarget();
|
virtual void findTarget();
|
||||||
|
virtual void resetAfterStop() OVERRIDE { m_overtake_ball = false; }
|
||||||
virtual int getCurrentNode() const;
|
virtual int getCurrentNode() const;
|
||||||
virtual bool isWaiting() const;
|
virtual bool isWaiting() const;
|
||||||
virtual bool canSkid(float steer_fraction) { return false; }
|
virtual bool canSkid(float steer_fraction) { return false; }
|
||||||
|
@ -97,7 +97,7 @@ void SoccerWorld::init()
|
|||||||
if (!m_ball)
|
if (!m_ball)
|
||||||
Log::fatal("SoccerWorld","Ball is missing in soccer field, abort.");
|
Log::fatal("SoccerWorld","Ball is missing in soccer field, abort.");
|
||||||
|
|
||||||
m_bgd.init();
|
m_bgd.init(m_ball->getPhysicalObject()->getRadius());
|
||||||
|
|
||||||
} // init
|
} // init
|
||||||
|
|
||||||
@ -423,9 +423,8 @@ void SoccerWorld::updateBallPosition(float dt)
|
|||||||
{
|
{
|
||||||
if (isRaceOver()) return;
|
if (isRaceOver()) return;
|
||||||
|
|
||||||
if (!(m_ball->getPhysicalObject()->getBody()
|
if (!(m_ball_body->getLinearVelocity().x() == 0.0f ||
|
||||||
->getLinearVelocity().x() == 0.0f || m_ball->getPhysicalObject()
|
m_ball_body->getLinearVelocity().z() == 0.0f))
|
||||||
->getBody()->getLinearVelocity().z() == 0.0f))
|
|
||||||
{
|
{
|
||||||
// Only update heading if the ball is moving
|
// Only update heading if the ball is moving
|
||||||
m_ball_heading = atan2f(m_ball_body->getLinearVelocity().getX(),
|
m_ball_heading = atan2f(m_ball_body->getLinearVelocity().getX(),
|
||||||
|
@ -78,6 +78,9 @@ private:
|
|||||||
{
|
{
|
||||||
// These data are used by AI to determine ball aiming angle
|
// These data are used by AI to determine ball aiming angle
|
||||||
private:
|
private:
|
||||||
|
// Radius of the ball
|
||||||
|
float m_radius;
|
||||||
|
|
||||||
// Slope of the line from ball to the center point of goals
|
// Slope of the line from ball to the center point of goals
|
||||||
float m_red_goal_slope;
|
float m_red_goal_slope;
|
||||||
float m_blue_goal_slope;
|
float m_blue_goal_slope;
|
||||||
@ -116,8 +119,16 @@ private:
|
|||||||
return m_trans;
|
return m_trans;
|
||||||
} // getTrans
|
} // getTrans
|
||||||
|
|
||||||
void init()
|
float getDiameter() const
|
||||||
{
|
{
|
||||||
|
return m_radius * 2;
|
||||||
|
} // getTrans
|
||||||
|
|
||||||
|
void init(float ball_radius)
|
||||||
|
{
|
||||||
|
m_radius = ball_radius;
|
||||||
|
assert(m_radius > 0.0f);
|
||||||
|
|
||||||
// Save two goals
|
// Save two goals
|
||||||
unsigned int n = CheckManager::get()->getCheckStructureCount();
|
unsigned int n = CheckManager::get()->getCheckStructureCount();
|
||||||
for (unsigned int i = 0; i < n; i++)
|
for (unsigned int i = 0; i < n; i++)
|
||||||
@ -191,7 +202,7 @@ private:
|
|||||||
return false;
|
return false;
|
||||||
} // isApproachingGoal
|
} // isApproachingGoal
|
||||||
|
|
||||||
Vec3 getAimPosition(SoccerTeam team) const
|
Vec3 getAimPosition(SoccerTeam team, bool reverse) const
|
||||||
{
|
{
|
||||||
// If it's likely to goal already, aim the ball straight behind
|
// If it's likely to goal already, aim the ball straight behind
|
||||||
// should do the job
|
// should do the job
|
||||||
@ -202,16 +213,17 @@ private:
|
|||||||
// This is done by using Pythagorean Theorem and solving the
|
// This is done by using Pythagorean Theorem and solving the
|
||||||
// equation from ball to goal center (y = (m_***_goal_slope) x)
|
// equation from ball to goal center (y = (m_***_goal_slope) x)
|
||||||
|
|
||||||
// We aim 1 unit behind the ball (easier to solve),
|
// We aim behind the ball from the center of the ball to its
|
||||||
// so 1 = sqrt (x2 + y2) and than x = sqrt (1 - y2)
|
// diameter, so 2*m_radius = sqrt (x2 + y2),
|
||||||
|
// which is next x = sqrt (2*m_radius - y2)
|
||||||
// And than we have x = y / m(m_***_goal_slope)
|
// And than we have x = y / m(m_***_goal_slope)
|
||||||
// After put that in the slope equation, we have
|
// After put that in the slope equation, we have
|
||||||
// y = sqrt(m2 / (1+m2))
|
// y = sqrt(2*m_radius*m2 / (1+m2))
|
||||||
float x = 0.0f;
|
float x = 0.0f;
|
||||||
float y = 0.0f;
|
float y = 0.0f;
|
||||||
if (team == SOCCER_TEAM_BLUE)
|
if (team == SOCCER_TEAM_BLUE)
|
||||||
{
|
{
|
||||||
y = sqrt((m_blue_goal_slope * m_blue_goal_slope) /
|
y = sqrt((m_blue_goal_slope * m_blue_goal_slope * m_radius*2) /
|
||||||
(1 + (m_blue_goal_slope * m_blue_goal_slope)));
|
(1 + (m_blue_goal_slope * m_blue_goal_slope)));
|
||||||
if (m_blue_goal_2.x() == 0.0f ||
|
if (m_blue_goal_2.x() == 0.0f ||
|
||||||
(m_blue_goal_2.x() > 0.0f && m_blue_goal_2.z() > 0.0f) ||
|
(m_blue_goal_2.x() > 0.0f && m_blue_goal_2.z() > 0.0f) ||
|
||||||
@ -224,7 +236,7 @@ private:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
y = sqrt((m_red_goal_slope * m_red_goal_slope) /
|
y = sqrt((m_red_goal_slope * m_red_goal_slope * m_radius*2) /
|
||||||
(1 + (m_red_goal_slope * m_red_goal_slope)));
|
(1 + (m_red_goal_slope * m_red_goal_slope)));
|
||||||
if (m_red_goal_2.x() == 0.0f ||
|
if (m_red_goal_2.x() == 0.0f ||
|
||||||
(m_red_goal_2.x() > 0.0f && m_red_goal_2.z() > 0.0f) ||
|
(m_red_goal_2.x() > 0.0f && m_red_goal_2.z() > 0.0f) ||
|
||||||
@ -237,7 +249,8 @@ private:
|
|||||||
assert (!std::isnan(x));
|
assert (!std::isnan(x));
|
||||||
assert (!std::isnan(y));
|
assert (!std::isnan(y));
|
||||||
// Return the world coordinates
|
// Return the world coordinates
|
||||||
return m_trans(Vec3(x, 0, y));
|
return (reverse ? m_trans(Vec3(-x, 0, -y)) :
|
||||||
|
m_trans(Vec3(x, 0, y)));
|
||||||
} // getAimPosition
|
} // getAimPosition
|
||||||
|
|
||||||
}; // BallGoalData
|
}; // BallGoalData
|
||||||
@ -344,29 +357,28 @@ public:
|
|||||||
}
|
}
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
int getKartNode(unsigned int kart_id) const
|
int getKartNode(unsigned int kart_id) const
|
||||||
{ return m_kart_on_node[kart_id]; }
|
{ return m_kart_on_node[kart_id]; }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
int getBallNode() const
|
int getBallNode() const
|
||||||
{ return m_ball_on_node; }
|
{ return m_ball_on_node; }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
const Vec3& getBallPosition() const
|
const Vec3& getBallPosition() const
|
||||||
{ return (Vec3&)m_ball_body->getCenterOfMassTransform().getOrigin(); }
|
{ return (Vec3&)m_ball_body->getCenterOfMassTransform().getOrigin(); }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
float getBallHeading() const
|
float getBallHeading() const
|
||||||
{ return m_ball_heading; }
|
{ return m_ball_heading; }
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
float getBallDiameter() const
|
||||||
|
{ return m_bgd.getDiameter(); }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
bool ballApproachingGoal(SoccerTeam team) const
|
bool ballApproachingGoal(SoccerTeam team) const
|
||||||
{
|
{ return m_bgd.isApproachingGoal(team); }
|
||||||
return m_bgd.isApproachingGoal(team);
|
|
||||||
}
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
Vec3 getBallAimPosition(SoccerTeam team) const
|
Vec3 getBallAimPosition(SoccerTeam team, bool reverse = false) const
|
||||||
{
|
{ return m_bgd.getAimPosition(team, reverse); }
|
||||||
return m_bgd.getAimPosition(team);
|
|
||||||
}
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
const btTransform& getBallTrans() const
|
const btTransform& getBallTrans() const
|
||||||
{ return m_bgd.getTrans(); }
|
{ return m_bgd.getTrans(); }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
bool isCorrectGoal(unsigned int kart_id, bool first_goal) const;
|
bool isCorrectGoal(unsigned int kart_id, bool first_goal) const;
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
@ -211,6 +211,8 @@ public:
|
|||||||
/** Add body to dynamic world */
|
/** Add body to dynamic world */
|
||||||
void addBody();
|
void addBody();
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
float getRadius() const { return m_radius; }
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
const std::string& getOnKartCollisionFunction() const { return m_on_kart_collision; }
|
const std::string& getOnKartCollisionFunction() const { return m_on_kart_collision; }
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
const std::string& getOnItemCollisionFunction() const { return m_on_item_collision; }
|
const std::string& getOnItemCollisionFunction() const { return m_on_item_collision; }
|
||||||
|
Loading…
Reference in New Issue
Block a user