Class MX_PongGame extends MX_MiniGameBase; var int Score,NumPlays; var vector PlayerPad,EnemyPad,BallPos,BallVel,BallTrajectory; var float ScreenHeight,BallWidth; var enum EBallHeading { BH_ToPlayer, BH_ToEnemy, BH_None, } BallHeading; var array<SoundCue> HitSoundsA,HitSoundsB; var SoundCue MissSound[2]; var int HitSoundIndex[2]; var float EnemyPadVel,AITactic,AITacticTimer,AITrajOffset; var bool bAIRandom; const PadWidth=0.015; const PadHeight=0.15; const LevelBoarderSize=0.05; const PadMoveLimit=0.125; const BallHeight=0.03; const InverseXOr=879379227; function Init() { local int i; Super.Init(); if (Data!=0) i = Data ^ InverseXOr; Score = (i >> 16) & 32767; if (Score>31767) Score = Score-32768; NumPlays = i & 65535; SetAIRating(); } final function UpdateScores() { Data = (((Max(Score,-1000) & 32767) << 16) | (NumPlays & 65535)) ^ InverseXOr; SaveConfig(); } function SetFXTrack(Object O) { local ObjectReferencer R; local int i; if (SoundCue(O)!=None) { HitSoundsA.AddItem(SoundCue(O)); HitSoundsB.AddItem(SoundCue(O)); } else if (ObjectReferencer(O)!=None) { R = ObjectReferencer(O); if (R.ReferencedObjects.Length<2) return; MissSound[0] = SoundCue(R.ReferencedObjects[0]); MissSound[1] = SoundCue(R.ReferencedObjects[1]); for (i=2; i<R.ReferencedObjects.Length; ++i) { if ((i & 1)==0) HitSoundsA.AddItem(SoundCue(R.ReferencedObjects[i])); else HitSoundsB.AddItem(SoundCue(R.ReferencedObjects[i])); } } } function StartGame() { Super.StartGame(); SetTimer(2,false,'RespawnBall',Self); } function Render(float XPos, float YPos, float XSize, float YSize) { local float H,W; ScreenHeight = YSize; // Score H = WorldInfo.RealTimeSeconds * 0.6; Canvas.Font = Canvas.GetDefaultCanvasFont(); W = FMin(YSize/200.f,3.f); if (!bGameStarted) { Canvas.SetDrawColor(128,64,64,Abs(Sin(H))*96.f+128); Canvas.SetPos(XPos+XSize*0.4,YPos+YSize*0.2); Canvas.DrawText("Press Fire to start pong",,W,W); } else { Canvas.SetDrawColor(255,255,128,Abs(Sin(H))*96.f); Canvas.SetPos(XPos+XSize*0.2,YPos+YSize*0.22); Canvas.DrawText("Score: "$string(Score),,W,W); Canvas.SetPos(XPos+XSize*0.2,YPos+YSize*0.68); Canvas.DrawText("Plays: "$string(NumPlays),,W,W); } // Borders Canvas.SetDrawColor(Abs(Sin(H))*255.f,Abs(Sin(H+1.25))*255.f,Abs(Sin(H+2.35))*255.f,255); Canvas.SetPos(XPos,YPos); Canvas.DrawTile(Canvas.DefaultTexture,XSize,YSize*LevelBoarderSize,0,0,1,1); Canvas.SetPos(XPos,YPos+YSize*(1.f-LevelBoarderSize)); Canvas.DrawTile(Canvas.DefaultTexture,XSize,YSize*LevelBoarderSize,0,0,1,1); // Player H = PadHeight*YSize; W = PadWidth*XSize; Canvas.SetDrawColor(128,255,128,255); Canvas.SetPos(XPos+PlayerPad.X*XSize,YPos+PlayerPad.Y*YSize-H*0.5); Canvas.DrawTile(Canvas.DefaultTexture,W,H,0,0,1,1); // Enemy Canvas.SetDrawColor(255,68,68,255); Canvas.SetPos(XPos+EnemyPad.X*XSize-W,YPos+EnemyPad.Y*YSize-H*0.5); Canvas.DrawTile(Canvas.DefaultTexture,W,H,0,0,1,1); // Pong ball Canvas.SetDrawColor(255,255,86,255); BallWidth = BallHeight*(YSize/XSize); H = BallHeight*YSize; W = H*0.5; Canvas.SetPos(XPos+BallPos.X*XSize-W,YPos+BallPos.Y*YSize-W); Canvas.DrawTile(Canvas.DefaultTexture,H,H,0,0,1,1); // Trajectory preview ball /*Canvas.SetDrawColor(255,255,86,64); Canvas.SetPos(XPos+BallTrajectory.X*XSize-W,YPos+BallTrajectory.Y*YSize-W); Canvas.DrawTile(Canvas.DefaultTexture,H,H,0,0,1,1);*/ } function UpdateMouse(float X, float Y) { Y /= (ScreenHeight*8/Sensitivity); PlayerPad.Y = FClamp(PlayerPad.Y-Y,PadMoveLimit,1.f-PadMoveLimit); } function SetMouse(float X, float Y) { PlayerPad.Y = FClamp(Y/ScreenHeight,PadMoveLimit,1.f-PadMoveLimit); } final function RespawnBall() { BallVel.X = -1; BallVel.Y = 0.5-FRand(); BallVel = Normal2D(BallVel)*0.35; BallPos = Default.BallPos; BallHeading = BH_ToPlayer; EnemyPadVel = 0.f; } final function NewRound() { BallVel = vect(0,0,0); BallPos = Default.BallPos; BallHeading = BH_None; SetTimer(1,false,'RespawnBall',Self); } final function PlayHitSound(bool bPlayer) { if (bPlayer) { if (HitSoundsA.Length==0) return; if (HitSoundsA[HitSoundIndex[0]]!=None) PlaySound(HitSoundsA[HitSoundIndex[0]],true); if (++HitSoundIndex[0]==HitSoundsA.Length) HitSoundIndex[0] = 0; } else { if (HitSoundsB.Length==0) return; if (HitSoundsB[HitSoundIndex[1]]!=None) PlaySound(HitSoundsB[HitSoundIndex[1]],true); if (++HitSoundIndex[1]==HitSoundsB.Length) HitSoundIndex[1] = 0; } } final function PlayerScored(bool bPlayer) { ++NumPlays; if (bPlayer) { ++Score; if (MissSound[1]!=None) PlaySound(MissSound[1],true); } else { --Score; if (MissSound[0]!=None) PlaySound(MissSound[0],true); } HitSoundIndex[0] = 0; HitSoundIndex[1] = 0; UpdateScores(); SetAIRating(); BallHeading = BH_None; SetTimer(2.5,false,'NewRound',Self); } // Calculate where the ball is going to hit on in enemy side. final function CalcEndPosition() { local float T,DY; local vector P,V; if (BallVel.X<=0.f) // Never. return; V = BallVel; P = BallPos; // Get hit time. T = (EnemyPad.X - PadWidth - (BallWidth*0.5) - P.X) / V.X; // Now take bounces into account. while (true) { if (V.Y<0.f) // Bottom. { DY = (LevelBoarderSize + (BallHeight*0.5) - P.Y) / V.Y; // Calc intersection time. if (DY<T) { P+=(V*DY); V.Y = -V.Y; T-=DY; } else break; // No more wallhits. } else if (V.Y>0.f) // Top. { DY = (1.f - LevelBoarderSize - (BallHeight*0.5) - P.Y) / V.Y; if (DY<T) { P+=(V*DY); V.Y = -V.Y; T-=DY; } else break; // No more wallhits. } else break; // No wallhits! } BallTrajectory = P+(V*T); } function Tick(float Delta) { local vector V,HN,ExtA,ExtB; local float DY; local bool bTraj,bRand; // Check collision unless out of bounds already. V = BallVel*Delta; if (BallHeading!=BH_None) { // Check paddles switch (BallHeading) { case BH_ToPlayer: if (BallPos.X<0.f) PlayerScored(false); else if ((BallPos.X+V.X)<0.05) { ExtA.X = PadWidth*0.5; ExtA.Y = PadHeight*0.5; ExtB.X = BallWidth*0.5; ExtB.Y = BallHeight*0.5; if (Box8DirTrace(BallPos,V,PlayerPad+vect(0.5,0,0)*PadWidth,ExtB,ExtA,HN,DY)) { BallPos+=(V*DY); V = vect(0,0,0); if (HN.X<0.25) // Hit edge of the paddle { PlayerScored(false); BallVel = MirrorVectorByNormal(BallVel,HN); } else { AITrajOffset = 0.f; if (AITactic>3) AITrajOffset = FMin((AITactic-3)*0.35,0.97)*(0.5-FRand())*(PadHeight+BallWidth); // Randomly chose to throw ball in the corners to give angular momentum. BallHeading = BH_ToEnemy; BallVel.X *= -1.05; BallVel.Y = (BallPos.Y-PlayerPad.Y) / PadHeight * Abs(BallVel.X) * 4.5; CalcEndPosition(); PlayHitSound(true); } } } break; case BH_ToEnemy: if (BallPos.X>1.f) PlayerScored(true); else if ((BallPos.X+V.X)>0.95) { ExtA.X = PadWidth*0.5; ExtA.Y = PadHeight*0.5; ExtB.X = BallWidth*0.5; ExtB.Y = BallHeight*0.5; if (Box8DirTrace(BallPos,V,EnemyPad-vect(0.5,0,0)*PadWidth,ExtB,ExtA,HN,DY)) { BallPos+=(V*DY); V = vect(0,0,0); if (HN.X>-0.25) // Hit edge of the paddle { PlayerScored(true); BallVel = MirrorVectorByNormal(BallVel,HN); } else { BallHeading = BH_ToPlayer; BallVel.X = -BallVel.X; BallVel.Y = (BallPos.Y-EnemyPad.Y) / PadHeight * Abs(BallVel.X) * 4.5; PlayHitSound(false); } } } break; } // Check edges // Top. if (V.Y<0.f) { DY = LevelBoarderSize + (BallHeight*0.5) - BallPos.Y; if (DY>V.Y) { DY = DY / V.Y; // Calc intersection time. BallPos+=(V*DY); V = vect(0,0,0); BallVel.Y = -BallVel.Y; BallPos.Y = FMax(BallPos.Y,LevelBoarderSize+(BallHeight*0.5)); CalcEndPosition(); } } // Bottom if (V.Y>0.f) { DY = 1.f - LevelBoarderSize - (BallHeight*0.5) - BallPos.Y; if (DY<V.Y) { DY = DY / V.Y; BallPos+=(V*DY); V = vect(0,0,0); BallVel.Y = -BallVel.Y; BallPos.Y = FMin(BallPos.Y,1.f-LevelBoarderSize-(BallHeight*0.5)); CalcEndPosition(); } } bRand = true; if (AITactic>0.f) // Directly follow ball { bTraj = false; if (AITactic<1.f) { if (BallHeading==BH_ToEnemy) bRand = BallPos.X>AITactic; } else { bRand = false; if (AITactic>2.f && BallHeading==BH_ToEnemy) bTraj = (AITactic>=4.f || BallPos.X>(2.f - AITactic*0.5)); } if (!bRand) { if (bTraj) { if (BallPos.X>0.5) HN.Y = BallTrajectory.Y+AITrajOffset-EnemyPad.Y; else HN.Y = BallTrajectory.Y-EnemyPad.Y; HN.X = FMin(2.f + ((AITactic-1.f)*3.f),13.f); // Calc paddle changespeed rate DY = FMin(0.15 + (AITactic*0.02f),2.f); // Calc paddle max speed } else { HN.Y = BallPos.Y-EnemyPad.Y; HN.X = FMin(3.f + (AITactic*6.f),15.f); // Calc paddle changespeed rate DY = FMin(0.25 + (AITactic*0.025f),2.f); // Calc paddle max speed } EnemyPadVel *= (1.f-Delta*HN.X); // Deaccel all the time. EnemyPadVel = FClamp(EnemyPadVel+(HN.Y*Delta*HN.X*6.f),-DY,DY); } } // Update AI if (bRand) // Random motion. { if (AITacticTimer<WorldInfo.TimeSeconds) { bAIRandom = (Rand(2)==0); AITacticTimer = WorldInfo.TimeSeconds+FRand(); } DY = FMax(FMin(Delta,0.65f-Abs(EnemyPadVel)),0.f); if (bAIRandom) EnemyPadVel += DY; else EnemyPadVel -= DY; } // Apply by velocity and limit movement. EnemyPad.Y = EnemyPad.Y+(EnemyPadVel*Delta); if (EnemyPad.Y<PadMoveLimit) { EnemyPad.Y = PadMoveLimit; EnemyPadVel = FMax(EnemyPadVel,0.f); } else if (EnemyPad.Y>(1.f-PadMoveLimit)) { EnemyPad.Y = 1.f-PadMoveLimit; EnemyPadVel = FMin(EnemyPadVel,0.f); } } BallPos+=V; } final function SetAIRating() { AITactic = float(Score)*0.1+0.5; } defaultproperties { PlayerPad=(X=0.005,Y=0.5) EnemyPad=(X=0.995,Y=0.5) ScreenHeight=800 BallPos=(X=0.75,Y=0.5) BallTrajectory=(X=1,Y=0.5) BallHeading=BH_None }