Files
RP412/MUNGA/CAMSHIP.cpp
T
CydandClaude Opus 5 9a674e9241 The camera measures its own smoothness
The last instrument was mis-specified: it compared an arriving update
against our current position, but those are at different times, so most of
the 0.40 m it reported was latency times speed rather than prediction
error. It could not have shown a visual jump even if one existed. The
follow trace agreed - per-step motion stayed tight at 1.36 to 1.68 m and
tracked speed, with no outliers riding on top.

It did settle one thing: corrections arrive 43 times a second, near the
50 Hz step rate, so whatever ticks a few times a second is not one per
correction.

So measure the symptom instead of a theory about its cause. A visible tick
IS a step that moves much further, or much less, than the steps around it,
and that is now counted directly: spikes above 2.5x a short running mean,
stalls below 0.4x, judged against the mean rather than an absolute distance
because a pod at 75 m/s moves 1.5 m per step and one against a wall moves
nothing. Respawns are counted and excluded - they teleport hundreds of
metres and are meant to be discontinuities.

Stated in advance, so the result cannot be read to taste: spikes at a few
per second confirms the tick is in entity motion and gives its rate; spikes
and stalls near zero refutes it, and points at frame delivery or the
newly-active map raster instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 07:53:44 -05:00

861 lines
21 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "camship.h"
#include "player.h"
#include "mission.h"
#include "cammppr.h"
#include "app.h"
#include "hostmgr.h"
//##########################################################################
//############################# CameraShip ################################
//##########################################################################
//
// Trackside camera cuts since the last RP412CAMLOG report - counted where
// the cut happens and drained where it is reported, both in FollowGoal.
//
static int gCameraCuts = 0;
//#############################################################################
// Shared Data Support
//
Derivation* CameraShip::GetClassDerivations()
{ static Derivation classDerivations(Mover::GetClassDerivations(), "CameraShip");
return &classDerivations;
}
CameraShip::SharedData
CameraShip::DefaultData(
CameraShip::GetClassDerivations(),
CameraShip::MessageHandlers,
CameraShip::GetAttributeIndex(),
CameraShip::StateCount,
(Entity::MakeHandler)CameraShip::Make
);
//#############################################################################
// Attribute Support
//
const CameraShip::IndexEntry
CameraShip::AttributePointers[]=
{
ATTRIBUTE_ENTRY(CameraShip, MapRange, mapRange),
ATTRIBUTE_ENTRY(CameraShip, MapLinearPosition, mapLinearPosition),
ATTRIBUTE_ENTRY(CameraShip, MapAngularPosition, mapAngularPosition)
};
CameraShip::AttributeIndexSet& CameraShip::GetAttributeIndex()
{
static CameraShip::AttributeIndexSet attributeIndex(ELEMENTS(CameraShip::AttributePointers),
CameraShip::AttributePointers,
Mover::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Messaging Support
//
const Receiver::HandlerEntry
CameraShip::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(CameraShip, Direction)
};
CameraShip::MessageHandlerSet
CameraShip::MessageHandlers(
ELEMENTS(CameraShip::MessageHandlerEntries),
CameraShip::MessageHandlerEntries,
Entity::GetMessageHandlers()
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::DirectionMessageHandler(DirectionMessage *message)
{
Check(this);
Check(message);
Check(application);
HostManager *host = application->GetHostManager();
Check(host);
SetGoalEntity(host->GetEntityPointer(message->goalEntity));
Check(goalEntity);
mapLinearPosition = &goalEntity->localOrigin.linearPosition;
focusOffset = message->focusOffset;
SetSimulationState(DefaultState);
timeOnCamera = message->timeOnCamera;
lastSwitch = Now();
lastSwitch -= timeOnCamera;
Check_Fpu();
}
//#############################################################################
// ControlsMapper Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::SetMappingSubsystem(CameraControlsMapper *mapper)
{
Check(this);
Check(mapper);
if(subsystemArray[ControlsMapperSubsystem])
{
Unregister_Object(subsystemArray[ControlsMapperSubsystem]);
delete subsystemArray[ControlsMapperSubsystem];
}
subsystemArray[ControlsMapperSubsystem] = mapper;
Check_Fpu();
}
//#############################################################################
// Simulation Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::DriveCamera(Scalar time_slice)
{
Check(this);
CameraControlsMapper* controls =
Cast_Object(
CameraControlsMapper*,
GetSubsystem(CameraShip::ControlsMapperSubsystem)
);
Check(controls);
//
//-----------------------------------------------
// If we are in positioning mode, move the camera
//-----------------------------------------------
//
if (controls->driveCamera > 0)
{
ApplyControlledPanAndTilt(controls, time_slice);
ApplyControlledCameraMotion(controls, time_slice);
ApplyControlledCameraHeight(controls);
currentCamera->SetLocalOrigin(localOrigin);
}
//
//------------------------------------------------------------------------
// Operate in camera mode, obeying the limits of the camera as established
// by the clamps
//------------------------------------------------------------------------
//
else
{
//
//-------------------------
// Stop any existing motion
//-------------------------
//
worldLinearVelocity = Vector3D::Identity;
//
//-----------------------------------------------------------------------
// Read the controls for panning the camera, and if the clamp override is
// on, tell the camera to allow this orientation
//-----------------------------------------------------------------------
//
ApplyControlledPanAndTilt(controls, time_slice);
localToWorld = localOrigin;
//
//---------------------------------------------------------------------
// Establish an aim point along negative Z and figure out where that is
// in world space, then point the camera there. The camera clamps will
// automatically do their thing
//---------------------------------------------------------------------
//
Point3D local_aim(0.0f, 0.0f, -1.0f);
Point3D world_aim;
world_aim.Multiply(local_aim, localToWorld);
if (controls->slideOrClamp > 0)
{
currentCamera->AllowLookingAt(world_aim);
}
AimCameraAtPoint(world_aim);
}
localToWorld = localOrigin;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::ApplyControlledPanAndTilt(
CameraControlsMapper *controls,
Scalar time_slice
)
{
Check(this);
Check(controls);
//
//-----------------------------------------
// Point the Camera in the direction of the
// Joystick Position Calc Pitch and Yaw only
//-----------------------------------------
//
Scalar rotation_amount;
if(controls->stickPosition.x < -0.02f)
{
rotation_amount = controls->stickPosition.x + 0.02f;
rotation_amount *= rotation_amount;
rotation_amount *= -1.0f;
}
else if (controls->stickPosition.x > 0.02f)
{
rotation_amount = controls->stickPosition.x - 0.02f;
rotation_amount *= rotation_amount;
}
else
{
rotation_amount = 0.0f;
}
YawPitchRoll ypr;
ypr = localOrigin.angularPosition;
ypr.yaw += rotation_amount * PI * time_slice;
ypr.pitch += controls->stickPosition.y * PI * 0.2f * time_slice;
localOrigin.angularPosition = ypr;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::ApplyControlledCameraHeight(CameraControlsMapper *controls)
{
Check(this);
Check(controls);
//
//----------------------------------------------------
// Raise and lower the cameraShip according to the pedals
//----------------------------------------------------
//
localOrigin.linearPosition.y += controls->pedalsPosition;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::ApplyControlledCameraMotion(
CameraControlsMapper *controls,
Scalar time_slice
)
{
Check(this);
Check(controls);
localAcceleration.linearMotion.z =
-25.0f * controls->throttlePosition * time_slice;
if (controls->reverseThrust > 0)
{
localAcceleration.linearMotion.z = -localAcceleration.linearMotion.z;
}
//
//~~~~~~~~~~~~~~~~~~~
// Apply Linear Drag
//~~~~~~~~~~~~~~~~~~~
//
Vector3D drag_force;
drag_force.Multiply(
localVelocity.linearMotion,
positiveLinearDragCoefficients
);
Vector3D new_accel;
new_accel.Subtract(
localAcceleration.linearMotion,
drag_force
);
localAcceleration.linearMotion = new_accel;
//
//~~~~~~~~~~~~~~~~~~~~~~~~
// ApplyWorldAccelerations
//~~~~~~~~~~~~~~~~~~~~~~~~
localVelocity.linearMotion += localAcceleration.linearMotion;
worldLinearVelocity.Multiply(
localVelocity.linearMotion,
localToWorld
);
localOrigin.linearPosition += worldLinearVelocity;
Check_Fpu();
}
//#############################################################################
// Follow GoalEntity Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::FollowGoal(Scalar time_slice)
{
Check(this);
//
//-----------------------------------------------
// If no goalEntity assigned ask for one and wait
//-----------------------------------------------
//
if (!goalEntity)
{
return;
}
Check(goalEntity);
Point3D target;
target.Multiply(focusOffset, goalEntity->localToWorld);
//
// RP412CAMLOG: is the thing we are following actually MOVING every
// step, or arriving in jumps?
//
// This runs on the fixed simulation step, so it is called at a steady
// rate whatever the frame rate. The entity it follows is a REMOTE pod,
// whose position only changes when an update lands - so if the watched
// point is identical on most steps and then leaps, the camera is
// tracking a staircase and no amount of smoothing here will hide it.
// Reported as: steps counted, how many of them saw any movement at
// all, the largest single jump, and how often the trackside camera was
// cut to a different one (a cut is a snap, not a glide, and would look
// like jank of a different kind).
//
if (RPCameraLog())
{
static Scalar next_say = 0.0f;
static Point3D last_target(0.0f, 0.0f, 0.0f);
static Logical have_last = False;
static int steps = 0;
static int moved = 0;
static Scalar biggest = 0.0f;
//
// A visible tick IS a step that moves much further, or much less,
// than the steps around it. Measuring that directly beats measuring
// anything about where the number came from: it does not care
// whether the cause is the network, the catch-up rule, or something
// nobody has thought of yet.
//
// Judged against a short running mean rather than an absolute
// distance, because a pod at 75 m/s moves 1.5 m per step and one
// sitting against a wall moves nothing - a fixed threshold would
// call every acceleration a spike.
//
// What confirms: spikes running at a few per second. That is the
// reported tick, and its rate is in the count.
// What refutes: spikes and stalls near zero. Then entity motion is
// smooth and the tick is not in the simulation at all, which points
// at frame delivery or the map raster instead.
//
static Scalar mean_step = 0.0f;
static int spikes = 0;
static int stalls = 0;
static int respawns = 0;
static Scalar worst_ratio = 0.0f;
++steps;
if (have_last)
{
Vector3D step_delta;
step_delta.Subtract(target, last_target);
Scalar distance = step_delta.Length();
if (distance > 0.0f)
{
++moved;
}
if (distance > biggest)
{
biggest = distance;
}
//
// A respawn teleports hundreds of metres and is MEANT to be a
// discontinuity. Counted, excluded, and the mean restarted so
// one does not brand the following steps as stalls.
//
if (distance > 50.0f)
{
++respawns;
mean_step = 0.0f;
}
else if (mean_step > 0.01f)
{
Scalar ratio = distance / mean_step;
if (ratio > 2.5f)
{
++spikes;
if (ratio > worst_ratio)
{
worst_ratio = ratio;
}
}
else if (ratio < 0.4f)
{
++stalls;
}
mean_step = mean_step * 0.9f + distance * 0.1f;
}
else
{
mean_step = distance;
}
}
last_target = target;
have_last = True;
if ((Scalar) Now() >= next_say)
{
if (next_say > 0.0f)
{
DEBUG_STREAM << "CamLog: follow - " << steps << " steps, "
<< moved << " moved, biggest jump " << biggest
<< "m, " << gCameraCuts << " cut(s)\n" << std::flush;
DEBUG_STREAM << "CamLog: smoothness - " << spikes
<< " spike(s), " << stalls << " stall(s), "
<< respawns << " respawn(s), worst " << worst_ratio
<< "x the running mean of " << mean_step << "m\n" << std::flush;
}
next_say = ((Scalar) Now()) + 5.0f;
steps = 0;
moved = 0;
biggest = 0.0f;
spikes = 0;
stalls = 0;
respawns = 0;
worst_ratio = 0.0f;
gCameraCuts = 0;
}
}
//
//------------------------------------------------------------------------
// If time has not yet expired on this camera, keep with it if can see the
// goal entity
//------------------------------------------------------------------------
//
Check(currentCamera);
if (
lastPerformance - lastSwitch > timeOnCamera
|| !currentCamera->CanCameraSee(target)
)
{
CameraInstance *old_camera = currentCamera;
GoToClosestCamera(target);
if (!currentCamera)
{
currentCamera = old_camera;
}
Check(currentCamera);
//
//------------------------------------------------------------------------
// If we need to switch cameras, have the new camera point directly at the
// target
//------------------------------------------------------------------------
//
if (currentCamera != old_camera)
{
++gCameraCuts;
AimCameraAtPoint(target);
lastSwitch = lastPerformance;
}
else
{
goto Rotate_Camera;
}
}
//
//---------------------------------------------------
// Otherwise, rotate the camera as needed and allowed
//---------------------------------------------------
//
else
{
Rotate_Camera:
RotateCameraToPoint(time_slice, target);
}
//
//------------------------
// Set the tranform matrix
//------------------------
//
localToWorld = localOrigin;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::GoToClosestCamera(const Point3D &point_3D)
{
Check(this);
currentCamera = cameraManager.FindClosestCamera(point_3D);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::AimCameraAtPoint(const Point3D &point_3D)
{
Check(this);
Check(currentCamera);
currentCamera->LookAt(&localOrigin, point_3D);
localVelocity.angularMotion = Vector3D::Identity;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::RotateCameraToPoint(
Scalar time_slice,
const Point3D &point_3D
)
{
Check(this);
Check(currentCamera);
Check(&point_3D);
//
//---------------------------------------------------------
// Calculate the rotation needed to point to the goalEntity
//---------------------------------------------------------
//
Origin target;
currentCamera->LookAt(&target, point_3D);
YawPitchRoll new_yaw_pitch_roll;
new_yaw_pitch_roll = target.angularPosition;
Vector3D new_pos(new_yaw_pitch_roll.pitch, new_yaw_pitch_roll.yaw, 0.0f);
YawPitchRoll old_pos_euler;
old_pos_euler = localOrigin.angularPosition;
Vector3D old_pos(old_pos_euler.pitch, old_pos_euler.yaw, 0.0f);
old_pos.x = Radian::Normalize(old_pos.x);
old_pos.y = Radian::Normalize(old_pos.y);
//
// new - old position
//
if (new_pos.x-old_pos.x > PI) {
new_pos.x -= TWO_PI;
}
else if (new_pos.x-old_pos.x < -PI) {
new_pos.x += TWO_PI;
}
if (new_pos.y-old_pos.y > PI) {
new_pos.y -= TWO_PI;
}
else if (new_pos.y-old_pos.y < -PI) {
new_pos.y += TWO_PI;
}
Vector3D delta_rot;
delta_rot.Subtract(
new_pos,
old_pos
);
//
//-------------------
// Apply Angular Drag
//-------------------
//
Vector3D
drag;
drag.Multiply(
localVelocity.angularMotion,
angularDragCoefficients
);
//
//~~~~~~~~~~~~~~~~~~~~~
// Apply SpringConstant
//~~~~~~~~~~~~~~~~~~~~~
//
Vector3D
delta_rot_spring;
delta_rot_spring.Multiply(
delta_rot,
elasticityCoefficient
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Calculate the new angular acceleration
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
localAcceleration.angularMotion.Subtract(
delta_rot_spring,
drag
);
Scalar halft_squared = 0.5 *(time_slice * time_slice);
Vector3D
accel_comp;
accel_comp.Multiply(
localAcceleration.angularMotion,
halft_squared
);
//
// velocity * delta time
//
Vector3D
scaled_velocity;
scaled_velocity.Multiply(
localVelocity.angularMotion,
time_slice
);
Vector3D
tmp_position;
tmp_position.Add(
scaled_velocity,
accel_comp
);
Vector3D
clamped_accel;
clamped_accel.Add(
old_pos,
tmp_position
);
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Apply Acceleration to angular Position
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
YawPitchRoll
new_yawpitchroll(clamped_accel.y, clamped_accel.x, clamped_accel.z);
LinearMatrix
rot_matrix(LinearMatrix::Identity);
rot_matrix = new_yawpitchroll;
localOrigin.angularPosition = rot_matrix;
//
// Calc new Velocity
//
Vector3D
scaled_accel;
scaled_accel.Multiply(
localAcceleration.angularMotion,
time_slice
);
Vector3D
ang_vel;
ang_vel.Add(
localVelocity.angularMotion,
scaled_accel
);
localVelocity.angularMotion = ang_vel;
Check_Fpu();
}
//#############################################################################
// CameraInstanceManager Support
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::CreateCameraInstance()
{
Check(this);
Check(&cameraManager);
currentCamera = cameraManager.CreateCameraInstance(localOrigin);
DEBUG_STREAM << currentCamera->GetCameraName() << std::endl << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::DeleteCameraInstance()
{
Check(this);
//
//---------------------------------------------
// If this is the last camera, don't delete it!
//---------------------------------------------
//
Check(&cameraManager);
CameraInstance *next_camera = cameraManager.IncrementIterator();
if (next_camera == currentCamera)
{
return;
}
currentCamera = cameraManager.DeleteCameraInstance();
Check(currentCamera);
DEBUG_STREAM << currentCamera->GetCameraName() << std::endl << std::flush;
localOrigin = currentCamera->GetLocalOrigin();
localToWorld = localOrigin;
Point3D local_aim(0.0f, 0.0f, -1.0f);
Point3D world_aim;
world_aim.Multiply(local_aim, localToWorld);
AimCameraAtPoint(world_aim);
localToWorld = localOrigin;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::IncrementCameraInstance()
{
Check(this);
Check(&cameraManager);
currentCamera = cameraManager.IncrementIterator();
Check(currentCamera);
DEBUG_STREAM << currentCamera->GetCameraName() << std::endl << std::flush;
localOrigin = currentCamera->GetLocalOrigin();
localToWorld = localOrigin;
Point3D local_aim(0.0f, 0.0f, -1.0f);
Point3D world_aim;
world_aim.Multiply(local_aim, localToWorld);
AimCameraAtPoint(world_aim);
localToWorld = localOrigin;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
void
CameraShip::DecrementCameraInstance()
{
Check(this);
Check(&cameraManager);
currentCamera = cameraManager.DecrementIterator();
Check(currentCamera);
DEBUG_STREAM << currentCamera->GetCameraName() << std::endl << std::flush;
localOrigin = currentCamera->GetLocalOrigin();
localToWorld = localOrigin;
Point3D local_aim(0.0f, 0.0f, -1.0f);
Point3D world_aim;
world_aim.Multiply(local_aim, localToWorld);
AimCameraAtPoint(world_aim);
localToWorld = localOrigin;
Check_Fpu();
}
//#############################################################################
// Motion Support Functions
//
//#############################################################################
// Construction and Destruction Support
//
CameraShip::CameraShip(
CameraShip::MakeMessage *creation_message,
CameraShip::SharedData &virtual_data
):
Mover(creation_message, virtual_data)
{
SetValidFlag();
if (GetInstance() == ReplicantInstance)
{
return;
}
//
//----------------------------
// Initialize local variables
//----------------------------
//
goalEntity = NULL;
localOrigin = Origin::Identity;
focusOffset = Vector3D::Identity;
//
//------------------------------
// Initialize attributes
//------------------------------
//
mapRange = 1000.0; // number of meters across display
mapLinearPosition = NULL; // force nav display to use object's lin. pos
mapAngularPosition = NULL; // force nav display to not turn or tilt
//
//------------------------------
// Initialize the subsystemArray
//------------------------------
//
subsystemCount = BasicSubsystemCount;
Verify(!subsystemArray);
subsystemArray = new (Subsystem (*[subsystemCount]));
Register_Pointer(subsystemArray);
subsystemArray[ControlsMapperSubsystem] = NULL;
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Initialize the camera's viewpoint to the first camera
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
currentCamera = cameraManager.GetCurrent();
if (!currentCamera)
{
CreateCameraInstance();
}
Check(currentCamera);
localOrigin = currentCamera->GetLocalOrigin();
localToWorld = localOrigin;
Point3D local_aim(0.0f, 0.0f, -1.0f);
Point3D world_aim;
world_aim.Multiply(local_aim, localToWorld);
AimCameraAtPoint(world_aim);
localToWorld = localOrigin;
lastSwitch = Now();
timeOnCamera = 0.0f;
SetPerformance(&CameraShip::FollowGoal);
SetSimulationState(DefaultState);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
CameraShip*
CameraShip::Make(MakeMessage *creation_message)
{
return new CameraShip(creation_message);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
CameraShip::~CameraShip()
{
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Logical
CameraShip::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}