Skip to content

Tennis

Singles and doubles on a hard court, in a sport center of ten courts. You play the CPU, or up to four people play on one screen, split in two or four, or online with people on other PCs (and their friends on the same screen as them). The ball is a FEMFX soft body, and so are the racket's strings: the ball flattens on them and on the court and wobbles back, and the strings give a pocket and ring. Every swing is made on the spot, with no animation clips: you hold a shot button to take the racket back (the longer, the harder, if your legs have it) and let go to swing; when you let go decides whether you meet the ball dead on, early or late. Fourteen strokes, from topspin drives and slices to volleys, smashes and three serves, each with its own path, speed and timing window, picked from where the ball is. Running flat out and hitting hard tire you: tired players run slower, hit softer and time worse. The score is real tennis: deuce and advantage, tiebreaks, lets, double faults, changing ends.

The demo teaches how to put a FEMFX object into a game that needs exact numbers (the ball's flight is the game's own maths; FEMFX gives it its body), a CPU player whose footwork is pure logic you can unit-test, rules as a small state machine the whole game listens to, procedural swings (two-bone IK and a turning, crouching body) with no animation clips, a FEMFX object pinned in a frame, the start menu with local players and CPU players, and online play where every machine flies the ball from the same hit. Start here for a sports game, any game with a ball, or any game where a soft body has to go exactly where the rules say.

The TV view of a CPU match: two players, the net, the fence, the next court

Or pick Sport center in the menu: walk about all ten courts, step up to a free court's gate to play (someone else at the gate is your opponent, or play the CPU), watch the CPU players' matches with a crowd of up to 100 people who sit, stand and cheer, and see who has won most on the board. Online, the host's sport center is everyone's: people from every screen walk about together, meet at a gate and play each other.

Run it

The executable is tennis (kke_add_game(tennis ...) in CMakeLists.txt). The root CMakeLists.txt adds it when KKE_ENABLE_JOLT, KKE_ENABLE_FEMFX and KKE_ENABLE_NET are all on: the everything presets. FEMFX has no ARM port yet, so there is no Android build.

cmake --build build --target tennis
cd build/bin
./tennis
KKE_SKIP_INTRO=1 ./tennis                            # skip the logo intro
KKE_TENNIS_BOTS=1 KKE_TENNIS_QUIT=120 ./tennis       # watch two CPU players, with a log
KKE_TENNIS_BALLTEST=1 ./tennis                       # the ball's test shots, logged
KKE_TENNIS_CENTER=1 KKE_TENNIS_BOTS=1 KKE_TENNIS_CROWD=100 ./tennis  # the whole sport center, CPU players only
KKE_TENNIS_BENCH=singles ./tennis                    # the busiest case: 100 people, 10 matches, a flying camera
KKE_TENNIS_BENCH=doubles KKE_BENCHMARK=40 ./tennis   # the same with doubles, measured, with a report
KKE_NET=host KKE_NET_NAME=Pip ./tennis                # host an online match...
KKE_NET=join:127.0.0.1 KKE_NET_NAME=Rook ./tennis     # ...and join it from a second terminal
Variable Effect
KKE_TENNIS_BOTS=1 CPU players only, no menu (a demo that plays itself)
KKE_TENNIS_DOUBLES=1 four players, two a side
KKE_TENNIS_LEVEL=0..3 the CPU players' level when there is no menu: Easy, Normal (default), Hard, Expert
KKE_TENNIS_GAMES=<n> games to win the set, over the menu's choice
KKE_TENNIS_LOBBY=0 skip the menu: you against one CPU player
KKE_TENNIS_QUIT=<s> quit after that many seconds of play, logging the score every 10 s and every call
KKE_TENNIS_SEED=<n> the CPU players' dice (default 1)
KKE_TENNIS_REPLAYS=0 no slow-motion replays of winners and aces (the menu's Replays row)
KKE_TENNIS_BENCH=singles / doubles the busiest case, a showcase and a benchmark: 100 people in the sport center, a CPU match on every court (20 players in singles, 40 in doubles), 6 or 5 people watching each match from the benches, the rest walking, and a camera flying from court to court. kke_benchmark runs both
KKE_NET=host / KKE_NET=join:ADDRESS host, or join a host, at startup (docs/NETWORKING.md); KKE_NET_NAME=<name> is player 1's name
KKE_TENNIS_CENTER=1 the sport center instead of one match (the menu's Play row)
KKE_TENNIS_CROWD=<n> how many CPU people walk about the sport center and watch (the menu's Crowd row: 0, 20, 40 or 100)
KKE_TENNIS_EMPTY=1 the sport center's free courts stay empty (no CPU matches)
KKE_TENNIS_WAIT=<n> hosting: start the match by itself once n people have joined (tests)
KKE_TENNIS_AUTOPLAY=1 the players at this screen are played by the CPU (tests: two copies that play each other online; in the sport center, walk to a free gate and play the CPU)
KKE_TENNIS_BALLTEST=1 no match: seven scripted shots (a drop, a groundstroke, into the net, a serve, both fences, the roof), every contact logged with how far the FEMFX body is from the flight
KKE_TENNIS_SWINGLOG=1 a log line for every hit: the stroke, the timing error and word, the spacing, the quality, the power, the stamina
KKE_TENNIS_STRINGTEST=1 one string bed on its own hit by a 30 m/s ball every 2 s, and every racket's pocket, logged in mm
KKE_TENNIS_CLOSEUP=<n> the camera side-on to player n (0 = the first), close: for looking at swings
KKE_TENNIS_CLOSEUP_DISTANCE=<m> how far that camera is (default 3.8): about 1.5 to look at the hands on the racket; negative puts it on the player's other side
KKE_TENNIS_POSE=<stroke>,<f\|b>,<t> player 1 held in one moment of a stroke, e.g. drive,b,-1 (a backhand drive's full takeback) or flat serve,f,0 (contact); names as strokeName in Swing.cpp
KKE_ANIMATIONS_DIR where to look for UAL1_Standard.fbx and UAL2.fbx
KKE_ASSETS_DIR also searched for Universal Animation Library 2/Unity/UAL2.fbx

Rebindings are saved to tennis_input.json and the menu's choices to tennis_lobby.json (the names passed in main.cpp).

Controls

All bindings are made in TennisModule::defineControls (TennisModule.cpp), the same for players 1 to 4, and are rebindable actions in the "Shots" and "Game" groups.

Action Keyboard / mouse Controller
Run (move, relative to the camera) WASD left stick
Look around (look.rate; the camera settles back behind you, facing the net, when you let go) none right stick
Topspin, the all-round shot; also the toss and the serve (tennis.topspin) Space / left mouse A (south)
Flat: hard and fast (tennis.flat) J X (west)
Slice: low and slow (tennis.slice) K / right mouse B (east)
Lob: over their head (tennis.lob) L Y (north)
Pause menu: settings, controls, quit Esc Start or Select
Back to the menu (tennis.menu; online, a client leaves the game; in the sport center, leave the court, the other side winning) M, or the pause menu the pause menu
Talk to the people near you (voice.talk, online) V LB
Developer panels (panels) F1 none
  • A shot. Press and hold a shot button as the ball comes: the racket goes back and the shot builds power (the power bar, bottom left; you move slower while you hold). Let go to swing. The forward swing takes 0.1 to 0.25 s depending on the stroke, so let go that long before the ball reaches your hitting spot, a little in front of you. The word bottom left says how it went: Perfect, Early, Late, Very early, Very late. Too early or too late by more than about 0.12 to 0.18 s and you miss it.
  • Which stroke is picked for you from where the ball is and the button: a high ball before the bounce is a smash, near the net a volley, just after the bounce and low a half volley, far to the side a stretch; otherwise Topspin is a drive, Flat a flat drive, Slice a slice, Lob a lob (the CPU players also play drop shots). Forehand or backhand is the side the ball is on.
  • Aim with the stick (or WASD) as you hit: left and right is across the court, forward is deeper, back is shorter.
  • Reading the ball. The camera sits close behind you, a little over your right shoulder, looking over the net. The ball has a soft shadow straight under it on the court (smaller and darker as it comes down), and a shot coming at you shows a yellow ring where it will land; the ring stays a moment after the bounce, shrinking (Marks.cpp).
  • The serve. Stand still and hold a shot button: the ball goes up and the racket goes back. Let go to swing; the best serve meets the ball near the top of the toss. Topspin and Lob serve a kick serve (high and safe, always the second serve's pick), Flat the fastest, Slice one that swings wide. Let the ball drop and you catch it and toss again.
  • Stamina (the green bar, bottom left) goes down when you run faster than a jog and with every swing (more the harder), and comes back slowly in a rally and quickly between points. Tired, you run slower, can't build full power, your timing window shrinks and your aim wanders.
  • Swing timing in the menu: Relaxed (a wider window), Normal, Pro (a narrower one), or Automatic (the swing lets go by itself at the right moment; you pick the shot, the power and the aim).
  • The HUD's hints show the right glyph for the device each player last touched (InputModule::promptText).
  • The engine's character actions this game doesn't use (jump, sprint, fire, look and so on) have their bindings cleared; voice.talk moves to V and LB (B is the slice).
  • Proximity voice chat online (kke::VoiceModule + kke::VoiceHudModule, docs/NETWORKING.md "Voice"): people within 30 m are heard from where they stand (the next court faintly), a ring over a talker's head or an arrow at the screen's edge shows where a voice comes from, and the menu's "Voices nearby" has a mute for each slot of the nearby talkers list.

How it plays

  • The menu. Controllers press A to join, up to four players, each with a name, a colour for their top, a skin tone, and trousers and shoes colours (docs/OUTFITS.md), and with the POLYGON City Characters pack a Body (one of eleven City people); online, everyone sees the clothes each player picked. Player 1 picks one match or the sport center (with its crowd and whether CPU players take the free courts), the CPU players (0 to 3, Easy to Expert), the match length (a short set to 4 games, a set to 6, best of three short sets, or quick: to 2), the teams (across the net, or all the people at this screen on one side against the CPU) and the helping hand, and the swing timing. Two players make singles; three or four make doubles (filled up with CPU players).
  • The sport center (the menu's Play row). Everyone at this screen walks the promenade between the courts (split screen for two or more, the camera behind each). A court's gate is at its end by the promenade: stand there and press Topspin to play on it. A second person at the gate is your opponent (a few seconds' countdown lets a third and fourth join for doubles); Lob plays the CPU now, Slice leaves. CPU players take any court nobody wants and hand it over at the end of a point when someone waits (the two middle courts stay free while anyone here plays). The crowd (the Crowd row) strolls, picks a court with a match on, sits on a bench or stands beside it, and cheers or groans at every point. After a match its players walk off at the gate, and every match won goes on the board top right, "Matches won here".
  • Scoring. Real tennis (Rules.cpp): 15, 30, 40, game; deuce and advantage (or no-ad); a tiebreak to 7, two clear, at 6 games all (or 4 all, or 2 all); the serve changes every game and every two points of a tiebreak; ends change after every odd game and every 6 tiebreak points; in doubles the partners take turns serving.
  • The umpire. A serve must land in the box diagonally across; out is a fault, two are a double fault; one that clips the net and lands in is a let. The return waits for the serve's bounce. In a rally the ball may be taken out of the air or after one bounce, never after two, and nobody hits it twice. Out, the fence or the roof before it bounces: the hitter loses the point. A ball that stops dead counts as bounced twice where it lies.
  • The helping hand (on by default): when you let go of the stick while the ball is coming to you, you walk to meet it, your reach is 0.45 m longer and a hit is never worse than fairly good. Off, it's all you.
  • Replays (the menu's Replays row, on by default). A winner or an ace is shown again at a quarter speed from a camera flying beside the ball: the hit, the flight and the landing, the ball and the strings squashing again. Any shot button skips it. Only in one match played at this screen: slowing the game down would slow every court with it.
  • The CPU players read your shot after a reaction time, run to where the ball can be met, pick a shot (lobs when you're at the net, drop shots when brave, slices, flats, topspin) and aim away from you. Their levels differ in speed (3.6 to 6.2 m/s), reaction time (0.45 to 0.1 s), how straight they aim, how hard they hit and how close to the lines they go (Bot.cpp BotSkill::forLevel).

How it works

Startup and the frame

main.cpp sets the mood clear_day and adds the modules in this order: Settings, Input, RigidBody (Jolt), Physics (FEMFX, metre scale, no demo objects, its own ground drawing off), Model, Ui, Audio, Lobby, Net, DemoPanel, Tennis, Stats. The order matters once: fixed updates run in add order, so FEMFX has stepped the ball before TennisModule::fixedUpdate reads it.

Each fixed step (TennisModule::fixedUpdate, stepMatch in Play.cpp): the ball moves and reports what it touched, the umpire hears it, every player thinks (CPU) or reads the controls (people), runs and maybe hits. Each frame (update): the menu, the controls, the bodies' animation and IK, the cameras, the HUD.

The ball: our flight, FEMFX's body

Ball.cpp. The first version let FEMFX fly the ball, and the ball test measured why that can't work for tennis: FEMFX's implicit solver is made for soft, heavy things, and on a 57 g ball it lost most of gravity (a 2 m drop reached the court at 1.2 m/s instead of 6.2) and nearly all of the bounce, and a 30 m/s shot passed through a thin wall between two steps. So the ball is two parts:

  • The flight is worked out in Ball::step each fixed step, in small steps (2 cm at most, so a 60 m/s serve can't jump the net tape): gravity plus the spin's pull, air drag, the court's bounce, the net (the tape lets it roll over slowly; the mesh stops it and drops it), the fence and the invisible roof (they take most of its speed). Every rule, the CPU and, later, the network read this one.
  • The body is a FEMFX tet-mesh sphere (buildSphere(4, 0.045), rubber of 400 kg/m³ and a stiffness of 1e4) that steerBody pushes along the flight each step: every vertex gets the same push, so only the whole ball moves and the squash and the wobble stay FEMFX's. A hit (Ball::strike) gives every vertex the new velocity plus a turn about the centre (spin) and a squeeze along the hit that grows with the distance from the centre, so the strings flatten it and FEMFX springs it back. The body stays within about 5 cm of the flight (10 cm at a hard hit), and FEMFX's own ground plane and the court's boxes squash it where it lands.

The engine calls it uses are new and general (PhysicsModule.h "Steering a whole object"): objectMotion, changeVertexVelocities, translateObject, resetObject.

Every court also gives FEMFX boxes for its fence walls (2 m thick), its roof at 14 m and its net (Ball::courtBoxes, one setExternalBoxes call for all ten courts): the flight never leaves the court, and the body can't either.

The maths everyone agrees on

Shot.cpp is pure maths, tested in tests/test_tennis.cpp:

  • Flight: position and velocity at any time, in closed form, with gravity, a spin's extra pull and linear air drag (0.4 per second: a 25 m/s drive loses about a third by the far baseline). When it comes down to a height, when it crosses the net.
  • bounce: 75% of the vertical speed back (the ITF's 2.54 m drop comes back to 1.35-1.47 m), 62% of the speed along the court (a medium-paced hard court); 40% of the spin's pull is left after.
  • planShot: the launch velocity that lands a ball exactly on a target at about a speed, raising the arc until it clears the net by a margin.
  • meetPoint, meetOnArc: where a player takes a ball after its bounce, waist high on the way down, or earlier when it would carry them too deep.

Swings and hitting

Swing.h is pure maths (tested): the fourteen strokes, each a StrokeShape (the racket head's path through ready, takeback, slot, contact and finish in the body's frame, the face's angle, how far the shoulders turn and the knees bend, how long the takeback, forward swing and follow-through take, the timing window, how fast and how steady), pickStroke (which stroke fits the ball), racketAt (where the racket and body are at a moment of the swing: a curve through the slot, mirrored for a backhand) and Stamina.

A swing runs on one clock, t: -2 to -1 the takeback (held as long as the button is), -1 to 0 the forward swing (from the release, taking forward seconds), 0 contact, 0 to 1 the follow-through. stepPlayer and stepSwing in Play.cpp: on release the contact time is known (contactAt); when the ball crosses the hitting plane (a distance in front of the body, or a height for an overhead) within reach, that is crossAt. The timing error is contactAt - crossAt; within maxError it's a hit (hitBall), otherwise a miss. Quality is spacing (the ball an arm and a racket to the side, between knee and shoulder) times timing (timingQuality: 1 dead on, ~0.8 at the window's edge). The target is the aim plus a wobble that grows as quality drops and with tiredness; early pulls it across the body, late pushes it wide. The stroke scales the speed and the wobble; planShot gives the velocity; the harder the hit, the more the strings squash the ball. CPU players (and Automatic timing) let go themselves, when the ball is forward seconds from the plane, plus a timing error drawn from their level (BotSkill::timing: 55 ms Easy to 18 ms Expert).

The strings

Strings.h, Strings.cpp. Each racket's string bed is a FEMFX slab (6 x 7 cells, 4 cm thick; thinner ones jitter) whose rim is pinned in the frame (TetSpawnOptions::pinnedVerts) and stretched 3% into it (moveVertices), so it has tension like strings. The slab stays still 150 m above the sport center, never drawn; the 16 x 19 strings are ribbons drawn on the racket from points embedded in it (kke::embedPoints, deformEmbedded), moved along the face's normal as the slab bends. A hit pushes the slab's vertices around the contact point in proportion to the ball's speed; FEMFX bends it into a pocket (about 17 mm for 30 m/s, measured with KKE_TENNIS_STRINGTEST) and lets it ring out over ~0.4 s, and then it is put to sleep (sleepObject; FEMFX never sleeps a lone object by itself). A real pocket lasts about 5 ms, less than a frame: these strings are softer, so it can be seen.

The CPU player

Bot.cpp has no engine in it: it is told where the ball is going (a Flight), where it stands, where the opponent stands and what the rules allow, and answers where to run, how urgently, and whether to arm a shot, which, aimed where. The test drives a Hard bot at its own speed toward what it says against 60 random shots and checks it reaches at least 90%.

Bodies and swings

Body.cpp. The players are the UAL mannequin with its idle, walk, jog and sprint clips blended by speed, UAL2's side steps when that pack is there, and sitting, cheering and groaning. There are no swing clips: racketAt gives the racket head's place, and the body follows it. The pelvis and spine turn (30% / 70%) with the shoulders, the hips drop and two-bone IK keeps the feet planted. The racket is an equipped item (docs/EQUIPMENT.md): kke::Equipment puts it in the right hand's palm socket by its "main" grip (the handle along the palm, the head past the thumb and index finger, the strings facing as the palm), and the fingers close round the handle until they touch it. The right arm reaches for where that puts the hand with the body-aware kke::solveHumanArm, which keeps to what a person's shoulder, elbow, forearm and wrist can do and never lets the arm or the racket go through the body (a follow-through wraps round the shoulder instead of through the head); the racket is then drawn where the hand got to. The left hand takes the "support" grip for a two-handed backhand and the "throat" at the ready, throws the toss or points up at a smash. The racket face opens for slice and lob and closes for topspin. The racket is POLYGON Shops' tennis racket when that pack is there (its flat string sheet taken out: the FEMFX strings go there), or a mesh built in code.

The sport center

Court.cpp, Scene.cpp. Ten courts to ITF size in two rows of five, one scene, with a green surround, lines, net and posts, fences with windscreens (see-through: drawTranslucent), an umpire's chair, floodlights, benches along the promenade between the rows, and spectators' seats beside every court and a standing row behind the promenade end. The fences and floodlight poles are Jolt walls, so people can't walk onto another court or through a pole.

Center.cpp runs it. Everyone out of a match is a Walker (a Jolt character and a Body): a person at this screen, moved by the stick relative to their camera, or one of the crowd. Each court has a Gate (who waits, the countdown); startCourt turns the people waiting into a match (hiding their walkers and removing their capsules), endCenterMatch counts the win and puts them back at the gate. Matches come and go, so player slots are reused (Player::alive).

The crowd is three states: stroll to a spot on the promenade, go to a free seat by a court with a match on (seats beside a court are in the gap between two fences, so they walk to the gap's mouth first and back out the same way), and watch for 25 to 75 s, facing the court. A point on the court they watch sets them cheering (three in four) or groaning. Each seat is held by one walker (m_seatTaken), so nobody stands in anybody. The walkers' camera pulls in in front of a pole or a fence (a Jolt raycast from the head), so it never sits inside one.

Cost, 100 in the crowd and ten matches: each person is a skinned mannequin (skinned on the CPU by ModelModule) and a Jolt character; the ten balls are ten FEMFX bodies. On the software renderer the CI uses it runs at 3 to 4 frames a second, all of it waiting on the renderer; a real GPU is the test that counts.

Online

Player 1 sets Online in the menu to Host or Join (the games found on the network and on this PC are listed; pick one and press Join). Everyone at each screen plays: a second person at a screen is a NetModule local player, like the menu's seats. Net.cpp has the game's side, NetTennis.h the messages (event kinds from 0x5400).

  • Who plays. The host's Start builds the match: its own seats, then everyone online, then the menu's CPU players, then more CPU players if a side is short. More than four people: the first four play. Its Setup says who plays where; each client builds the same match, its own seats played from its own controllers.
  • The CPU players are the host's. It sends where they all are, every match's in one Cpus event ten times a second.
  • Each machine runs its own players and sends where they are, where they face and how far into which swing (toState, a NetPlayerState with the swing's contact packed into its extra bytes). Everyone else's players are kinematic capsules put where their machine says.
  • The host is the umpire. Its Serve starts each point (with a number, so a hit that arrives after the call is dropped) and its Point ends it with the result and the call; every machine applies that result to the same score. A client never calls a point from its own ball.
  • A hit is all the ball needs. The hitter's machine plans the shot and sends Hit (where, the velocity, spin, pull); everyone, the hitter too, flies the ball from the numbers as they come off the wire, so the flights match to the bit (a test checks the bytes). The host checks a client's hit could happen (their turn, this point, this shot) before it passes it on. A serve's Toss goes the same way.
  • Drift is put right. Five times a second the host sends its ball; a client more than 25 cm or 1 m/s off takes it, unless a hit of its own is on the way to the host.
  • Someone leaves: the host ends the match for everyone (End) and all go back to the menu; a client's menu button leaves the game.
  • The sport center online. When the host picks Sport center, every client walks in too (Board says it's on). The host runs every court: each match has its own number, so every event names the match it belongs to, and a Hit or a Toss names the player by their place in it. Each machine sends where its people walk; the others see them walk. At a gate, a client asks the host (Gate: join, leave, play the CPU now) and the host keeps the queues; its Board, twice a second, has how many wait at each gate, the countdowns and the wins. Someone who joins later gets the Board and a Setup for every match on, with the points so far, so they see the right score. The crowd is each screen's own: it only watches. Someone leaving mid-match forfeits it (the other side wins).

Tested with two copies on one PC, both played by the CPU (KKE_TENNIS_AUTOPLAY=1, the commands in "Run it" plus KKE_TENNIS_WAIT=1 KKE_TENNIS_LOBBY=0 KKE_TENNIS_QUIT=70): both logs make the same calls after the same number of shots, rallies of 6 to 15 shots. The sport center the same way, the host with KKE_TENNIS_CENTER=1 (no KKE_TENNIS_WAIT): each copy's person walks to a different free court's gate and plays the CPU, and both logs show every court's matches.

Replays

Replay.cpp keeps the last five seconds of a match, one frame per fixed step: the ball's flight state and every player's feet, facing and swing. When the point's last hitter wins it, the replay starts 0.7 s before that hit and ends 0.6 s after the ball lands. The ball flies again from its saved state (its flight is the game's own maths, so it flies exactly the same way) and is struck again at the recorded step, which squashes the FEMFX ball and pushes the FEMFX strings again; the players are drawn from the frames. The engine's Application::setTimeScale(0.25) makes it slow: as many fixed steps a second, each a quarter as long, so FEMFX stays smooth. The ball camera starts beside the hitter, a little in front, on the court's middle side, and watches the ball come in and the swing meet it; after the hit it swoops off after the ball, beside and behind it, and stays on the landing spot once the ball has bounced.

The busiest case (KKE_TENNIS_BENCH)

Bench.cpp fills the sport center to 100 people: a CPU match on every court (always singles, or always doubles), 6 (or 5) spectators put straight onto each court's benches, spread along both sides, and the rest strolling the promenade. Matches are sets to 6 so the courts stay busy, and a court that finishes starts the next match at once with the same spectators. The camera flies along one row of courts and back along the other, 4.5 s over each court's promenade end, gliding across it while it watches the ball, both players and the benches. The seed is fixed, so every run plays the same points. It loads FEMFX with 10 balls and 20 or 40 string beds, Jolt with 100 characters, and 100 procedural bodies.

Cameras and the HUD

With people playing, each gets a camera behind their own baseline, high enough to see over the net, that swings round when ends change; two people split the screen, three or four split it in four. With only CPU players (and in the menu) it's the TV view: high behind one end, both baselines in sight. The HUD is RmlUi (ui/tennis_hud.rml, Hud.cpp): the scoreboard with who serves, the umpire's call, "Second serve" and "Tiebreak", and the controls for whoever is at this screen.

Design decisions

  • The flight is ours, the body is FEMFX's. Measured above: FEMFX on its own can't give a tennis ball's numbers, and the rules, the CPU and a network all need one flight they agree on to the millimetre.
  • Linear drag. Real drag grows with the square of the speed; linear drag is close over a rally's speeds and keeps every sum closed-form, so planShot lands exactly where it aims.
  • Press early, swing on arrival. Timing a button to the exact frame is not fun on a TV; arming the shot and grading how early you pressed keeps timing a skill without being a lottery.
  • Procedural swings. Kees has no tennis animations (they are expensive) and wants the fun to be in procedural ones: every stroke is a path, a body turn and IK, so it bends to wherever the ball really is.
  • Hold to take back, let go to swing. The player measures the swing (how long to hold: power, costing stamina) and times it (when to let go); the stroke itself is picked from the ball so a pad has enough buttons. Automatic timing keeps it playable for anyone who wants only the tactics.
  • Hits, not a streamed ball, online. Sending the hit and letting every machine fly the same maths costs one small message per shot and looks smooth at any latency; the host's ball a few times a second only mops up. Streaming the ball's position would lag it and jitter the squash.
  • Strings far away. Carrying a FEMFX slab on a racket swinging at 40 m/s would cost solver time and shake it; only the pocket matters, so the slab stays still and its shape is copied onto the racket.
  • No Synty courts yet. The Shopping Mall pack Kees mentioned is not on the share; the courts are built from boxes. The racket is Synty's.

Tuning

What Where Value
Bounce: vertical kept, along kept BounceModel in Shot.h 0.75, 0.62
Air drag kDrag in Shot.h 0.4 /s
Spin pull (topspin, slice, lob, drop, serve) spinPull in Shot.cpp 6, -2.5, 1, -1.5, 2 m/s²
Shot speeds speedFor in Play.cpp flat 25-35, topspin 20-28, slice 15-20, lob 9-10.5, serve 30-46 m/s
Run speed, while charging kRunSpeed, kChargeSpeed in Play.cpp 5.8, 3.6 m/s
Reach, helping hand's extra kReachMin, kReachMax, kReachHelp 0.2, 1.5, 0.45 m
Stroke paths, times, windows, speed, steadiness shapes() in Swing.cpp forward swing 0.1-0.25 s, window 25-50 ms, miss past 100-180 ms
Stamina costs and effects Stamina in Swing.cpp swing 0.015 + 0.05 x power², recovery 0.02 /s in a rally, 0.18 /s between points
Swing timing choice setTiming Relaxed 1.5x, Normal 1x, Pro 0.7x the window
CPU timing error BotSkill::timing in Bot.cpp 55, 38, 27, 18 ms
The strings stringMaterial, kStretch, kPush in Strings.cpp 1e4 stiffness, 3% stretch, 0.1 x the ball's speed
CPU levels BotSkill::forLevel in Bot.cpp speed, reaction, aim, power, risk
A CPU's aim wandering in a long rally tired in hitBall (Play.cpp) +10 cm a shot after the 3rd
Sport center: walking, the crowd's stroll kWalkSpeed, kCrowdSpeed in Center.cpp 4.2, 1.5 m/s
Sport center: gate reach, countdown, a court's rest before CPU players take it kGateReach, kGateCountdown, kCourtRest 3 m, 4 s, 6 s
The ball's body ballMaterial, buildSphere(4, ...) in Ball.cpp 400 kg/m³, 1e4 stiffness
How hard the body is steered steerBody in Ball.cpp 25 /s blend, 20 /s pull onto the path

Engine features it uses

  • FEMFX soft bodies and the steering calls (PhysicsModule, see the physics demo)
  • Jolt characters and walls (RigidBodyModule)
  • The start menu (LobbyModule, docs/LOBBY.md)
  • Input with four players, rebinding and button glyphs (docs/INPUT.md)
  • Skinned models, the animator and two-bone IK (ModelModule, docs/PROCEDURAL_ANIMATION.md)
  • Split screen (kke::splitScreen)
  • RmlUi for the HUD, the demo panel (DemoPanelModule, docs/DEMO_PANEL.md)
  • Impact sounds (AudioModule::playImpact)
  • Moods (docs/MOODS.md)
  • Online play with local players (NetModule: sendEvent, relayEvent, addLocalPlayer, setLocalPlayer, LAN search; docs/NETWORKING.md)

Assets

  • The mannequin and its clips: assets/animations/UAL1_Standard.fbx (Quaternius' Universal Animation Library, CC0), in the repository.
  • Side steps: UAL2.fbx (Universal Animation Library 2, CC0) from assets/animations/ or KKE_ASSETS_DIR. Without it the log says so once at info level and players turn to run sideways.
  • The ball's felt and seam: textures/tennis_ball.png, made by textures/make_ball_texture.py.
  • The racket: SM_Prop_Sport_Tennis_Racket_01.fbx and its atlas PolygonShops_Texture_01_A.png from Synty's POLYGON Shops pack, found anywhere under assets/synty or KKE_ASSETS_DIR (Synty packs are never in the repository). Without it the racket is built in code.
  • Everything else (the courts) is built in code.

Make a game like this

  1. Build with FEMFX (cmake --workflow --preset everything) and copy the folder: cp -r games/tennis games/my_sport; rename the target, the namespace and the input and menu files.
  2. Keep the rules pure. A Rules.cpp with no engine in it that hears "hit", "bounce", "net", "out" and answers who won the point is easy to test and easy to change (badminton, squash, volleyball).
  3. Own the flight, give FEMFX the body. For any ball that must go where the rules say, fly it with your own maths (Flight, or your own) and steer the FEMFX body along with changeVertexVelocities; kick it with strike-like velocity fields for the squash.
  4. Write the CPU as pure logic that gets a view and returns a decision, then test its footwork without a window.
  5. Online, send what happened, not where things are. A hit, a toss, the umpire's call: small events every machine applies the same way (Net.cpp); only people's poses stream.
  6. Read next: the physics demo, Climb Race for more online play with local guests, docs/LOBBY.md.

Files

File What's in it
main.cpp The modules and the mood
TennisModule.h, TennisModule.cpp Players and matches, startup, controls, the frame, the ball test
Play.cpp A match's step: points, the umpire, people's controls, CPU players, serving, hitting
Scene.cpp The sport center's meshes and walls, bodies, cameras
Hud.cpp, ui/tennis_hud.rml The scoreboard, the calls, the hints
Lobby.cpp The start menu's fields and options
Center.cpp The sport center: walkers, gates, the crowd, CPU matches on free courts, the win board
Replay.cpp Slow-motion replays of winners and aces, and the ball camera
Bench.cpp The busiest case: ten matches, 100 people, the flying camera
Net.cpp Online: Host / Join rows, who plays, events in and out, other machines' players
NetTennis.h, NetTennis.cpp The online messages and how they are packed (tested)
Rules.h, Rules.cpp The score and the umpire (pure)
Shot.h, Shot.cpp Flights, bounces, planning a shot, where to meet a ball (pure)
Court.h, Court.cpp Court sizes, boxes, positions, the ten-court layout, seats
Ball.h, Ball.cpp The ball: its flight and its FEMFX body
Bot.h, Bot.cpp The CPU player (pure)
Swing.h, Swing.cpp The strokes, their paths and timing, stamina (pure, tested)
Strings.h, Strings.cpp The FEMFX string beds and the strings' ribbons
Body.h, Body.cpp The mannequin, its clips, the procedural swings, the racket
textures/ The ball's texture and the script that makes it