void MainPanel::Draw() const { FrameTimer loadTimer; glClear(GL_COLOR_BUFFER_BIT); engine.Draw(); if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(static_cast<int>(load * 100. + .5)) + "% GPU"; Color color = *GameData::Colors().Get("medium"); FontSet::Get(14).Draw(loadString, Point(10., Screen::Height() * -.5 + 5.), color); loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } } }
TEST(Timer, FrameTimer) { FrameTimer ft; for (int i = 0; i < 9; ++i) ft.newFrame(); std::this_thread::sleep_for(std::chrono::seconds(1)); ft.newFrame(); AM_LOG_INFO("Delta: %f", ft.getDelta()); AM_LOG_INFO("FPS: %f", ft.getFps()); AM_LOG_INFO("Frame Count: %d", ft.getFrameCount()); AM_LOG_INFO("\n"); ASSERT_TRUE(ft.getFrameCount() == 10); ASSERT_TRUE(std::fabs(ft.getFps() - 10.0) < 0.1); }
void MainPanel::Draw() { FrameTimer loadTimer; glClear(GL_COLOR_BUFFER_BIT); engine.Draw(); if(isDragging) { if(canDrag) { const Color &dragColor = *GameData::Colors().Get("drag select"); LineShader::Draw(dragSource, Point(dragSource.X(), dragPoint.Y()), .8f, dragColor); LineShader::Draw(Point(dragSource.X(), dragPoint.Y()), dragPoint, .8f, dragColor); LineShader::Draw(dragPoint, Point(dragPoint.X(), dragSource.Y()), .8f, dragColor); LineShader::Draw(Point(dragPoint.X(), dragSource.Y()), dragSource, .8f, dragColor); } else isDragging = false; } if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(lround(load * 100.)) + "% GPU"; const Color &color = *GameData::Colors().Get("medium"); FontSet::Get(14).Draw(loadString, Point(10., Screen::Height() * -.5 + 5.), color); loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } } }
int main() { extern void UnitTest_Math(); UnitTest_Math(); ScreenMode::Type screenMode = ScreenMode::Windowed; VertSync::Type vertSync = VertSync::Disable; GraphicsEngine& gfxEngine = GraphicsEngine::Instance(); gfxEngine.Initialize("Star Fox", (int)SCREEN_WIDTH, (int)SCREEN_HEIGHT, 32, screenMode, vertSync); // By default, when lighting is enabled, material parameters are used for shading, but vertex colors are ignored; // while the opposite is true when lighting is disabled. // Enabling GL_COLOR_MATERIAL tells OpenGL to substitute the current color for the ambient and for the diffuse material // color when doing lighting computations. glEnable(GL_COLOR_MATERIAL); GLUtil::SetBlending(false); GLUtil::SetDepthTesting(true); GLUtil::SetFrontFace(Winding::CounterClockwise, CullBackFace::True); GLUtil::SetLighting(true); GLUtil::SetShadeModel(ShadeModel::Smooth); GLUtil::SetTexturing(true); ProjectionInfo perspMain; perspMain.SetPerspective(45.f, SCREEN_WIDTH / SCREEN_HEIGHT, 1.0f, 10000.f); GLUtil::SetProjection(perspMain); GLfloat light_position[] = { 1.0, 1.0, 0.5, 0.0 }; // Directional glLightfv(GL_LIGHT0, GL_POSITION, light_position); glEnable(GL_LIGHT0); FrameTimer frameTimer; frameTimer.SetMinFPS(10.0f); //frameTimer.SetMaxFPS(60.f); KeyboardMgr& kbMgr = KeyboardMgr::Instance(); // Create SceneNodes SceneNodeWeakPtr pwCamera; { FbxLoader fbxLoader; fbxLoader.Init(); auto psShip = SceneNode::Create("Ship"); auto psStaticMesh = fbxLoader.LoadStaticMesh("data/Arwing_001.fbx"); psShip->AddComponent<StaticMeshComponent>()->Init(psStaticMesh); psShip->AddComponent<PlayerControlComponent>(); psShip->ModifyLocalToWorld().trans.y = 50.f; // The ship starts above the ground auto psGround = SceneNode::Create("Ground"); psGround->AddComponent<GroundComponent>(); auto psAnchor = SceneNode::Create("Anchor"); psAnchor->AddComponent<AnchorComponent>(); psAnchor->AttachChild(psShip); psAnchor->AttachChild(psGround); auto psCamera = SceneNode::Create("Camera"); //auto pCamerComponent = psCamera->AddComponent<OrbitTargetCameraComponent>(); auto pCamerComponent = psCamera->AddComponent<FollowShipCameraComponent>(); pCamerComponent->SetTarget(psShip); pwCamera = psCamera; // Create a bunch of randomly positioned buildings { const float32 firstZ = 5000.f; const float32 deltaZ = 2000.f; auto psStaticMesh = fbxLoader.LoadStaticMesh("data/Building1.fbx"); for (uint32 i = 0; i < 100; ++i) { auto psBuilding = SceneNode::Create(str_format("Building_%d", i)); psBuilding->AddComponent<StaticMeshComponent>()->Init(psStaticMesh); auto& mLocal = psBuilding->ModifyLocalToParent(); mLocal.trans.z = firstZ + deltaZ * i; mLocal.trans.x = MathEx::Rand(-500.f, 500.f); } psStaticMesh = nullptr; } fbxLoader.Shutdown(); } // Main game loop bool bQuit = false; while (!bQuit) { // Handle pause if ( !System::IsDebuggerAttached() && !gfxEngine.HasFocus() ) // Auto-pause when we window loses focus { frameTimer.SetPaused(true); } else if (kbMgr[vkeyPause].JustPressed()) { frameTimer.TogglePaused(); } // Frame time update frameTimer.Update(); // Update and apply time scale //@TODO: Fold this into frameTimer? static float32 timeScales[] = {0.1f, 0.25f, 0.5f, 1.f, 2.f, 4.f}; static int32 timeScaleIndex = 3; if (kbMgr[vkeyTimeScaleInc].JustPressed()) { timeScaleIndex = MathEx::Min(timeScaleIndex+1, (int)ARRAYSIZE(timeScales)-1); } else if (kbMgr[vkeyTimeScaleDec].JustPressed()) { timeScaleIndex = MathEx::Max(timeScaleIndex-1, 0); } else if (kbMgr[vkeyTimeScaleReset].JustPressed()) { timeScaleIndex = 3; } const float32 timeScale = timeScales[timeScaleIndex]; const float32 deltaTime = timeScale * frameTimer.GetFrameDeltaTime(); gfxEngine.SetTitle( str_format("Star Fox (Real Time: %.2f, Game Time: %.2f, GameDT: %.4f (scale: %.2f), FPS: %.2f)", frameTimer.GetRealElapsedTime(), frameTimer.GetElapsedTime(), frameTimer.GetFrameDeltaTime(), timeScale, frameTimer.GetFPS()).c_str() ); kbMgr.Update(deltaTime); if (kbMgr[VK_CONTROL].IsDown()) { if (kbMgr[VK_F1].JustPressed()) { static bool bLighting = glIsEnabled(GL_LIGHTING) == GL_TRUE; bLighting = !bLighting; bLighting? glEnable(GL_LIGHTING) : glDisable(GL_LIGHTING); } if (kbMgr[VK_F2].JustPressed()) { //static bool bSmoothShading = GLUtil::GetShadeModel() == ShadeModel::Smooth; //bSmoothShading = !bSmoothShading; //GLUtil::SetShadeModel(bSmoothShading? ShadeModel::Smooth : ShadeModel::Flat); g_drawSockets = !g_drawSockets; } if (kbMgr[VK_F3].JustPressed()) { g_drawNormals = !g_drawNormals; } if (kbMgr[VK_F4].JustPressed()) { GLUtil::SetTexturing( !GLUtil::GetTexturing() ); } if (kbMgr[VK_F5].JustPressed()) { static bool bWireframe = false; bWireframe = !bWireframe; GLUtil::SetWireFrame(bWireframe); } if (kbMgr[VK_F6].JustPressed()) { g_renderSceneGraph = !g_renderSceneGraph; } } // UPDATE std::vector<SceneNodeWeakPtr> sceneNodeList = SceneNode::GetAllNodesSnapshot(); if ( !frameTimer.IsPaused() ) { for (auto pwNode : sceneNodeList) { if (const auto& psNode = pwNode.lock()) psNode->Update(deltaTime); } } SceneNode::ValidateSceneGraph(); // RENDER glClearColor(0.f, 0.f, 0.3f, 0.f); glClearDepth(1.f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Load inverse camera matrix so future transforms are in camera space glMatrixMode(GL_MODELVIEW); //glLoadIdentity(); Matrix43 mInvCam = pwCamera.lock()->GetLocalToWorld(); //assert(mInvCam.IsOrthogonal()); mInvCam.axisZ = -mInvCam.axisZ; // Game -> OpenGL (flip Z axis) mInvCam.InvertSRT(); GLfloat mCamGL[16]; GLUtil::Matrix43ToGLMatrix(mInvCam, mCamGL); glLoadMatrixf(mCamGL); // Render scene nodes in camera space for (auto pwNode : sceneNodeList) { if (const auto& psNode = pwNode.lock()) { psNode->Render(); } } // Render debug objects g_debugDrawManager.Render(); // Render scene graph if (g_renderSceneGraph) { for (auto pwNode : sceneNodeList) { if (const auto& psNode = pwNode.lock()) { if (is_weak_to_shared_ptr(pwCamera, psNode)) continue; GLUtil::PushAndMultMatrix(psNode->GetLocalToWorld()); auto pQuadric = gluNewQuadric(); glColor3f(1.f, 0.f, 0.f); gluSphere(pQuadric, 10.f, 8, 8); gluDeleteQuadric(pQuadric); for (const auto& pwChildNode : psNode->GetChildren()) { if (const auto& psChildNode = pwChildNode.lock()) { const auto& mChildLocal = psChildNode->GetLocalToParent(); glBegin(GL_LINES); glVertex3fv(Vector3::Zero().v); glVertex3fv(mChildLocal.trans.v); glEnd(); } } glPopMatrix(); } } } // Flip buffers, process msgs, etc. gfxEngine.Update(bQuit); // Handle frame stepping static bool stepFrame = false; const bool inputStepSingleFrame = kbMgr[vkeyStepFrame].JustPressed(); const bool inputStepMultipleFrames = kbMgr[vkeyStepFrameRepeatModifier].IsDown() && kbMgr[vkeyStepFrame].IsDown(); if ( !stepFrame && (inputStepSingleFrame || inputStepMultipleFrames) ) { // Unpause for one frame stepFrame = true; frameTimer.SetPaused(false); } else if ( stepFrame && !inputStepMultipleFrames ) { // Go back to being paused stepFrame = false; frameTimer.SetPaused(true); } } SceneNode::DestroyAllNodes(); gfxEngine.Shutdown(); }
void Engine::CalculateStep() { FrameTimer loadTimer; // Clear the list of objects to draw. draw[calcTickTock].Clear(step); radar[calcTickTock].Clear(); if(!player.GetSystem()) return; // Now, all the ships must decide what they are doing next. ai.Step(ships, player); const Ship *flagship = player.Flagship(); bool wasHyperspacing = (flagship && flagship->IsEnteringHyperspace()); // Now, move all the ships. We must finish moving all of them before any of // them fire, or their turrets will be targeting where a given ship was // instead of where it is now. This is also where ships get deleted, and // where they may create explosions if they are dying. for(auto it = ships.begin(); it != ships.end(); ) { // Give the ship the list of effects so that if it is dying, it can // create explosions. Eventually ships might create other effects too. // Note that engine flares are handled separately, so that they will be // drawn immediately under the ship. if(!(*it)->Move(effects)) it = ships.erase(it); else { // Boarding: bool autoPlunder = !(*it)->GetGovernment()->IsPlayer(); shared_ptr<Ship> victim = (*it)->Board(autoPlunder); if(victim) eventQueue.emplace_back(*it, victim, (*it)->GetGovernment()->IsEnemy(victim->GetGovernment()) ? ShipEvent::BOARD : ShipEvent::ASSIST); ++it; } } if(!wasHyperspacing && flagship && flagship->IsEnteringHyperspace()) Audio::Play(Audio::Get(flagship->Attributes().Get("jump drive") ? "jump_drive" : "hyperspace")); // If the player has entered a new system, update the asteroids, etc. if(wasHyperspacing && !flagship->IsEnteringHyperspace()) { doFlash = true; EnterSystem(); } else if(flagship && player.GetSystem() != flagship->GetSystem()) { // Wormhole travel: player.ClearTravel(); doFlash = true; EnterSystem(); } // Now we know the player's current position. Draw the planets. Point center; Point centerVelocity; if(flagship) { center = flagship->Position(); centerVelocity = flagship->Velocity(); } else if(player.GetPlanet()) { for(const StellarObject &object : player.GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) center = object.Position(); } if(!flagship) doClick = false; for(const StellarObject &object : player.GetSystem()->Objects()) if(!object.GetSprite().IsEmpty()) { Point position = object.Position(); Point unit = object.Unit(); position -= center; int type = object.IsStar() ? Radar::SPECIAL : !object.GetPlanet() ? Radar::INACTIVE : object.GetPlanet()->IsWormhole() ? Radar::ANOMALOUS : object.GetPlanet()->CanLand() ? Radar::FRIENDLY : Radar::HOSTILE; double r = max(2., object.Radius() * .03 + .5); // Don't apply motion blur to very large planets and stars. bool isBig = (object.GetSprite().Width() >= 280); draw[calcTickTock].Add(object.GetSprite(), position, unit, isBig ? Point() : -centerVelocity); radar[calcTickTock].Add(type, position, r, r - 1.); if(doClick && object.GetPlanet() && (clickPoint - position).Length() < object.Radius()) player.Flagship()->SetTargetPlanet(&object); } // Add all neighboring systems to the radar. const System *targetSystem = flagship ? flagship->GetTargetSystem() : nullptr; for(const System *system : player.GetSystem()->Links()) radar[calcTickTock].AddPointer( (system == targetSystem) ? Radar::SPECIAL : Radar::INACTIVE, system->Position() - player.GetSystem()->Position()); // Now that the planets have been drawn, we can draw the asteroids on top // of them. This could be done later, as long as it is done before the // collision detection. asteroids.Step(); asteroids.Draw(draw[calcTickTock], center, centerVelocity); // Move existing projectiles. Do this before ships fire, which will create // new projectiles, since those should just stay where they are created for // this turn. This is also where projectiles get deleted, which may also // result in a "die" effect or a sub-munition being created. We could not // move the projectiles before this because some of them are homing and need // to know the current positions of the ships. list<Projectile> newProjectiles; for(auto it = projectiles.begin(); it != projectiles.end(); ) { if(!it->Move(effects)) { it->MakeSubmunitions(newProjectiles); it = projectiles.erase(it); } else ++it; } projectiles.splice(projectiles.end(), newProjectiles); // Keep track of the relative strength of each government in this system. Do // not add more ships to make a winning team even stronger. This is mostly // to avoid having the player get mobbed by pirates, say, if they hang out // in one system for too long. map<const Government *, int64_t> strength; // Now, ships fire new projectiles, which includes launching fighters. If an // anti-missile system is ready to fire, it does not actually fire unless a // missile is detected in range during collision detection, below. vector<Ship *> hasAntiMissile; for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem()) { strength[ship->GetGovernment()] += ship->Cost(); // Note: if a ship "fires" a fighter, that fighter was already in // existence and under the control of the same AI as the ship, but // its system was null to mark that it was not active. ship->Launch(ships); if(ship->Fire(projectiles, effects)) hasAntiMissile.push_back(ship.get()); int scan = ship->Scan(); if(scan) { shared_ptr<Ship> target = ship->GetTargetShip(); if(target && target->IsTargetable()) eventQueue.emplace_back(ship, target, scan); } // This is a good opportunity to draw all the ships in system. if(ship->GetSprite().IsEmpty()) continue; Point position = ship->Position() - center; if(ship->IsThrusting()) { for(const Point &point : ship->EnginePoints()) { Point pos = ship->Facing().Rotate(point) * .5 * ship->Zoom() + position; for(const auto &it : ship->Attributes().FlareSprites()) for(int i = 0; i < it.second; ++i) { if(ship->Cloaking()) { draw[calcTickTock].Add( it.first.GetSprite(), pos, ship->Unit(), ship->Velocity() - centerVelocity, ship->Cloaking()); } else { draw[calcTickTock].Add( it.first, pos, ship->Unit(), ship->Velocity() - centerVelocity); } } } if(ship.get() == flagship) { for(const auto &it : ship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first); } } bool isPlayer = ship->GetGovernment()->IsPlayer(); if(ship->Cloaking()) { if(isPlayer) { Animation animation = ship->GetSprite(); animation.SetSwizzle(7); draw[calcTickTock].Add( animation, position, ship->Unit(), ship->Velocity() - centerVelocity); } draw[calcTickTock].Add( ship->GetSprite().GetSprite(), position, ship->Unit(), ship->Velocity() - centerVelocity, ship->Cloaking(), ship->GetSprite().GetSwizzle()); } else { draw[calcTickTock].Add( ship->GetSprite(), position, ship->Unit(), ship->Velocity() - centerVelocity); } // Do not show cloaked ships on the radar, except the player's ships. if(ship->Cloaking() == 1. && !isPlayer) continue; if(doClick && &*ship != player.Flagship()) { const Mask &mask = ship->GetSprite().GetMask(step); if(mask.WithinRange(clickPoint - position, ship->Facing(), 50.)) { player.Flagship()->SetTargetShip(ship); // If we've found an enemy within the click zone, favor // targeting it rather than any other ship. Otherwise, keep // checking for hits because another ship might be an enemy. if(ship->GetGovernment()->IsEnemy()) doClick = false; } } auto target = ship->GetTargetShip(); radar[calcTickTock].Add( (ship->GetGovernment()->IsPlayer() || ship->GetPersonality().IsEscort()) ? Radar::PLAYER : (ship->IsDisabled() || ship->IsOverheated()) ? Radar::INACTIVE : !ship->GetGovernment()->IsEnemy() ? Radar::FRIENDLY : (target && target->GetGovernment()->IsPlayer()) ? Radar::HOSTILE : Radar::UNFRIENDLY, position, sqrt(ship->GetSprite().Width() + ship->GetSprite().Height()) * .1 + .5); } // Collision detection: for(Projectile &projectile : projectiles) { // The asteroids can collide with projectiles, the same as any other // object. If the asteroid turns out to be closer than the ship, it // shields the ship (unless the projectile has blast radius). Point hitVelocity; double closestHit = 0.; shared_ptr<Ship> hit; const Government *gov = projectile.GetGovernment(); // If this "projectile" is a ship explosion, it always explodes. if(gov) { closestHit = asteroids.Collide(projectile, step, &hitVelocity); // Projectiles can only collide with ships that are in the current // system and are not landing, and that are hostile to this projectile. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && !ship->IsLanding() && ship->Cloaking() < 1.) { if(ship.get() != projectile.Target() && !gov->IsEnemy(ship->GetGovernment())) continue; // This returns a value of 0 if the projectile has a trigger // radius and the ship is within it. double range = projectile.CheckCollision(*ship, step); if(range < closestHit) { closestHit = range; hit = ship; hitVelocity = ship->Velocity(); } } } if(closestHit < 1.) { // Create the explosion the given distance along the projectile's // motion path for this step. projectile.Explode(effects, closestHit, hitVelocity); // If this projectile has a blast radius, find all ships within its // radius. Otherwise, only one is damaged. if(projectile.HasBlastRadius()) { // Even friendly ships can be hit by the blast. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && ship->Zoom() == 1.) if(projectile.InBlastRadius(*ship, step)) { int eventType = ship->TakeDamage(projectile, ship != hit); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), ship, eventType); } } else if(hit) { int eventType = hit->TakeDamage(projectile); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), hit, eventType); } if(hit) DoGrudge(hit, projectile.GetGovernment()); } else if(projectile.MissileStrength()) { radar[calcTickTock].Add( Radar::SPECIAL, projectile.Position() - center, 1.); // If the projectile did not hit anything, give the anti-missile // systems a chance to shoot it down. for(Ship *ship : hasAntiMissile) if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()) || ship->GetGovernment()->IsEnemy(gov)) if(ship->FireAntiMissile(projectile, effects)) { projectile.Kill(); break; } } else if(projectile.HasBlastRadius()) radar[calcTickTock].Add( Radar::SPECIAL, projectile.Position() - center, 1.8); // Now, we can draw the projectile. The motion blur should be reduced // depending on how much motion blur is in the sprite itself: double innateVelocity = 2. * projectile.GetWeapon().Velocity(); Point relativeVelocity = projectile.Velocity() - centerVelocity - projectile.Unit() * innateVelocity; draw[calcTickTock].Add( projectile.GetSprite(), projectile.Position() - center + .5 * projectile.Velocity(), projectile.Unit(), relativeVelocity, closestHit); } // Finally, draw all the effects, and then move them (because their motion // is not dependent on anything else, and this way we do all the work on // them in a single place. for(auto it = effects.begin(); it != effects.end(); ) { draw[calcTickTock].Add( it->GetSprite(), it->Position() - center, it->Unit()); if(!it->Move()) it = effects.erase(it); else ++it; } // Add incoming ships. for(const System::FleetProbability &fleet : player.GetSystem()->Fleets()) if(!Random::Int(fleet.Period())) { const Government *gov = fleet.Get()->GetGovernment(); if(!gov) continue; int64_t enemyStrength = 0; for(const auto &it : strength) if(gov->IsEnemy(it.first)) enemyStrength += it.second; if(enemyStrength && strength[gov] > 2 * enemyStrength) continue; fleet.Get()->Enter(*player.GetSystem(), ships); } if(!Random::Int(36000)) { // Loop through all persons once to see if there are any who can enter // this system. int sum = 0; for(const auto &it : GameData::Persons()) sum += it.second.Frequency(player.GetSystem()); if(sum) { // Adjustment factor: special persons will appear once every ten // minutes, but much less frequently if the game only specifies a // few of them. This way, they will become more common as I add // more, without needing to change the 10-minute constant above. sum = Random::Int(sum + 1000); for(const auto &it : GameData::Persons()) { const Person &person = it.second; sum -= person.Frequency(player.GetSystem()); if(sum < 0) { shared_ptr<Ship> ship = person.GetShip(); ship->Recharge(true); ship->SetName(it.first); ship->SetGovernment(person.GetGovernment()); ship->SetPersonality(person.GetPersonality()); ship->SetHail(person.GetHail()); Fleet::Enter(*player.GetSystem(), *ship); ships.push_front(ship); break; } } } } // Occasionally have some ship hail you. if(!Random::Int(600) && !ships.empty()) { shared_ptr<Ship> source; unsigned i = Random::Int(ships.size()); for(const shared_ptr<Ship> &it : ships) if(!i--) { source = it; break; } if(source->GetGovernment() && !source->GetGovernment()->IsPlayer() && !source->IsDisabled()) { string message = source->GetHail(); if(!message.empty() && source->GetSystem() == player.GetSystem()) Messages::Add(source->GetGovernment()->GetName() + " ship \"" + source->Name() + "\": " + message); } } // A mouse click should only be active for a single step. doClick = false; // Keep track of how much of the CPU time we are using. loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } }
void Engine::CalculateStep() { FrameTimer loadTimer; // Clear the list of objects to draw. draw[calcTickTock].Clear(step); radar[calcTickTock].Clear(); if(!player.GetSystem()) return; // Now, all the ships must decide what they are doing next. ai.Step(ships, player); const Ship *flagship = player.Flagship(); bool wasHyperspacing = (flagship && flagship->IsEnteringHyperspace()); // Now, move all the ships. We must finish moving all of them before any of // them fire, or their turrets will be targeting where a given ship was // instead of where it is now. This is also where ships get deleted, and // where they may create explosions if they are dying. for(auto it = ships.begin(); it != ships.end(); ) { int hyperspaceType = (*it)->HyperspaceType(); bool wasHere = (flagship && (*it)->GetSystem() == flagship->GetSystem()); bool wasHyperspacing = (*it)->IsHyperspacing(); // Give the ship the list of effects so that it can draw explosions, // ion sparks, jump drive flashes, etc. if(!(*it)->Move(effects, flotsam)) { // If Move() returns false, it means the ship should be removed from // play. That may be because it was destroyed, because it is an // ordinary ship that has been out of system for long enough to be // "forgotten," or because it is a fighter that just docked with its // mothership. Report it destroyed if that's really what happened: if((*it)->IsDestroyed()) eventQueue.emplace_back(nullptr, *it, ShipEvent::DESTROY); it = ships.erase(it); } else { if(&**it != flagship) { // Did this ship just begin hyperspacing? if(wasHere && !wasHyperspacing && (*it)->IsHyperspacing()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump out" : "hyperdrive out"), (*it)->Position()); // Did this ship just jump into the player's system? if(!wasHere && flagship && (*it)->GetSystem() == flagship->GetSystem()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump in" : "hyperdrive in"), (*it)->Position()); } // Boarding: bool autoPlunder = !(*it)->GetGovernment()->IsPlayer(); shared_ptr<Ship> victim = (*it)->Board(autoPlunder); if(victim) eventQueue.emplace_back(*it, victim, (*it)->GetGovernment()->IsEnemy(victim->GetGovernment()) ? ShipEvent::BOARD : ShipEvent::ASSIST); ++it; } } if(!wasHyperspacing && flagship && flagship->IsEnteringHyperspace()) Audio::Play(Audio::Get(flagship->HyperspaceType() >= 200 ? "jump drive" : "hyperdrive")); if(flagship && player.GetSystem() != flagship->GetSystem()) { // Wormhole travel: if(!wasHyperspacing) for(const auto &it : player.GetSystem()->Objects()) if(it.GetPlanet() && it.GetPlanet()->IsWormhole() && it.GetPlanet()->WormholeDestination(player.GetSystem()) == flagship->GetSystem()) player.Visit(it.GetPlanet()); doFlash = Preferences::Has("Show hyperspace flash"); player.SetSystem(flagship->GetSystem()); EnterSystem(); } // Draw the planets. Point newCenter = center; Point newCenterVelocity; if(flagship) { newCenter = flagship->Position(); newCenterVelocity = flagship->Velocity(); } else doClick = false; draw[calcTickTock].SetCenter(newCenter, newCenterVelocity); radar[calcTickTock].SetCenter(newCenter); for(const StellarObject &object : player.GetSystem()->Objects()) if(object.HasSprite()) { // Don't apply motion blur to very large planets and stars. if(object.Width() >= 280.) draw[calcTickTock].AddUnblurred(object); else draw[calcTickTock].Add(object); double r = max(2., object.Radius() * .03 + .5); radar[calcTickTock].Add(RadarType(object), object.Position(), r, r - 1.); if(object.GetPlanet()) object.GetPlanet()->DeployDefense(ships); Point position = object.Position() - newCenter; if(doClick && object.GetPlanet() && (clickPoint - position).Length() < object.Radius()) { if(&object == player.Flagship()->GetTargetPlanet()) { if(!object.GetPlanet()->CanLand()) Messages::Add("The authorities on " + object.GetPlanet()->Name() + " refuse to let you land."); else { clickCommands |= Command::LAND; Messages::Add("Landing on " + object.GetPlanet()->Name() + "."); } } else player.Flagship()->SetTargetPlanet(&object); } } // Add all neighboring systems to the radar. const System *targetSystem = flagship ? flagship->GetTargetSystem() : nullptr; const vector<const System *> &links = (flagship && flagship->Attributes().Get("jump drive")) ? player.GetSystem()->Neighbors() : player.GetSystem()->Links(); for(const System *system : links) radar[calcTickTock].AddPointer( (system == targetSystem) ? Radar::SPECIAL : Radar::INACTIVE, system->Position() - player.GetSystem()->Position()); // Now that the planets have been drawn, we can draw the asteroids on top // of them. This could be done later, as long as it is done before the // collision detection. asteroids.Step(); asteroids.Draw(draw[calcTickTock], newCenter); // Move existing projectiles. Do this before ships fire, which will create // new projectiles, since those should just stay where they are created for // this turn. This is also where projectiles get deleted, which may also // result in a "die" effect or a sub-munition being created. We could not // move the projectiles before this because some of them are homing and need // to know the current positions of the ships. list<Projectile> newProjectiles; for(auto it = projectiles.begin(); it != projectiles.end(); ) { if(!it->Move(effects)) { it->MakeSubmunitions(newProjectiles); it = projectiles.erase(it); } else ++it; } projectiles.splice(projectiles.end(), newProjectiles); // Move the flotsam, which should be drawn underneath the ships. for(auto it = flotsam.begin(); it != flotsam.end(); ) { if(!it->Move(effects)) { it = flotsam.erase(it); continue; } Ship *collector = nullptr; for(const shared_ptr<Ship> &ship : ships) { if(ship->GetSystem() != player.GetSystem() || ship->CannotAct()) continue; if(ship.get() == it->Source() || ship->Cargo().Free() < it->UnitSize()) continue; const Mask &mask = ship->GetMask(step); if(mask.Contains(it->Position() - ship->Position(), ship->Facing())) { collector = ship.get(); break; } } if(collector) { string name; if(collector->IsYours()) { if(collector->GetParent()) name = "Your ship \"" + collector->Name() + "\" picked up "; else name = "You picked up "; } string commodity; int amount = 0; if(it->OutfitType()) { amount = collector->Cargo().Add(it->OutfitType(), it->Count()); if(!name.empty()) { if(it->OutfitType()->Get("installable") < 0.) commodity = it->OutfitType()->Name(); else Messages::Add(name + Format::Number(amount) + " " + it->OutfitType()->Name() + (amount == 1 ? "." : "s.")); } } else { amount = collector->Cargo().Add(it->CommodityType(), it->Count()); if(!name.empty()) commodity = it->CommodityType(); } if(!commodity.empty()) Messages::Add(name + (amount == 1 ? "a ton" : Format::Number(amount) + " tons") + " of " + Format::LowerCase(commodity) + "."); it = flotsam.erase(it); continue; } // Draw this flotsam. draw[calcTickTock].Add(*it); ++it; } // Keep track of the relative strength of each government in this system. Do // not add more ships to make a winning team even stronger. This is mostly // to avoid having the player get mobbed by pirates, say, if they hang out // in one system for too long. map<const Government *, int64_t> strength; // Now, ships fire new projectiles, which includes launching fighters. If an // anti-missile system is ready to fire, it does not actually fire unless a // missile is detected in range during collision detection, below. vector<Ship *> hasAntiMissile; double clickRange = 50.; const Ship *previousTarget = nullptr; const Ship *clickTarget = nullptr; if(player.Flagship() && player.Flagship()->GetTargetShip()) previousTarget = &*player.Flagship()->GetTargetShip(); bool showFlagship = false; bool hasHostiles = false; for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem()) { strength[ship->GetGovernment()] += ship->Cost(); // Note: if a ship "fires" a fighter, that fighter was already in // existence and under the control of the same AI as the ship, but // its system was null to mark that it was not active. ship->Launch(ships); if(ship->Fire(projectiles, effects)) hasAntiMissile.push_back(ship.get()); int scan = ship->Scan(); if(scan) { shared_ptr<Ship> target = ship->GetTargetShip(); if(target && target->IsTargetable()) eventQueue.emplace_back(ship, target, scan); } // This is a good opportunity to draw all the ships in system. if(!ship->HasSprite()) continue; // Draw the flagship separately, on top of everything else. if(ship.get() != flagship) { AddSprites(*ship); if(ship->IsThrusting()) { for(const auto &it : ship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first, ship->Position()); } } else showFlagship = true; // Do not show cloaked ships on the radar, except the player's ships. bool isPlayer = ship->GetGovernment()->IsPlayer(); if(ship->Cloaking() == 1. && !isPlayer) continue; if(doClick && &*ship != player.Flagship() && ship->IsTargetable()) { Point position = ship->Position() - newCenter; const Mask &mask = ship->GetMask(step); double range = mask.Range(clickPoint - position, ship->Facing()); if(range <= clickRange) { clickRange = range; clickTarget = ship.get(); player.Flagship()->SetTargetShip(ship); // If we've found an enemy within the click zone, favor // targeting it rather than any other ship. Otherwise, keep // checking for hits because another ship might be an enemy. if(!range && ship->GetGovernment()->IsEnemy()) doClick = false; } } double size = sqrt(ship->Width() + ship->Height()) * .14 + .5; bool isYourTarget = (flagship && ship == flagship->GetTargetShip()); int type = RadarType(*ship); hasHostiles |= (type == Radar::HOSTILE); radar[calcTickTock].Add(isYourTarget ? Radar::SPECIAL : type, ship->Position(), size); } if(flagship && showFlagship) { AddSprites(*flagship); if(flagship->IsThrusting()) { for(const auto &it : flagship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first); } } if(clickTarget && clickTarget == previousTarget) clickCommands |= Command::BOARD; if(alarmTime) --alarmTime; else if(hasHostiles && !hadHostiles) { if(Preferences::Has("Warning siren")) Audio::Play(Audio::Get("alarm")); alarmTime = 180; hadHostiles = true; } else if(!hasHostiles) hadHostiles = false; // Collision detection: if(grudgeTime) --grudgeTime; for(Projectile &projectile : projectiles) { // The asteroids can collide with projectiles, the same as any other // object. If the asteroid turns out to be closer than the ship, it // shields the ship (unless the projectile has a blast radius). Point hitVelocity; double closestHit = 0.; shared_ptr<Ship> hit; const Government *gov = projectile.GetGovernment(); // If this "projectile" is a ship explosion, it always explodes. if(gov) { closestHit = asteroids.Collide(projectile, step, &hitVelocity); // Projectiles can only collide with ships that are in the current // system and are not landing, and that are hostile to this projectile. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && !ship->IsLanding() && ship->Cloaking() < 1.) { if(ship.get() != projectile.Target() && !gov->IsEnemy(ship->GetGovernment())) continue; // This returns a value of 0 if the projectile has a trigger // radius and the ship is within it. double range = projectile.CheckCollision(*ship, step); if(range < closestHit) { closestHit = range; hit = ship; hitVelocity = ship->Velocity(); } } } if(closestHit < 1.) { // Create the explosion the given distance along the projectile's // motion path for this step. projectile.Explode(effects, closestHit, hitVelocity); // If this projectile has a blast radius, find all ships within its // radius. Otherwise, only one is damaged. if(projectile.HasBlastRadius()) { // Even friendly ships can be hit by the blast. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && ship->Zoom() == 1.) if(projectile.InBlastRadius(*ship, step, closestHit)) { int eventType = ship->TakeDamage(projectile, ship != hit); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), ship, eventType); } } else if(hit) { int eventType = hit->TakeDamage(projectile); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), hit, eventType); } if(hit) DoGrudge(hit, projectile.GetGovernment()); } else if(projectile.MissileStrength()) { bool isEnemy = projectile.GetGovernment() && projectile.GetGovernment()->IsEnemy(); radar[calcTickTock].Add( isEnemy ? Radar::SPECIAL : Radar::INACTIVE, projectile.Position(), 1.); // If the projectile did not hit anything, give the anti-missile // systems a chance to shoot it down. for(Ship *ship : hasAntiMissile) if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()) || ship->GetGovernment()->IsEnemy(gov)) if(ship->FireAntiMissile(projectile, effects)) { projectile.Kill(); break; } } else if(projectile.HasBlastRadius()) radar[calcTickTock].Add(Radar::SPECIAL, projectile.Position(), 1.8); // Now, we can draw the projectile. The motion blur should be reduced // depending on how much motion blur is in the sprite itself: double innateVelocity = 2. * projectile.GetWeapon().Velocity(); Point relativeVelocity = projectile.Velocity() - projectile.Unit() * innateVelocity; draw[calcTickTock].AddProjectile(projectile, relativeVelocity, closestHit); } // Finally, draw all the effects, and then move them (because their motion // is not dependent on anything else, and this way we do all the work on // them in a single place. for(auto it = effects.begin(); it != effects.end(); ) { draw[calcTickTock].AddUnblurred(*it); if(!it->Move()) it = effects.erase(it); else ++it; } // Add incoming ships. for(const System::FleetProbability &fleet : player.GetSystem()->Fleets()) if(!Random::Int(fleet.Period())) { const Government *gov = fleet.Get()->GetGovernment(); if(!gov) continue; int64_t enemyStrength = 0; for(const auto &it : strength) if(gov->IsEnemy(it.first)) enemyStrength += it.second; if(enemyStrength && strength[gov] > 2 * enemyStrength) continue; fleet.Get()->Enter(*player.GetSystem(), ships); } if(!Random::Int(36000) && !player.GetSystem()->Links().empty()) { // Loop through all persons once to see if there are any who can enter // this system. int sum = 0; for(const auto &it : GameData::Persons()) sum += it.second.Frequency(player.GetSystem()); if(sum) { // Adjustment factor: special persons will appear once every ten // minutes, but much less frequently if the game only specifies a // few of them. This way, they will become more common as I add // more, without needing to change the 10-minute constant above. sum = Random::Int(sum + 1000); for(const auto &it : GameData::Persons()) { const Person &person = it.second; sum -= person.Frequency(player.GetSystem()); if(sum < 0) { shared_ptr<Ship> ship = person.GetShip(); ship->Recharge(); ship->SetName(it.first); ship->SetGovernment(person.GetGovernment()); ship->SetPersonality(person.GetPersonality()); ship->SetHail(person.GetHail()); Fleet::Enter(*player.GetSystem(), *ship); ships.push_front(ship); break; } } } } // Occasionally have some ship hail you. if(!Random::Int(600) && !ships.empty()) { shared_ptr<Ship> source; unsigned i = Random::Int(ships.size()); for(const shared_ptr<Ship> &it : ships) if(!i--) { source = it; break; } if(source->GetGovernment() && !source->GetGovernment()->IsPlayer() && !source->IsDisabled() && source->Crew()) { string message = source->GetHail(); if(!message.empty() && source->GetSystem() == player.GetSystem()) Messages::Add(source->GetGovernment()->GetName() + " ship \"" + source->Name() + "\": " + message); } } // A mouse click should only be active for a single step. doClick = false; // Keep track of how much of the CPU time we are using. loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } }