Objekt in Welt mit bestimmter Geschwindigkeit bewegen

FBIagent

Erfahrenes Mitglied
Guten tag,

wie im Titel dieses Threads schon angegeben, versuche ich ein Objekt mit
bestimmter Geschwindigkeit zu bewegen. Dazu habe ich mir einen kleinen
Algorythmus geschrieben, der das Objekt halt nach seiner Geschwindigkeit
bewegt. Hierzu habe ich 2 Variablen die speichern wo das Objekt gerade
ist(m_X,m_Y), 2 Variablen die speichern wo das Objekt hin
soll(m_DestX,m_DestY) und eine Variable die speichert wann angefangen
wurde sich zu bewegen(m_StartMove).

Main Algorythmus lässt das Objekt aber zu schnell wandern
wenn entweder m_X == m_DestX oder m_Y == m_DestY.

Zum starten der Bewegung gibt es eine Methode namens StartMove().
Zum aktualisieren der Position des Objekts gibt es die Methode Move(),
die bei jedem frame aufgerufen wird.

Wäre nett wenn mal jemand drüber schauen kann.

Code:
//////////////////// WORLDOBJECT.H
#pragma once

class WorldObject {
public:
    int GetObjectId();
    int GetX();
    int GetY();
    const char *GetName();
protected:
    int m_ObjectId;
    volatile int m_X;
    volatile int m_Y;
    char m_Name[ 32 ];

    WorldObject( int ObjectId, int X, int Y );
};
//////////////////// WORLDOBJECT.CPP
#include "./WorldObject.h"

int WorldObject::GetObjectId() {
    return m_ObjectId;
}

int WorldObject::GetX() {
    return m_X;
}

int WorldObject::GetY() {
    return m_Y;
}

const char *WorldObject::GetName() {
    return m_Name;
}

WorldObject::WorldObject( int ObjectId, int X, int Y )
: m_ObjectId( ObjectId ), m_X( X ), m_Y( Y ) {
}

Code:
//////////////////// WORLDMOVEABLE.H
#pragma once

#include "./WorldObject.h"

#define MOVE_MS 100
#define MOVES_PER_MS_TICK 1

class WorldMoveable
: public WorldObject {
public:
	void StartMove( int DestX, int DestY );
	void StopMove();

	// should be called on every frame
	void Move();

	virtual void OnArrived() = 0;

	int GetDestX();
	int GetDestY();
protected:
	int m_DestX;
	int m_DestY;

	WorldMoveable( int ObjectId, int X, int Y );
private:
	unsigned long m_StartMove;
	long double m_MoveDiffX100Start;
	long double m_MoveDiffY100Start;
};
//////////////////// WORLDMOVEABLE.CPP
#include "./WorldMoveable.h"

#include <windows.h>
#include <cmath>

void WorldMoveable::StartMove( int DestX, int DestY ) {
	m_StartMove = GetTickCount();
	m_DestX = DestX;
	m_DestY = DestY;
	m_MoveDiffX100Start = abs( ( long double )m_X - ( long double )m_DestX );
	m_MoveDiffY100Start = abs( ( long double )m_Y - ( long double )m_DestY );
}

void WorldMoveable::StopMove() {
	m_DestX = m_X;
	m_DestY = m_Y;
}

// should be called on every frame
void WorldMoveable::Move() {
	if ( m_X == m_DestX && m_Y == m_DestY ) return;

	unsigned long Elapsed = GetTickCount() - m_StartMove;
	int Moves = ( Elapsed / MOVE_MS ) * MOVES_PER_MS_TICK;
	if ( Moves == 0 )return;

	long ModX = m_X > m_DestX ? -1 : ( m_X < m_DestX ? 1 : 0 );
	long ModY = m_Y > m_DestY ? -1 : ( m_Y < m_DestY ? 1 : 0 );
	// in points
	long double DiffX100 = 0;
	long double DiffY100 = 0;
	// percentage
	long double DiffX = 0;
	long double DiffY = 0;
	long NextX = m_X;
	long NextY = m_Y;

	while ( ( m_X != m_DestX || m_Y != m_DestY ) && Moves > 0 ) {
		DiffX100 = abs( ( long double )m_X - ( long double )m_DestX );
		DiffY100 = abs( ( long double )m_Y - ( long double )m_DestY );
		DiffX = 100.L * DiffX100 / m_MoveDiffX100Start;
		DiffY = 100.L * DiffY100 / m_MoveDiffY100Start;

		if ( ( DiffX < DiffY )/* || ( (int)DiffX == (int)DiffY && DiffX100 > DiffY100 )*/ ) {
			NextY += ModY;
		} else if ( ( DiffX > DiffY )/* || ( (int)DiffX == (int)DiffY && DiffX100 < DiffY100 )*/ ) {
			NextX += ModX;
		} else {
			NextX += ModX;
			NextY += ModY;
		}

		m_X = NextX;
		m_Y = NextY;
		--Moves;
	}

	// in case we didn't reach the destination yet
	if ( m_X != m_DestX && m_Y != m_DestY ) m_StartMove = GetTickCount();
	else if ( m_X == m_DestX && m_Y == m_DestY ) OnArrived();
}

int WorldMoveable::GetDestX() {
	return m_DestX;
}

int WorldMoveable::GetDestY() {
	return m_DestY;
}

WorldMoveable::WorldMoveable( int ObjectId, int X, int Y )
: WorldObject( ObjectId, X, Y ), m_DestX( X ), m_DestY( Y ) {
}

Die Zielpositionen werden in der WindowProc des fensters gesetzt:
Code:
	case WM_KEYDOWN:
		switch ( wParam ) {
		case VK_UP:
			OneNpc->StartMove( OneNpc->GetDestX(), OneNpc->GetDestY() - 50 );
			break;
		case VK_DOWN:
			OneNpc->StartMove( OneNpc->GetDestX(), OneNpc->GetDestY() + 50 );
			break;
		case VK_LEFT:
			OneNpc->StartMove( OneNpc->GetDestX() - 50, OneNpc->GetDestY() );
			break;
		case VK_RIGHT:
			OneNpc->StartMove( OneNpc->GetDestX() + 50, OneNpc->GetDestY() );
			break;
		default:
			break;
		}

		return 0;

Und hier wo gerendert wird, wird Move() aufgerufen um die Position zu aktualisieren:
Code:
	if ( MainWindow_Init() ) {
		while ( true ) {
			if ( PeekMessage( &wndMsg, NULL, 0, 0, PM_REMOVE ) ) {
				if ( wndMsg.message == WM_QUIT ) break;
				TranslateMessage( &wndMsg );
				DispatchMessage( &wndMsg );
			}

			MainWindow_RenderFrame();
		}
	}
........................................
void MainWindow_RenderFrame() {
	// move npcs
/*	for ( g_NpcsIter = g_Npcs.begin();g_NpcsIter != g_Npcs.end();++ g_NpcsIter ) {
		(*g_NpcsIter)->Move();
	}*/
	OneNpc->Move();

	g_D2DRenderTarget->BeginDraw();
	//g_D2DRenderTarget->SetTransform( D2D1::Matrix3x2F::Identity() );
	g_D2DRenderTarget->Clear( D2D1::ColorF( D2D1::ColorF::White ) );

	// Calculate the number of frames per one second:
	g_dwFrames++;
	g_dwCurrentTime = GetTickCount(); // Even better to use timeGetTime()
	g_dwElapsedTime = g_dwCurrentTime - g_dwLastUpdateTime;

	if ( g_dwElapsedTime >= 10 ) {
		g_FPS = g_dwFrames * ( 1000UL / g_dwElapsedTime );
		g_dwFrames = 0UL;
		g_dwLastUpdateTime = g_dwCurrentTime;
	}

	D2D1_RECT_F NpcRect;

/*	// draw npc's
	for ( g_NpcsIter = g_Npcs.begin();g_NpcsIter != g_Npcs.end();++ g_NpcsIter ) {
		NpcRect.top = (*g_NpcsIter)->GetY() - 8;
		NpcRect.bottom = (*g_NpcsIter)->GetY() + 8;
		NpcRect.left = (*g_NpcsIter)->GetX() - 8;
		NpcRect.right = (*g_NpcsIter)->GetX() + 8;
		g_D2DRenderTarget->DrawRectangle( NpcRect, g_D2DBrushGray );
	}*/

	NpcRect.top = OneNpc->GetY() - 8;
	NpcRect.bottom = OneNpc->GetY() + 8;
	NpcRect.left = OneNpc->GetX() - 8;
	NpcRect.right = OneNpc->GetX() + 8;
	g_D2DRenderTarget->DrawRectangle( NpcRect, g_D2DBrushGray );

	NpcRect.top = OneNpc->GetDestY() - 8;
	NpcRect.bottom = OneNpc->GetDestY() + 8;
	NpcRect.left = OneNpc->GetDestX() - 8;
	NpcRect.right = OneNpc->GetDestX() + 8;
	g_D2DRenderTarget->DrawRectangle( NpcRect, g_D2DBrushGreen );

/*	g_D2DRenderPoint2.x = g_FPS;
	g_D2DRenderTarget->DrawLine( g_D2DRenderPoint1, g_D2DRenderPoint2, g_D2DBrushGray, 10 );*/
	// g_D2DRenderTarget->DrawTextW( itoa( FPS, szFPS, 10 ), wcslen( FPS ),

	g_D2DRenderTarget->EndDraw();
}
 
Zuletzt bearbeitet:
Hallo,

ich habe deinen Algorithmus jetzt nicht im Detail analysiert, aber ich finde es etwas problematisch, dass du die aktuelle Position des Objektes nur als Ganzzahl speicherst und immer ausgehend von dieser Position die Aktualisierung durchführst. Dadurch kann es passieren, dass sich das Objekt überhaupt nicht mehr bewegt, wenn die Framerate zu hoch wird (Moves wird dann mit 0 initialisiert). Weiterhin rufst du für jedes Objekt die Funktion GetTickCount() erneut auf, wodurch verschiedene Objekte sich von einem Frame zum nächsten möglicherweise mit unterschiedlichen Geschwindigkeiten bewegen (da diese Funktion auch noch mit einer relativ geringen Genauigkeit arbeitet, könnte der "Sprung" von einem Objekt zum nächsten sogar [relativ gesehen] recht groß ausfallen). Man sollte hier besser in der Hauptschleife des Programms einmal pro Frame die aktuelle (bzw. vergangene) Zeit ermitteln und diese dann an die Move-Methode per Parameter übergeben (oder diesen Wert sonst irgendwie global zugänglich machen).

Mein Vorschlag wäre jetzt, in der Methode StartMove die aktuelle Position des Objektes in zwei zusätzliche Instanzvariablen (nennen wir sie mal suggestiv m_StartX und m_StartY) zu kopieren. In Move ermittelst du dann die aktuelle Position des Objektes dadurch, indem du zwischen der gemerkten Startposition der Bewegung un der Endposition (linear) interpolierst. Also in (Pseudo-)code:
Code:
Move(float CurrentTimeMillis) {
  float PercentageToDestination = (float)(CurrentTimeMillis - m_StartMoveMillis) / MOVE_MS;
  m_X = m_StartX + PercentageToDestination * (m_DestX - m_StartX);
  m_Y = m_StartY + PercentageToDestination * (m_DestY - m_StartY);
}
Dabei sollte man natürlich noch Werte über 1 für PercentageToDestination abfangen, aber ich bin mir sicher dass du das auch selber schaffst :)

Eine Alternative wäre, die Position des Objektes zusätzlich noch als Fließkommazahl zu speichern. Das hätte den Vorteil, dass die Erweiterung auf Bewegungen mit z.B. nicht-konstanter Geschwindigkeit leichter fällt.

Grüße, Matthias
 
Ich habe mir deine Vorschläge mal angeschaut und bin zu folgendem
Ergebniss gekommen:
Code:
////////////////////////// WORLDMOVEABLE.H
#pragma once

#include "./WorldObject.h"

#include <windows.h>

class WorldMoveable
: public WorldObject {
public:
    void MoveLock();
    void MoveUnlock();
    void MoveStart( int DestX, int DestY );
    void MoveStop();
    void Move( DWORD CurTick );

    virtual void OnArrived() = 0;

    void SetMoveSpeed( int MoveSpeed );

    int GetMoveDestX();
    int GetMoveDestY();
    int GetMoveSpeed();

    WorldMoveable( int ObjectId, int X, int Y );
private:
    CRITICAL_SECTION m_MoveCs;
    DWORD m_MoveStartTick;
    int m_MoveStartX;
    int m_MoveStartY;
    int m_MoveDestX;
    int m_MoveDestY;
    int m_MoveDiffX;
    int m_MoveDiffY;
    int m_MoveSpeed;
};
////////////////////////// WORLDMOVEABLE.CPP
#include "./WorldMoveable.h"

#include <cmath>

void WorldMoveable::MoveLock() {
    EnterCriticalSection( &m_MoveCs );
}

void WorldMoveable::MoveUnlock() {
    LeaveCriticalSection( &m_MoveCs );
}

void WorldMoveable::MoveStart( int DestX, int DestY ) {
    m_MoveStartTick = GetTickCount();
    m_MoveStartX = m_X;
    m_MoveStartY = m_Y;
    m_MoveDestX = DestX;
    m_MoveDestY = DestY;
    m_MoveDiffX = m_MoveDestX - m_MoveStartX;
    m_MoveDiffY = m_MoveDestY - m_MoveStartY;
}

void WorldMoveable::MoveStop() {
    m_MoveDestX = m_X;
    m_MoveDestY = m_Y;
}

void WorldMoveable::Move( DWORD CurTick ) {
    if ( m_X == m_MoveDestX && m_Y == m_MoveDestY ) return;

    double MoveDistPassed = ( double )m_MoveSpeed * ( ( double )CurTick - ( double )m_MoveStartTick ) / 1000.;
    double MoveDistFraction = MoveDistPassed / sqrt( ( double )( m_MoveDiffX * m_MoveDiffX ) + ( m_MoveDiffY * m_MoveDiffY ) );

    if ( MoveDistFraction >= 1. ) {
        m_X = m_MoveDestX;
        m_Y = m_MoveDestY;
        OnArrived();
    } else {
        m_X = m_MoveStartX + m_MoveDiffX * MoveDistFraction;
        m_Y = m_MoveStartY + m_MoveDiffY * MoveDistFraction;
    }
}

void WorldMoveable::SetMoveSpeed( int MoveSpeed ) {
    m_MoveSpeed = MoveSpeed;
}

int WorldMoveable::GetMoveDestX() {
    return m_MoveDestX;
}

int WorldMoveable::GetMoveDestY() {
    return m_MoveDestY;
}

int WorldMoveable::GetMoveSpeed() {
    return m_MoveSpeed;
}

WorldMoveable::WorldMoveable( int ObjectId, int X, int Y )
: WorldObject( ObjectId, X, Y ), m_MoveDestX( X ), m_MoveDestY( Y ), m_MoveSpeed( 250 ) {
    InitializeCriticalSection( &m_MoveCs );
}

Funktioniert auch mit variabler Geschwindigkeit des Objekts wobei man
beachten sollte, dass vor änderung der Geschwindigkeit nochmal Move
aufgerufen wird, damit die vergangende Zeit mit der alten Geschwindigkeit
aktualisiert wird, und nach änderung der Geschwindigkeit einmal StartMove
mit dem aktuell gesetzten Ziel, damit neue Anfangswerte für die neue
Geschwindigkeit vorhanden sind:
Code:
    case WM_KEYDOWN:
        switch ( wParam ) {
        case VK_UP:
            OneNpc->MoveStart( OneNpc->GetMoveDestX(), OneNpc->GetMoveDestY() - 500 );
            break;
        case VK_DOWN:
            OneNpc->MoveStart( OneNpc->GetMoveDestX(), OneNpc->GetMoveDestY() + 500 );
            break;
        case VK_LEFT:
            OneNpc->MoveStart( OneNpc->GetMoveDestX() - 500, OneNpc->GetMoveDestY() );
            break;
        case VK_RIGHT:
            OneNpc->MoveStart( OneNpc->GetMoveDestX() + 500, OneNpc->GetMoveDestY() );
            break;
        case VK_NUMPAD2:
            OneNpc->Move( GetTickCount() );
            OneNpc->SetMoveSpeed( OneNpc->GetMoveSpeed() - 1 );
            OneNpc->MoveStart( OneNpc->GetMoveDestX(), OneNpc->GetMoveDestY() );
            break;
        case VK_NUMPAD8:
            OneNpc->Move( GetTickCount() );
            OneNpc->SetMoveSpeed( OneNpc->GetMoveSpeed() + 1 );
            OneNpc->MoveStart( OneNpc->GetMoveDestX(), OneNpc->GetMoveDestY() );
            break;
        default:
            break;
        }

        return 0;
 
Zuletzt bearbeitet:
Zurück