Ver código fonte

NKAI: object graph improved and optimized

Andrii Danylchenko 1 ano atrás
pai
commit
e66ceff154

+ 13 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -73,13 +73,25 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		}
 
 		if(objToVisit && !shouldVisit(ai->nullkiller.get(), path.targetHero, objToVisit))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
+#endif
 			continue;
+		}
 
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
+		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
+			&& (path.getTotalDanger() == 0 || path.turn() > 0)
+			&& path.exchangeCount > 1)
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Hero %s is SCOUT, chain used and no danger", path.targetHero->getNameTranslated());
+#endif
 			continue;
+		}
 
 		auto firstBlockedAction = path.getFirstBlockedAction();
 		if(firstBlockedAction)

+ 2 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -27,8 +27,8 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	const int BUCKET_COUNT = 5;
-	const int BUCKET_SIZE = 3;
+	const int BUCKET_COUNT = 3;
+	const int BUCKET_SIZE = 5;
 	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }

+ 8 - 3
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -19,14 +19,19 @@ namespace NKAI
 namespace AIPathfinding
 {
 	bool QuestAction::canAct(const AIPathNode * node) const
+	{
+		return canAct(node->actor->hero);
+	}
+
+	bool QuestAction::canAct(const CGHeroInstance * hero) const
 	{
 		if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
 		{
-			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
+			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
 		}
 
-		return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
-			|| questInfo.quest->checkQuest(node->actor->hero);
+		return questInfo.quest->activeForPlayers.count(hero->getOwner())
+			|| questInfo.quest->checkQuest(hero);
 	}
 
 	Goals::TSubgoal QuestAction::decompose(const CGHeroInstance * hero) const

+ 2 - 0
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -30,6 +30,8 @@ namespace AIPathfinding
 
 		bool canAct(const AIPathNode * node) const override;
 
+		bool canAct(const CGHeroInstance * hero) const;
+
 		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
 		void execute(const CGHeroInstance * hero) const override;

+ 288 - 38
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -15,10 +15,18 @@
 #include "../../../lib/mapping/CMap.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/logging/VisualLogger.h"
+#include "Actions/QuestAction.h"
 
 namespace NKAI
 {
 
+struct ConnectionCostInfo
+{
+	float totalCost = 0;
+	float avg = 0;
+	int connectionsCount = 0;
+};
+
 class ObjectGraphCalculator
 {
 private:
@@ -34,10 +42,14 @@ private:
 public:
 	ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
 		:ai(ai), target(target)
+	{
+	}
+
+	void setGraphObjects()
 	{
 		for(auto obj : ai->memory->visitableObjs)
 		{
-			if(obj && obj->isVisitable() && obj->ID != Obj::HERO)
+			if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
 			{
 				addObjectActor(obj);
 			}
@@ -47,7 +59,70 @@ public:
 		{
 			addObjectActor(town);
 		}
+	}
+
+	void calculateConnections()
+	{
+		updatePaths();
+
+		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+			{
+				calculateConnections(pos);
+			});
+
+		removeExtraConnections();
+	}
+
+	void addMinimalDistanceJunctions()
+	{
+		foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
+			{
+				if(target->hasNodeAt(pos))
+					return;
+
+				if(ai->cb->getGuardingCreaturePosition(pos).valid())
+					return;
 
+				ConnectionCostInfo currentCost = getConnectionsCost(pos);
+
+				if(currentCost.connectionsCount <= 2)
+					return;
+
+				float neighborCost = currentCost.avg + 0.001f;
+
+				if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+				{
+					logAi->trace("Checking junction %s", pos.toString());
+				}
+
+				foreach_neighbour(
+					ai->cb.get(),
+					pos,
+					[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+					{
+						auto costTotal = this->getConnectionsCost(neighbor);
+
+						if(costTotal.avg < neighborCost)
+						{
+							neighborCost = costTotal.avg;
+
+							if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+							{
+								logAi->trace("Better node found at %s", neighbor.toString());
+							}
+						}
+					});
+
+				if(currentCost.avg < neighborCost)
+				{
+					addJunctionActor(pos);
+				}
+			});
+	}
+
+private:
+	void updatePaths()
+	{
 		PathfinderSettings ps;
 
 		ps.mainTurnDistanceLimit = 5;
@@ -59,11 +134,31 @@ public:
 
 	void calculateConnections(const int3 & pos)
 	{
-		auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid();
+		if(target->hasNodeAt(pos))
+		{
+			foreach_neighbour(
+				ai->cb.get(),
+				pos,
+				[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
+				{
+					if(target->hasNodeAt(neighbor))
+					{
+						auto paths = ai->pathfinder->getPathInfo(neighbor);
+
+						for(auto & path : paths)
+						{
+							if(pos == path.targetHero->visitablePos())
+							{
+								target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
+							}
+						}
+					}
+				});
 
-		if(guarded)
 			return;
+		}
 
+		auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
 		auto paths = ai->pathfinder->getPathInfo(pos);
 
 		for(AIPath & path1 : paths)
@@ -73,25 +168,32 @@ public:
 				if(path1.targetHero == path2.targetHero)
 					continue;
 
+				auto pos1 = path1.targetHero->visitablePos();
+				auto pos2 = path2.targetHero->visitablePos();
+
+				if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
+					continue;
+
 				auto obj1 = actorObjectMap[path1.targetHero];
 				auto obj2 = actorObjectMap[path2.targetHero];
 
-				auto tile1 = cb->getTile(obj1->visitablePos());
-				auto tile2 = cb->getTile(obj2->visitablePos());
+				auto tile1 = cb->getTile(pos1);
+				auto tile2 = cb->getTile(pos2);
 
 				if(tile2->isWater() && !tile1->isWater())
 				{
-					auto linkTile = cb->getTile(pos);
+					if(!cb->getTile(pos)->isWater())
+						continue;
 
-					if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD)
+					if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
 						continue;
 				}
 
-				auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true);
+				auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
 
 				auto updated = target->tryAddConnection(
-					obj1->visitablePos(),
-					obj2->visitablePos(), 
+					pos1,
+					pos2,
 					path1.movementCost() + path2.movementCost(),
 					danger);
 
@@ -99,8 +201,8 @@ public:
 				{
 					logAi->trace(
 						"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
-						obj1->getObjectName(), obj1->visitablePos().toString(),
-						obj2->getObjectName(), obj2->visitablePos().toString(),
+						obj1 ? obj1->getObjectName() : "J", pos1.toString(),
+						obj2 ? obj2->getObjectName() : "J", pos2.toString(),
 						pos.toString(),
 						path1.movementCost() + path2.movementCost());
 				}
@@ -108,7 +210,49 @@ public:
 		}
 	}
 
-private:
+	bool isExtraConnection(float direct, float side1, float side2) const
+	{
+		float sideRatio = (side1 + side2) / direct;
+
+		return sideRatio < 1.25f && direct > side1 && direct > side2;
+	}
+
+	void removeExtraConnections()
+	{
+		std::vector<std::pair<int3, int3>> connectionsToRemove;
+
+		for(auto & actor : temporaryActorHeroes)
+		{
+			auto pos = actor->visitablePos();
+			auto & currentNode = target->getNode(pos);
+
+			target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
+				{
+					target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
+						{
+							auto direct = currentNode.connections.find(n2);
+
+							if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
+							{
+								connectionsToRemove.push_back({pos, n2});
+							}
+						});
+				});
+		}
+
+		vstd::removeDuplicates(connectionsToRemove);
+
+		for(auto & c : connectionsToRemove)
+		{
+			target->removeConnection(c.first, c.second);
+
+			if(NKAI_GRAPH_TRACE_LEVEL >= 2)
+			{
+				logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
+			}
+		}
+	}
+
 	void addObjectActor(const CGObjectInstance * obj)
 	{
 		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
@@ -126,11 +270,77 @@ private:
 			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
 		}
 
+		assert(objectActor->visitablePos() == visitablePos);
+
 		actorObjectMap[objectActor] = obj;
 		actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
 
 		target->addObject(obj);
-	};
+	}
+
+	void addJunctionActor(const int3 & visitablePos)
+	{
+		auto internalCb = temporaryActorHeroes.front()->cb;
+		auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
+
+		CRandomGenerator rng;
+
+		objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
+		objectActor->initHero(rng, static_cast<HeroTypeID>(0));
+		objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
+		objectActor->initObj(rng);
+
+		if(cb->getTile(visitablePos)->isWater())
+		{
+			objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
+		}
+
+		assert(objectActor->visitablePos() == visitablePos);
+
+		actorObjectMap[objectActor] = nullptr;
+		actors[objectActor] = HeroRole::SCOUT;
+
+		target->registerJunction(visitablePos);
+	}
+
+	ConnectionCostInfo getConnectionsCost(const int3 & pos) const
+	{
+		auto paths = ai->pathfinder->getPathInfo(pos);
+		std::map<int3, float> costs;
+
+		for(auto & path : paths)
+		{
+			auto fromPos = path.targetHero->visitablePos();
+			auto cost = costs.find(fromPos);
+			
+			if(cost == costs.end())
+			{
+				costs.emplace(fromPos, path.movementCost());
+			}
+			else
+			{
+				if(path.movementCost() < cost->second)
+				{
+					costs[fromPos] = path.movementCost();
+				}
+			}
+		}
+
+		ConnectionCostInfo result;
+
+		for(auto & cost : costs)
+		{
+			result.totalCost += cost.second;
+			result.connectionsCount++;
+		}
+
+		if(result.connectionsCount)
+		{
+			result.avg = result.totalCost / result.connectionsCount;
+		}
+
+		return result;
+	}
 };
 
 bool ObjectGraph::tryAddConnection(
@@ -142,23 +352,24 @@ bool ObjectGraph::tryAddConnection(
 	return nodes[from].connections[to].update(cost, danger);
 }
 
+void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
+{
+	nodes[from].connections.erase(to);
+}
+
 void ObjectGraph::updateGraph(const Nullkiller * ai)
 {
 	auto cb = ai->cb;
 
 	ObjectGraphCalculator calculator(this, ai);
 
-	foreach_tile_pos(cb.get(), [this, &calculator](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
-		{
-			if(nodes.find(pos) != nodes.end())
-				return;
-
-			calculator.calculateConnections(pos);
-		});
+	calculator.setGraphObjects();
+	calculator.calculateConnections();
+	calculator.addMinimalDistanceJunctions();
+	calculator.calculateConnections();
 
 	if(NKAI_GRAPH_TRACE_LEVEL >= 1)
 		dumpToLog("graph");
-
 }
 
 void ObjectGraph::addObject(const CGObjectInstance * obj)
@@ -166,6 +377,11 @@ void ObjectGraph::addObject(const CGObjectInstance * obj)
 	nodes[obj->visitablePos()].init(obj);
 }
 
+void ObjectGraph::registerJunction(const int3 & pos)
+{
+	nodes[pos].initJunction();
+}
+
 void ObjectGraph::removeObject(const CGObjectInstance * obj)
 {
 	nodes[obj->visitablePos()].objectExists = false;
@@ -198,6 +414,9 @@ void ObjectGraph::connectHeroes(const Nullkiller * ai)
 
 		for(AIPath & path : paths)
 		{
+			if(path.getFirstBlockedAction())
+				continue;
+
 			auto heroPos = path.targetHero->visitablePos();
 
 			nodes[pos].connections[heroPos].update(
@@ -259,20 +478,55 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
 		GraphPathNodePointer pos = pq.top();
 		pq.pop();
 
-		auto & node = getNode(pos);
+		auto & node = getOrCreateNode(pos);
+		std::shared_ptr<SpecialAction> transitionAction;
+
+		if(node.obj)
+		{
+			if(node.obj->ID == Obj::QUEST_GUARD
+				|| node.obj->ID == Obj::BORDERGUARD
+				|| node.obj->ID == Obj::BORDER_GATE)
+			{
+				auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
+				auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
+
+				if(node.obj->ID == Obj::QUEST_GUARD
+					&& questObj->quest->mission == Rewardable::Limiter{}
+					&& questObj->quest->killTarget == ObjectInstanceID::NONE)
+				{
+					continue;
+				}
+
+				auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
+
+				if(!questAction->canAct(targetHero))
+				{
+					transitionAction = questAction;
+				}
+			}
+		}
 
 		node.isInQueue = false;
 
-		graph.iterateConnections(pos.coord, [&](int3 target, ObjectLink o)
+		graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
 			{
-				auto targetNodeType = o.danger ? GrapthPathNodeType::BATTLE : pos.nodeType;
+				auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
 				auto targetPointer = GraphPathNodePointer(target, targetNodeType);
-				auto & targetNode = getNode(targetPointer);
+				auto & targetNode = getOrCreateNode(targetPointer);
 
 				if(targetNode.tryUpdate(pos, node, o))
 				{
-					if(graph.getNode(target).objTypeID == Obj::HERO)
-						return;
+					targetNode.specialAction = transitionAction;
+
+					auto targetGraphNode = graph.getNode(target);
+
+					if(targetGraphNode.objID.hasValue())
+					{
+						targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
+
+						if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
+							return;
+					}
 
 					if(targetNode.isInQueue)
 					{
@@ -321,11 +575,6 @@ bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathN
 
 	if(newCost < cost)
 	{
-		if(nodeType < pos.nodeType)
-		{
-			logAi->error("Linking error");
-		}
-
 		previous = pos;
 		danger = prev.danger + link.danger;
 		cost = newCost;
@@ -348,7 +597,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 		if(!node.reachable())
 			continue;
 
-		std::vector<int3> tilesToPass;
+		std::vector<GraphPathNodePointer> tilesToPass;
 
 		uint64_t danger = node.danger;
 		float cost = node.cost;
@@ -372,7 +621,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 			vstd::amax(danger, currentNode.danger);
 			vstd::amax(cost, currentNode.cost);
 
-			tilesToPass.push_back(current.coord);
+			tilesToPass.push_back(current);
 
 			if(currentNode.cost < 2.0f)
 				break;
@@ -383,7 +632,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 		if(tilesToPass.empty())
 			continue;
 
-		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back());
+		auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
 
 		for(auto & path : entryPaths)
 		{
@@ -394,12 +643,13 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
 			{
 				AIPathNodeInfo n;
 
-				n.coord = *graphTile;
+				n.coord = graphTile->coord;
 				n.cost = cost;
 				n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
 				n.danger = danger;
 				n.targetHero = hero;
-				n.parentIndex = 0;
+				n.parentIndex = -1;
+				n.specialAction = getNode(*graphTile).specialAction;
 
 				for(auto & node : path.nodes)
 				{

+ 24 - 2
AI/Nullkiller/Pathfinding/ObjectGraph.h

@@ -50,6 +50,13 @@ struct ObjectNode
 		objID = obj->id;
 		objTypeID = obj->ID;
 	}
+
+	void initJunction()
+	{
+		objectExists = false;
+		objID = ObjectInstanceID();
+		objTypeID = Obj();
+	}
 };
 
 class ObjectGraph
@@ -59,8 +66,11 @@ class ObjectGraph
 public:
 	void updateGraph(const Nullkiller * ai);
 	void addObject(const CGObjectInstance * obj);
+	void registerJunction(const int3 & pos);
 	void connectHeroes(const Nullkiller * ai);
 	void removeObject(const CGObjectInstance * obj);
+	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
+	void removeConnection(const int3 & from, const int3 & to);
 	void dumpToLog(std::string visualKey) const;
 
 	template<typename Func>
@@ -77,7 +87,10 @@ public:
 		return nodes.at(tile);
 	}
 
-	bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
+	bool hasNodeAt(const int3 & tile) const
+	{
+		return vstd::contains(nodes, tile);
+	}
 };
 
 struct GraphPathNode;
@@ -131,6 +144,8 @@ struct GraphPathNode
 	GraphPathNodePointer previous;
 	float cost = BAD_COST;
 	uint64_t danger = 0;
+	const CGObjectInstance * obj = nullptr;
+	std::shared_ptr<SpecialAction> specialAction;
 
 	using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
 
@@ -157,7 +172,7 @@ public:
 	void dumpToLog() const;
 
 private:
-	GraphPathNode & getNode(const GraphPathNodePointer & pos)
+	GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
 	{
 		auto & node = pathNodes[pos.coord][pos.nodeType];
 
@@ -165,6 +180,13 @@ private:
 
 		return node;
 	}
+
+	const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
+	{
+		auto & node = pathNodes.at(pos.coord)[pos.nodeType];
+
+		return node;
+	}
 };
 
 }

+ 19 - 0
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -42,15 +42,34 @@ namespace AIPathfinding
 		}
 
 		auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
+
 		if(blocker == BlockingReason::NONE)
 		{
 			destination.blocked = nodeStorage->isDistanceLimitReached(source, destination);
 
+			if(destination.nodeObject
+				&& !destination.blocked
+				&& !allowBypassObjects
+				&& !dynamic_cast<const CGTeleport *>(destination.nodeObject)
+				&& destination.nodeObject->ID != Obj::EVENT)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
 			return;
 		}
 		
 		if(!allowBypassObjects)
+		{
+			if(destination.nodeObject)
+			{
+				destination.blocked = true;
+				destination.node->locked = true;
+			}
+
 			return;
+		}
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp

@@ -49,7 +49,7 @@ namespace AIPathfinding
 					return;
 
 				// when actor represents moster graph node, we need to let him escape monster
-				if(!destination.guarded && cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
+				if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
 					return;
 			}
 

+ 5 - 0
client/mapView/MapView.cpp

@@ -71,6 +71,8 @@ public:
 
 	virtual void drawLine(int3 start, int3 end) override
 	{
+		const Point offset = Point(30, 30);
+
 		auto level = model->getLevel();
 
 		if(start.z != level || end.z != level)
@@ -83,6 +85,9 @@ public:
 		pStart.x += 3;
 		pEnd.x -= 3;
 
+		pStart += offset;
+		pEnd += offset;
+
 		if(viewPort.isInside(pStart) && viewPort.isInside(pEnd))
 		{
 			target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0));