diff --git a/source/core/TaskManager.cpp b/source/core/TaskManager.cpp
index 0581fac28f72fd17da7a92dd1620f1eb46d3df8e..b63dd5ee1574aee7a2ade0a761eb690ffbc3c441 100644
--- a/source/core/TaskManager.cpp
+++ b/source/core/TaskManager.cpp
@@ -115,7 +115,7 @@ void CTaskManager::forceSinglethreaded()
 void CTaskManager::addTask(TaskPtr task, std::chrono::steady_clock::time_point tpWhen)
 {
 	{
-		ScopedSpinLock lock(m_slShedulerArray);
+		ScopedLock lock(m_mutexSchedulerThread);
 		m_aScheduled.insert({task, tpWhen}, [](const ScheduledTask &a, const ScheduledTask &b){
 			return(a.tpRunAt > b.tpRunAt);
 		});
@@ -360,26 +360,25 @@ void CTaskManager::schedulerMain()
 	while(m_isRunning)
 	{
 		tpNow = tpWaitUntil = std::chrono::steady_clock::now();
+
+		std::unique_lock<std::mutex> lock(m_mutexSchedulerThread);
+		for(UINT i = 0, l = m_aScheduled.size(); i < l; ++i)
 		{
-			ScopedSpinLock lock(m_slShedulerArray);
-			for(UINT i = 0, l = m_aScheduled.size(); i < l; ++i)
-			{
-				pScheduledTask = &m_aScheduled[i];
+			pScheduledTask = &m_aScheduled[i];
 
-				if(pScheduledTask->tpRunAt <= tpNow)
-				{
-					addTask(pScheduledTask->pTask);
-					m_aScheduled.erase(i);
-					--i; --l;
-				}
-				else
-				{
-					tpWaitUntil = pScheduledTask->tpRunAt;
-					break;
-				}
+			if(pScheduledTask->tpRunAt <= tpNow)
+			{
+				addTask(pScheduledTask->pTask);
+				m_aScheduled.erase(i);
+				--i; --l;
+			}
+			else
+			{
+				tpWaitUntil = pScheduledTask->tpRunAt;
+				break;
 			}
 		}
-		std::unique_lock<std::mutex> lock(m_mutexSchedulerThread);
+
 		if(tpWaitUntil != tpNow)
 		{
 			m_ConditionSchedulerThread.wait_until(lock, tpWaitUntil);
diff --git a/source/core/TaskManager.h b/source/core/TaskManager.h
index 1f4e98d378290581f2831c4ed26942ba0cb64943..0a312fdf23bdd61caac5b877a7319b8f95233217 100644
--- a/source/core/TaskManager.h
+++ b/source/core/TaskManager.h
@@ -93,7 +93,6 @@ private:
 	mutable mutex m_mutexFor;
 	mutable mutex m_mutexIOThread;
 	mutable mutex m_mutexSchedulerThread;
-	mutable SpinLock m_slShedulerArray;
 	Condition m_Condition;
 	Condition m_ConditionIOThread;
 	Condition m_ConditionSchedulerThread;
diff --git a/source/game/BaseTrigger.cpp b/source/game/BaseTrigger.cpp
index 6c057ad3351f40b17b47052eb870da0bbc9bc9a5..3d23d5f18d63ef089cb2eebb4583c096bd1d473e 100644
--- a/source/game/BaseTrigger.cpp
+++ b/source/game/BaseTrigger.cpp
@@ -30,19 +30,26 @@ END_PROPTABLE()
 
 REGISTER_ENTITY(CBaseTrigger, trigger);
 
+IEventChannel<XEventPhysicsStep> *CBaseTrigger::m_pTickEventChannel = NULL;
+
+CBaseTrigger::CBaseTrigger():
+	m_physicsTicker(this)
+{
+	if(!m_pTickEventChannel)
+	{
+		m_pTickEventChannel = Core_GetIXCore()->getEventChannel<XEventPhysicsStep>(EVENT_PHYSICS_STEP_GUID);
+	}
+}
+
 CBaseTrigger::~CBaseTrigger()
 {
 	removePhysBody();
-	CLEAR_INTERVAL(m_idUpdateInterval);
 }
 
 void CBaseTrigger::onPostLoad()
 {
 	BaseClass::onPostLoad();
 
-	assert(!ID_VALID(m_idUpdateInterval));
-	m_idUpdateInterval = SET_INTERVAL(update, 0);
-
 	if(m_pModel)
 	{
 		//m_pModel->enable(false);
@@ -55,10 +62,10 @@ void CBaseTrigger::enable()
 	if(!m_bEnabled)
 	{
 		m_bEnabled = true;
-		m_idUpdateInterval = SET_INTERVAL(update, 0);
 		if(m_pGhostObject)
 		{
 			SPhysics_GetDynWorld()->addCollisionObject(m_pGhostObject, CG_TRIGGER, CG_CHARACTER);
+			m_pTickEventChannel->addListener(&m_physicsTicker);
 		}
 	}
 }
@@ -67,9 +74,9 @@ void CBaseTrigger::disable()
 	if(m_bEnabled)
 	{
 		m_bEnabled = false;
-		CLEAR_INTERVAL(m_idUpdateInterval);
 		if(m_pGhostObject)
 		{
+			m_pTickEventChannel->removeListener(&m_physicsTicker);
 			SPhysics_GetDynWorld()->removeCollisionObject(m_pGhostObject);
 		}
 	}
@@ -86,15 +93,15 @@ void CBaseTrigger::toggle()
 	}
 }
 
-void CBaseTrigger::inEnable(inputdata_t * pInputdata)
+void CBaseTrigger::inEnable(inputdata_t *pInputdata)
 {
 	enable();
 }
-void CBaseTrigger::inDisable(inputdata_t * pInputdata)
+void CBaseTrigger::inDisable(inputdata_t *pInputdata)
 {
 	disable();
 }
-void CBaseTrigger::inToggle(inputdata_t * pInputdata)
+void CBaseTrigger::inToggle(inputdata_t *pInputdata)
 {
 	toggle();
 }
@@ -113,6 +120,7 @@ void CBaseTrigger::createPhysBody()
 		m_pGhostObject->setCollisionFlags(m_pGhostObject->getCollisionFlags() ^ btCollisionObject::CF_NO_CONTACT_RESPONSE);
 
 		SPhysics_GetDynWorld()->addCollisionObject(m_pGhostObject, CG_TRIGGER, CG_CHARACTER);
+		m_pTickEventChannel->addListener(&m_physicsTicker);
 	}
 }
 
@@ -121,6 +129,7 @@ void CBaseTrigger::removePhysBody()
 	if(m_pGhostObject)
 	{
 		SPhysics_GetDynWorld()->removeCollisionObject(m_pGhostObject);
+		m_pTickEventChannel->removeListener(&m_physicsTicker);
 		mem_delete(m_pGhostObject);
 	}
 }
@@ -138,16 +147,17 @@ void CBaseTrigger::onTouchEndAll(CBaseEntity *pActivator)
 	FIRE_OUTPUT(m_onTouchEndAll, pActivator);
 }
 
-void CBaseTrigger::onSync()
+void CBaseTrigger::onPhysicsStep()
 {
-	BaseClass::onSync();
-
-	static const bool *dev_show_triggers = GET_PCVAR_BOOL("dev_show_triggers");
-
-	if(m_pModel && *dev_show_triggers != m_isModelEnabled)
 	{
-		m_isModelEnabled = *dev_show_triggers;
-		m_pModel->enable(m_isModelEnabled);
+		// TODO move this!
+		static const bool *dev_show_triggers = GET_PCVAR_BOOL("dev_show_triggers");
+
+		if(m_pModel && *dev_show_triggers != m_isModelEnabled)
+		{
+			m_isModelEnabled = *dev_show_triggers;
+			m_pModel->enable(m_isModelEnabled);
+		}
 	}
 
 	if(!m_pGhostObject || !m_bEnabled)
@@ -190,17 +200,12 @@ void CBaseTrigger::onSync()
 		}
 	}
 	//m_pGhostObject->getOverlappingObject(0);
-	//printf("%d\n", m_iTouches);
+
+	update();
 }
 
-void CBaseTrigger::update(float dt)
+void CBaseTrigger::update()
 {
-	if(!m_bEnabled)
-	{
-		return;
-	}
-
-	
 	bool bFound;
 	for(int i = 0, l = m_aNewTouches.size(); i < l; ++i)
 	{
@@ -209,7 +214,8 @@ void CBaseTrigger::update(float dt)
 		{
 			if(m_aNewTouches[i] == m_aTouches[j])
 			{
-				m_aTouches[j] = NULL;
+				m_aTouches[j] = m_aTouches[jl - 1];
+				m_aTouches.erase(jl - 1);
 				bFound = true;
 				break;
 			}
@@ -219,16 +225,11 @@ void CBaseTrigger::update(float dt)
 			onTouchStart(m_aNewTouches[i]);
 		}
 	}
-	CBaseEntity * pLastTouch = NULL;
 	for(int j = 0, jl = m_aTouches.size(); j < jl; ++j)
 	{
-		if(m_aTouches[j])
-		{
-			onTouchEnd(m_aTouches[j]);
-			pLastTouch = m_aTouches[j];
-		}
+		onTouchEnd(m_aTouches[j]);
 	}
-	if(pLastTouch && !m_aNewTouches.size())
+	if(m_aTouches.size() && !m_aNewTouches.size())
 	{
 		onTouchEndAll(m_aTouches[0]);
 	}
@@ -257,3 +258,8 @@ void CBaseTrigger::setOrient(const SMQuaternion & q)
 		SPhysics_GetDynWorld()->updateSingleAabb(m_pGhostObject);
 	}
 }
+
+void CPhysicsTickEventListener::onEvent(const XEventPhysicsStep *pData)
+{
+	m_pTrigger->onPhysicsStep();
+}
diff --git a/source/game/BaseTrigger.h b/source/game/BaseTrigger.h
index 25d6855c841fd7ad82672372d04e70b31502ad46..f9dc832c137df29676a8862c92904577415399a4 100644
--- a/source/game/BaseTrigger.h
+++ b/source/game/BaseTrigger.h
@@ -19,16 +19,31 @@ See the license in LICENSE
 
 #include "BaseAnimating.h"
 
+class CBaseTrigger;
+class CPhysicsTickEventListener final: public IEventListener<XEventPhysicsStep>
+{
+public:
+	CPhysicsTickEventListener(CBaseTrigger *pTrigger):
+		m_pTrigger(pTrigger)
+	{
+	}
+	void onEvent(const XEventPhysicsStep *pData) override;
+
+private:
+	CBaseTrigger *m_pTrigger;
+};
+
 //! Базовый класс триггера
 class CBaseTrigger: public CBaseAnimating
 {
 	DECLARE_CLASS(CBaseTrigger, CBaseAnimating);
 	DECLARE_PROPTABLE();
+
+	friend class CPhysicsTickEventListener;
 public:
-	DECLARE_TRIVIAL_CONSTRUCTOR();
+	DECLARE_CONSTRUCTOR();
 	~CBaseTrigger();
 
-	void onSync() override;
 	void onPostLoad() override;
 
 	void enable();
@@ -40,7 +55,6 @@ public:
 
 protected:
 	bool m_bEnabled = true;
-	ID m_idUpdateInterval = -1;
 
 	ID m_idDevMaterial = -1;
 	
@@ -51,7 +65,7 @@ protected:
 	output_t m_onTouchEnd;
 	output_t m_onTouchEndAll;
 
-	void update(float dt);
+	void update();
 
 	btPairCachingGhostObject *m_pGhostObject = NULL;
 
@@ -67,6 +81,13 @@ protected:
 	virtual void onTouchStart(CBaseEntity *pActivator);
 	virtual void onTouchEnd(CBaseEntity *pActivator);
 	virtual void onTouchEndAll(CBaseEntity *pActivator);
+
+private:
+	static IEventChannel<XEventPhysicsStep> *m_pTickEventChannel;
+
+	CPhysicsTickEventListener m_physicsTicker;
+
+	void onPhysicsStep();
 };
 
 #endif
diff --git a/source/game/FuncRotating.cpp b/source/game/FuncRotating.cpp
index e6d205ab1613048f739ad07a974ab30c3ab8f26e..a1bd46b4f5599eab645af5364d5b90c8ed71efaa 100644
--- a/source/game/FuncRotating.cpp
+++ b/source/game/FuncRotating.cpp
@@ -58,7 +58,7 @@ void CFuncRotating::toggle(inputdata_t *pInputdata)
 void CFuncRotating::think(float fDt)
 {
 	float3 fAxis = getOrient() * float3(0.0f, m_isReversed ? -1.0f : 1.0f, 0.0f);
-	setOrient(getOrient() * SMQuaternion(fAxis, m_fSpeed * fDt));
+	setOrient((getOrient() * SMQuaternion(fAxis, m_fSpeed * fDt)).Normalize());
 }
 
 void CFuncRotating::updateFlags()
diff --git a/source/game/NPCBase.cpp b/source/game/NPCBase.cpp
index 588e56ce069e94c7f12f5cbaf7ef6c9ce46fbbb0..4b715957f582b42b85407598f81e67ce4b5427eb 100644
--- a/source/game/NPCBase.cpp
+++ b/source/game/NPCBase.cpp
@@ -568,6 +568,8 @@ void CNPCBase::stopOrientAt()
 	}
 }
 
+
+#if 0
 void CNPCBase::onSync()
 {
 	BaseClass::onSync();
@@ -589,7 +591,6 @@ void CNPCBase::onSync()
 	//SMQuaternion rot = m_pHeadEnt->getOrient();
 	//SPhysics_GetDynWorld()->getDebugDrawer()->drawLine(F3_BTVEC(m_pHeadEnt->getPos()), F3_BTVEC(m_pHeadEnt->getPos() + (rot * float3(0,0,1))), btVector3(1,1,1));
 }
-#if 0
 void CNPCBase::gridCheckBeyond()
 {
 	//находим ближайший квад к текущей позиции нпс
diff --git a/source/game/NPCBase.h b/source/game/NPCBase.h
index bc871dad50df2a4158d8856002274a9951fc4032..1f4ef794fd983bcc6033c6975dd769c00a05dbca 100644
--- a/source/game/NPCBase.h
+++ b/source/game/NPCBase.h
@@ -72,8 +72,6 @@ public:
 	void stopOrientAt();
 
 protected:
-	void onSync() override;
-
 	//void think(float fDelta);
 
 	//! процедура проверки найденности пути
diff --git a/source/game/NPCZombie.cpp b/source/game/NPCZombie.cpp
index 478d7c23ced60b2494b35fa7fe9acbfb5387924c..8359a23ff5f8fed36213a69dfcaefa92301ee7c0 100644
--- a/source/game/NPCZombie.cpp
+++ b/source/game/NPCZombie.cpp
@@ -69,13 +69,11 @@ void CNPCZombie::onDeath(CBaseEntity *pAttacker, CBaseEntity *pInflictor)
 	m_pActiveTool->stopAction();
 }
 
-void CNPCZombie::onSync()
+void CNPCZombie::setPos(const float3 &pos)
 {
-	BaseClass::onSync();
-
-	SAFE_CALL(m_pSndIdle, setWorldPos, getPos());
-	SAFE_CALL(m_pSndIdle2, setWorldPos, getPos());
-	SAFE_CALL(m_pSndDeath, setWorldPos, getPos());
+	SAFE_CALL(m_pSndIdle, setWorldPos, pos);
+	SAFE_CALL(m_pSndIdle2, setWorldPos, pos);
+	SAFE_CALL(m_pSndDeath, setWorldPos, pos);
 }
 
 void CNPCZombie::rotateThink(float dt)
diff --git a/source/game/NPCZombie.h b/source/game/NPCZombie.h
index 0b9a932b037def71a0bd3302eb67847a7f088643..44253c35f0ca667a0f75bc70c452d1e6d3c02554 100644
--- a/source/game/NPCZombie.h
+++ b/source/game/NPCZombie.h
@@ -40,10 +40,9 @@ public:
 
 	void dispatchDamage(CTakeDamageInfo &takeDamageInfo);
 
-protected:
-
-	void onSync() override;
+	void setPos(const float3 & pos) override;
 
+protected:
 	void think(float fDelta);
 	void removeThis(float fDelta);
 
diff --git a/source/physics/PhyWorld.cpp b/source/physics/PhyWorld.cpp
index 3abb3c949dcce3546d1ff7ab0887ab9fcc41603c..9e294766d2f2d30338247b1a4dc7d6eee2ef7157 100644
--- a/source/physics/PhyWorld.cpp
+++ b/source/physics/PhyWorld.cpp
@@ -106,6 +106,10 @@ CPhyWorld::CPhyWorld():
 
 	m_pDynamicsWorld->getSolverInfo().m_numIterations = 30;
 
+	// typedef void (*btInternalTickCallback)(btDynamicsWorld *world, btScalar timeStep);
+
+	m_pDynamicsWorld->setInternalTickCallback(TickCallback, this);
+
 	//btCreateDefaultTaskScheduler();
 	static CTaskScheduler taskSheduler;
 	btSetTaskScheduler(&taskSheduler);
@@ -126,6 +130,8 @@ CPhyWorld::CPhyWorld():
 	btSetCustomEnterProfileZoneFunc(CProfileManager::Start_Profile);
 	btSetCustomLeaveProfileZoneFunc(CProfileManager::Stop_Profile);
 
+	m_pTickEventChannel = Core_GetIXCore()->getEventChannel<XEventPhysicsStep>(EVENT_PHYSICS_STEP_GUID);
+	
 #if 0
 	Core_GetIXCore()->getEventChannel<XEventLevel>(EVENT_LEVEL_GUID)->addListener([](const XEventLevel *pData)
 	{
@@ -853,6 +859,17 @@ void CPhyWorld::enableSimulation()
 	m_iSkipFrames = 3;
 }
 
+void CPhyWorld::TickCallback(btDynamicsWorld *world, btScalar timeStep)
+{
+	CPhyWorld *pThis = (CPhyWorld*)world->getWorldUserInfo();
+
+	XEventPhysicsStep ev;
+	ev.fTimeStep = timeStep;
+	ev.pPhysics = NULL;
+
+	pThis->m_pTickEventChannel->broadcastEvent(&ev);
+}
+
 //##############################################################
 
 void XMETHODCALLTYPE CPhyWorld::CRenderable::renderStage(X_RENDER_STAGE stage, IXRenderableVisibility *pVisibility)
diff --git a/source/physics/PhyWorld.h b/source/physics/PhyWorld.h
index 5985d411a632acca6343d920696c569ee8d40f87..1112d84dc01f6151961b907491d1bbf0b0e00b3e 100644
--- a/source/physics/PhyWorld.h
+++ b/source/physics/PhyWorld.h
@@ -197,6 +197,10 @@ protected:
 
 	bool m_isRunning;
 	int m_iSkipFrames = 3;
+
+	IEventChannel<XEventPhysicsStep> *m_pTickEventChannel = NULL;
+
+	static void TickCallback(btDynamicsWorld *world, btScalar timeStep);
 };
 
 #endif
diff --git a/source/xcommon/XEvents.h b/source/xcommon/XEvents.h
index 7231051f7c4628df58f5169138db2e7b02229d5b..2a3929d35ceb605a2b64f06474bfbe5ecfe529a3 100644
--- a/source/xcommon/XEvents.h
+++ b/source/xcommon/XEvents.h
@@ -27,46 +27,34 @@ class IEventChannel: public IBaseEventChannel
 public:
 	void addListener(PFNLISTENER fnListener)
 	{
-		for(UINT i = 0, l = m_vListeners.size(); i < l; ++i)
+		if(m_vListeners.indexOf(fnListener) < 0)
 		{
-			if(m_vListeners[i] == fnListener)
-			{
-				return;
-			}
+			m_vListeners.push_back(fnListener);
 		}
-		m_vListeners.push_back(fnListener);
 	}
 	void addListener(IEventListener<T> *pListener)
 	{
-		for(UINT i = 0, l = m_vListeners2.size(); i < l; ++i)
+		if(m_vListeners2.indexOf(pListener) < 0)
 		{
-			if(m_vListeners2[i] == pListener)
-			{
-				return;
-			}
+			m_vListeners2.push_back(pListener);
 		}
-		m_vListeners2.push_back(pListener);
 	}
 	void removeListener(PFNLISTENER fnListener)
 	{
-		for(UINT i = 0, l = m_vListeners.size(); i < l; ++i)
+		int idx = m_vListeners.indexOf(fnListener);
+		if(idx >= 0)
 		{
-			if(m_vListeners[i] == fnListener)
-			{
-				m_vListeners.erase(i);
-				return;
-			}
+			m_vListeners[idx] = m_vListeners[m_vListeners.size() - 1];
+			m_vListeners.erase(m_vListeners.size() - 1);
 		}
 	}
 	void removeListener(IEventListener<T> *pListener)
 	{
-		for(UINT i = 0, l = m_vListeners2.size(); i < l; ++i)
+		int idx = m_vListeners2.indexOf(pListener);
+		if(idx >= 0)
 		{
-			if(m_vListeners2[i] == pListener)
-			{
-				m_vListeners2.erase(i);
-				return;
-			}
+			m_vListeners2[idx] = m_vListeners2[m_vListeners2.size() - 1];
+			m_vListeners2.erase(m_vListeners2.size() - 1);
 		}
 	}
 	void broadcastEvent(const T *pEvent)
@@ -241,4 +229,15 @@ struct XEventSkyboxChanged
 	IXTexture *pTexture;
 };
 
+
+// {1DB80A19-7DFA-4D61-923B-34906590DBB0}
+#define EVENT_PHYSICS_STEP_GUID DEFINE_XGUID(0x1db80a19, 0x7dfa, 0x4d61, 0x92, 0x3b, 0x34, 0x90, 0x65, 0x90, 0xdb, 0xb0)
+
+class IXPhysics;
+struct XEventPhysicsStep
+{
+	IXPhysics *pPhysics;
+	float fTimeStep;
+};
+
 #endif