This commit is contained in:
Miloslav Ciz 2025-01-28 22:27:19 +01:00
parent e695921c5c
commit 6986028e33
4 changed files with 171 additions and 155 deletions

22
audio.h
View file

@ -1,5 +1,10 @@
/**
audio: this file implements the audio system.
/*
Licar audio
This file implements the audio system. The module only computes audio samples
(playing them must be done by the frontend). The audio format is 8bit, 8 KHz
mono. This module does NOT deal with music (music is stored in a separate
file, left to be optionally loaded and played by the frontend).
*/
#ifndef _LCR_AUDIO
@ -26,6 +31,9 @@ struct
int engineInc;
} LCR_audio;
/**
Initializes the audio system, only call once.
*/
void LCR_audioInit(void)
{
LCR_LOG0("initializing audio");
@ -40,11 +48,18 @@ void LCR_audioInit(void)
LCR_audio.engineIntensity = 0;
}
/**
Sets the intensity of car engine sound that's constantly played (unless when
intensitiy has been set to 0). Maximum value is 255.
*/
void LCR_audioSetEngineIntensity(uint8_t value)
{
LCR_audio.engineIntensity = value;
}
/**
Tells the audio system to play certain sound (see the sound constants).
*/
void LCR_audioPlaySound(uint8_t sound)
{
LCR_LOG2("playing sound");
@ -58,6 +73,9 @@ uint8_t _LCR_audioNoise(void)
return LCR_audio.noise >> 16;
}
/**
Gets the next audio sample.
*/
uint8_t LCR_audioGetNextSample(void)
{
unsigned char result = 128;

96
game.h
View file

@ -8,6 +8,9 @@
graphics, sound etc., and is meant to be included and used by specific
frontends (which will handle each platform's hardware details and I/O). See
the frontend info below for help with porting the game to a new platform.
This module (with the help of other modules) will perform all the computations
(physics, graphics, audio, ...) and only use the frontend's quite primitive
functions to actually present the results to the user.
The code uses LCR_ (or _LCR) prefix as a kind of namespace preventing
collision with 3rd party identifiers.
@ -59,19 +62,21 @@
/*
FOR FRONTENDS (porting to other platforms):
- Implement the below described functions according to their description.
- Implement the frontend functions given below according to their description.
- Implement the main program and game loop.
- Call the below described functions as described.
- If you want to support music, make your frontend play music from the "music"
file in assets. It is in raw format, storing 8bit unsigned samples at 8000
Hz. Use the LCR_gameMusicOn to check what the music volume is. If you
don't support music, set LCR_SETTING_MUSIC to 0 in your frontend code so
that the game knows music is disabled.
- Call some of the frontend funtions given below in your main program in
places as described in the description of the function.
- If you want to support music, make your code load and play the "music"
file in the asset directory. It is in raw format, storing 8bit unsigned
samples at 8 KHz mono. Use the LCR_gameMusicOn function to check whether the
music is currently enabled (if not, stop playing it). If you don't want to
support music, set LCR_SETTING_MUSIC to 0 in your frontend code (before
including this module) so that the game knows music is disabled.
*/
/**
Implement this in your frontend. Returns 1 if given key is pressed or 0
otherwise.
Implement this in your frontend. Returns 1 if given key (see LCR_KEY_*
constants) is pressed or 0 otherwise.
*/
uint8_t LCR_keyPressed(uint8_t key);
@ -87,35 +92,40 @@ void LCR_sleep(uint16_t timeMs);
to the screen back buffer (i.e. NOT directly to screen, back buffer shall
only be copied to front buffer once the LCR_gameStep function finishes all
rendering). This function should NOT check for out-of-screen coordinates, this
is handled by the game internals and out-of-screen pixels will never be drawn.
The color value depends on game settings but is normally an RGB565 value.
is handled by the game internals and out-of-screen pixels will never be drawn
(your code unnecessarily checking it again would likely slow down rendering a
lot, as drawing pixels is a bottleneck). The color value depends on game
settings but is normally an RGB565 value. The index parameter says the
coordinate at which to write the pixel, assuming the pixels are stored by
rows, from top to bottom.
*/
void LCR_drawPixel(unsigned long index, uint16_t color);
/**
Implement this in your frontend. This function will be called to log what the
program is doing. If you want to ignore logging, simply make the function do
nothing.
program is doing. Typically this function should print the string to console.
If you want to ignore logging, simply make the function do nothing.
*/
void LCR_log(const char *str);
/**
Implement this in your frontend. This function serves for loading optional
data file that allows to add more maps, replays etc. If your frontend
won't support this, just make the function return 0. Otherwise it must return
characters from the data file one by one; after reaching the end of file
0 must be returned and the reading position will be reset to start again.
data file that allows adding more maps, replays etc. If your frontend won't
support this, just make the function return 0. Otherwise it must return
characters from the data file one by one; after reaching the end of file 0
must be returned and the reading position will be reset to start from
beginning again.
*/
char LCR_getNextDataFileChar(void);
/**
Implement this in your frontend. This serves to store data in the optional
data file, e.g. replays. If your frontend doesn't support this (e.g.
because the file is read only), the function may ignore the append, but if
the file is otherwise supported, a rewind of the read position must still be
done. If appending is supported, the function must append the provided string
to the data file AND then reset the data file reading position back to
the start.
data file (e.g. replays and info about beaten maps). If your frontend doesn't
support this (e.g. because the file is read only), the function may ignore the
append, but if the file is otherwise supported, a rewind of the read position
must still be performed. If appending is supported, the function must append
the provided string to the data file AND then reset the data file reading
position back to the start.
*/
void LCR_appendDataStr(const char *str);
@ -134,18 +144,22 @@ void LCR_gameEnd(void);
Call this function in your frontend repeatedly inside the main loop, pass the
current time as the number of milliseconds since program start. This function
will perform the game step AND other things such as checking the input states,
rendering or sleeping (all using the above functions you should implement).
Returns 0 if program should end, otherwise 1.
rendering or sleeping (all using the above described functions you should
implement). Returns 0 if program should end, otherwise 1.
*/
uint8_t LCR_gameStep(uint32_t timeMs);
/**
Gets the current music volume;
Returns 1 if music is currently on, else 0. If your frontend supports music,
use this function at each frame to know whether you should start/stop playing
music. Otherwise ignore this.
*/
uint8_t LCR_gameMusicOn(void);
/**
Gets next audio sample (unsigned 8bit samples, 8 KHz).
Gets the next audio sample (unsigned 8bit samples, 8 KHz mono). If your
frontend supports sound, call this continuously and play the stream of
samples, otherwise ignore this.
*/
uint8_t LCR_gameGetNextAudioSample(void);
@ -187,7 +201,7 @@ uint8_t LCR_gameGetNextAudioSample(void);
// forward decls of pixel drawing functions for the renderer
/**
Internal pixel drawing function that draws pixel at specified screen coords
Internal pixel drawing function that puts a pixel at specified screen coords
without checking for safety (it's faster but can only be done if we know for
sure we're not drawing outside the screen).
*/
@ -213,7 +227,7 @@ static inline void LCR_drawPixelXYSafe(unsigned int x, unsigned int y,
#if LCR_SETTING_GHOST_MAX_SAMPLES == 0
#undef LCR_MENU_TABS
#define LCR_MENU_TABS 3
#define LCR_MENU_TABS 3 // no ghosts => remove the tab for ghosts
#endif
#define LCR_MENU_STRING_SIZE 16
@ -241,18 +255,17 @@ struct
{
uint8_t state;
uint32_t stateStartTime;
uint32_t time;
uint32_t frame;
uint32_t nextRenderFrameTime;
uint32_t nextRacingTickTime;
uint32_t time; ///< Current frame's time.
uint32_t frame; ///< Current frame number.
uint32_t nextRenderFrameTime; ///< At which frame to render next frame.
uint32_t nextRacingTickTime; ///< When to simulate next physics tick.
uint8_t cameraMode;
uint8_t debugDraw;
uint8_t musicOn;
uint8_t keyStates[LCR_KEYS_TOTAL]; /**< Assures unchanging key states
during a single frame, hold number of
frames for which the key has been
continuously held. */
uint32_t runTimeMS; /**< Current time of the run */
uint32_t runTimeMS; ///< Current time of the run
struct
{
@ -260,14 +273,14 @@ struct
uint8_t selectedItem;
uint8_t itemCount;
char itemNames[LCR_MENU_MAX_ITEMS][LCR_MENU_STRING_SIZE];
const char *itemNamePtrs[LCR_MENU_MAX_ITEMS]; ///< helper array
const char *itemNamePtrs[LCR_MENU_MAX_ITEMS]; ///< Helper array.
} menu;
struct
{
int state; ///< -1 if reading external res. f., else pos.
int state; ///< -1 if reading external data f., else pos.
// indices and counts are among the data of the same type
// Indices and counts are among the data of the same type.
unsigned int firstItemIndex;
unsigned int itemsTotal;
} dataFile;
@ -286,7 +299,6 @@ struct
is to allow ghosts for even long replays. */
} ghost;
#endif
} LCR_game;
uint8_t LCR_gameMusicOn(void)
@ -448,7 +460,7 @@ void _LCR_gamePrepareGhost(void)
LCR_game.ghost.stretch = 0;
while (((int) LCR_replay.achievedTime) >
while (((int) LCR_racing.replay.achievedTime) >
(LCR_SETTING_GHOST_STEP << LCR_game.ghost.stretch) *
LCR_SETTING_GHOST_MAX_SAMPLES)
{
@ -780,7 +792,7 @@ void LCR_gameInit(int argc, const char **argv)
LCR_game.menu.selectedItem = 0;
LCR_game.frame = 0;
LCR_game.musicOn = 1;
LCR_game.musicOn = LCR_SETTING_MUSIC;
LCR_game.nextRenderFrameTime = 0;
LCR_game.nextRacingTickTime = 0;
LCR_game.cameraMode = LCR_CAMERA_MODE_DRIVE;
@ -1176,9 +1188,11 @@ void LCR_gameHandleInput(void)
LCR_rendererCameraReset();
break;
#if LCR_SETTING_MUSIC
case 1:
LCR_game.musicOn = !LCR_game.musicOn;
break;
#endif
case 2:
LCR_audio.on = !LCR_audio.on;

16
map.h
View file

@ -94,7 +94,7 @@
#define LCR_BLOCK_RAMP_34 '/' ///< plain ramp, 3/4 size
#define LCR_BLOCK_RAMP_12 '<' ///< plain ramp, 1/2 size
#define LCR_BLOCK_RAMP_14 '_' ///< plain ramp, 1/4 size
#define LCR_BLOCK_RAMP_CORNER 'v' ///< corner of ramp
#define LCR_BLOCK_RAMP_CORNER 'v' ///< corner of a ramp
#define LCR_BLOCK_RAMP_CURVED_PLAT ']' ///< curved ramp with top platgform
#define LCR_BLOCK_RAMP_CURVED ')' ///< curv. ramp without top platf.
#define LCR_BLOCK_RAMP_CURVED_WALL '}' ///< curved ramp plus small wall
@ -103,6 +103,8 @@
#define LCR_BLOCK_CORNER_12 '\\' ///< diagonal corner (1/2 wide)
#define LCR_BLOCK_HILL '(' ///< curved "hill"
#define LCR_BLOCK_BUMP '~' ///< small bump on the road
#define LCR_BLOCK_CORNER_CONVEX 'n' ///< curved corner (convex)
#define LCR_BLOCK_CORNER_CONCAVE 'l' ///< curved corner (concave)
#define LCR_BLOCK_FULL_ACCEL '>'
#define LCR_BLOCK_BOTTOM_ACCEL 'z'
@ -111,9 +113,6 @@
#define LCR_BLOCK_FULL_FAN 'o'
#define LCR_BLOCK_RAMP_FAN 'V'
#define LCR_BLOCK_CORNER_CONVEX 'n'
#define LCR_BLOCK_CORNER_CONCAVE 'l'
#define LCR_BLOCK_CHECKPOINT_0 '+' ///< checkpoint, not taken
#define LCR_BLOCK_CHECKPOINT_1 '\'' ///< checkpoint, taken
@ -131,15 +130,10 @@
/*
TODO:
- ramp corner???
- curved corner?
- curved out corner?
- curved "hill"
- bumpy road
- bigger structures like a loop, sloped road etc?
*/
#define LCR_MAP_BLOCK_CACHE_SIZE (8 * 2) /// do not change
#define LCR_MAP_BLOCK_CACHE_SIZE (8 * 2) ///< Do not change.
/**
Cache for accelerating LCR_mapGetBlockAtFast, consists of 8 2-item records,
@ -249,7 +243,7 @@ void LCR_rampGetDimensions(uint8_t rampType, uint8_t *height4ths,
uint8_t *LCR_getMapBlockAtCoordNumber(uint32_t coord)
{
// binary search the block:
// Binary search the block:
uint16_t a = 0, b = LCR_currentMap.blockCount - 1;

152
racing.h
View file

@ -69,7 +69,7 @@ typedef int32_t LCR_GameUnit; ///< abstract game unit
#define LCR_CAR_CRASH_SPEED_SMALL 400
#define LCR_CAR_CRASH_SPEED_BIG 800
// multipliers (in 8ths) of friction and acceleration on concrete:
// Multipliers (in 8ths) of friction and acceleration on concrete:
#define LCR_CAR_GRASS_FACTOR 5
#define LCR_CAR_DIRT_FACTOR 3
#define LCR_CAR_ICE_FACTOR 1
@ -87,7 +87,7 @@ struct
TPE_Joint carJoints[LCR_CAR_JOINTS];
TPE_Connection carConnections[LCR_CAR_CONNECTIONS];
uint32_t tick;
uint32_t tick; ///< Physics tick (frame) number.
uint8_t wheelCollisions; /**< In individual bits records for each car wheel
whether it's currently touching the ground.
Lower bits record current collisions, higher
@ -111,10 +111,9 @@ struct
uint16_t crashState;
uint8_t playingReplay;
} LCR_racing;
struct
{
struct
{
uint16_t eventCount;
uint16_t events[LCR_SETTING_REPLAY_MAX_SIZE];
@ -122,7 +121,8 @@ struct
uint16_t currentEvent;
uint16_t currentFrame;
uint32_t achievedTime;
} LCR_replay; // TODO: move inside LCR_racing?
} replay;
} LCR_racing;
/**
Gets times of the run in milliseconds.
@ -144,15 +144,15 @@ TPE_Vec3 _LCR_TPE_vec3DividePlain(TPE_Vec3 v, TPE_Unit d)
void LCR_replayInitRecording(void)
{
LCR_LOG1("initializing replay recording");
LCR_replay.eventCount = 0;
LCR_replay.achievedTime = 0;
LCR_racing.replay.eventCount = 0;
LCR_racing.replay.achievedTime = 0;
}
void LCR_replayInitPlaying(void)
{
LCR_LOG1("initializing replay playing");
LCR_replay.currentEvent = 0;
LCR_replay.currentFrame = 0;
LCR_racing.replay.currentEvent = 0;
LCR_racing.replay.currentFrame = 0;
}
/**
@ -185,14 +185,16 @@ void LCR_replayOutputStr(void (*printChar)(char))
// 8 decimal digits are enough to record 24 hours
#define PUTD(order) printChar('0' + (LCR_replay.achievedTime / order) % 10);
#define PUTD(order) \
printChar('0' + (LCR_racing.replay.achievedTime / order) % 10);
PUTD(10000000) PUTD(1000000) PUTD(100000) PUTD(10000)
PUTD(1000) PUTD(100) PUTD(10) PUTD(1)
#undef PUTD
for (int i = 0; i < LCR_replay.eventCount; ++i)
for (int i = 0; i < LCR_racing.replay.eventCount; ++i)
{
uint16_t e = LCR_replay.events[i];
uint16_t e = LCR_racing.replay.events[i];
printChar(':');
for (int j = 0; j < 4; ++j)
@ -208,37 +210,27 @@ void LCR_replayOutputStr(void (*printChar)(char))
/**
Reads replay from string using provided function that returns next character
in the string. The mapHash and nameHash pointers are optional: if non-zero,
they will be filled with the map hash and name hash. Returns 1 on success,
else 0.
the memory they point to will be filled with the map hash and name hash.
Returns 1 on success, else 0.
*/
int LCR_replayLoadFromStr(char (*nextChar)(void),
uint32_t *mapHash, uint16_t *nameHash)
{
char c = ' ';
LCR_replay.eventCount = 0;
LCR_replay.achievedTime = 0;
LCR_racing.replay.eventCount = 0;
LCR_racing.replay.achievedTime = 0;
if (nameHash)
if (nameHash)
*nameHash = _LCR_simpleStrHash(nextChar,';');
else
else
_LCR_simpleStrHash(nextChar,';');
/*
do // map name
{
c = nextChar();
if (c == 0)
return 0;
} while (c != ';');
*/
if (mapHash)
if (mapHash)
*mapHash = 0;
for (int i = 0; i < 8; ++i) // hash
{
for (int i = 0; i < 8; ++i) // hash
{
c = nextChar();
if (_LCR_hexDigitVal(c) < 0)
@ -246,13 +238,7 @@ for (int i = 0; i < 8; ++i) // hash
if (mapHash)
*mapHash = ((*mapHash) << 4) | _LCR_hexDigitVal(c);
}
/*
for (int i = 0; i < 8; ++i) // hash
if (_LCR_hexDigitVal(nextChar()) < 0)
return 0;
*/
}
nextChar();
@ -263,7 +249,8 @@ for (int i = 0; i < 8; ++i) // hash
if (c < '0' || c > '9')
break;
LCR_replay.achievedTime = LCR_replay.achievedTime * 10 + c - '0';
LCR_racing.replay.achievedTime =
LCR_racing.replay.achievedTime * 10 + c - '0';
}
while (c != 0) // events
@ -278,11 +265,11 @@ for (int i = 0; i < 8; ++i) // hash
if (e == 0)
break;
if (LCR_replay.eventCount >= LCR_SETTING_REPLAY_MAX_SIZE)
if (LCR_racing.replay.eventCount >= LCR_SETTING_REPLAY_MAX_SIZE)
return 0;
LCR_replay.events[LCR_replay.eventCount] = e;
LCR_replay.eventCount++;
LCR_racing.replay.events[LCR_racing.replay.eventCount] = e;
LCR_racing.replay.eventCount++;
}
c = nextChar();
@ -292,60 +279,61 @@ for (int i = 0; i < 8; ++i) // hash
}
/**
During playing of a replay returns the next input and shifts to next frame.
When playing back a replay this function returns the next recorded input and
shifts to the next frame.
*/
uint8_t LCR_replayGetNextInput(void)
{
if (LCR_replay.currentEvent >= LCR_replay.eventCount)
if (LCR_racing.replay.currentEvent >= LCR_racing.replay.eventCount)
{
LCR_replay.currentFrame++; // has to be here
LCR_racing.replay.currentFrame++; // has to be here
return 0;
}
if (LCR_replay.currentFrame ==
(LCR_replay.events[LCR_replay.currentEvent] >> 4))
if (LCR_racing.replay.currentFrame ==
(LCR_racing.replay.events[LCR_racing.replay.currentEvent] >> 4))
{
LCR_replay.currentEvent++;
LCR_replay.currentFrame = 0;
LCR_racing.replay.currentEvent++;
LCR_racing.replay.currentFrame = 0;
}
LCR_replay.currentFrame++;
LCR_racing.replay.currentFrame++;
return LCR_replay.currentEvent ?
(LCR_replay.events[LCR_replay.currentEvent - 1] & 0x0f) : 0;
return LCR_racing.replay.currentEvent ?
(LCR_racing.replay.events[LCR_racing.replay.currentEvent - 1] & 0x0f) : 0;
}
int LCR_replayHasFinished(void)
{
if (LCR_replay.currentEvent == LCR_replay.eventCount)
if (LCR_racing.replay.currentEvent == LCR_racing.replay.eventCount)
{
uint32_t totalTime = LCR_replay.currentFrame;
uint32_t totalTime = LCR_racing.replay.currentFrame;
for (int i = 0; i < LCR_replay.eventCount; ++i)
totalTime += LCR_replay.events[i] >> 4;
for (int i = 0; i < LCR_racing.replay.eventCount; ++i)
totalTime += LCR_racing.replay.events[i] >> 4;
return totalTime >= LCR_replay.achievedTime;
return totalTime >= LCR_racing.replay.achievedTime;
}
return LCR_replay.currentEvent > LCR_replay.eventCount;
return LCR_racing.replay.currentEvent > LCR_racing.replay.eventCount;
}
/**
Records another input event. Returns 1 on success, or 0 if the event couldn't
be recorded. The event is only added if necessary, i.e. this function can
(and must) be called every frame without worrying about inflating its size.
When the run ends, the LCR_REPLAY_EVENT_END has to be fed!
Records another input event into a replay. Returns 1 on success, or 0 if the
event couldn't be recorded. The event is only added if necessary, i.e. this
function can, and MUST, be called every frame without worrying about inflating
its size. When the run ends, the LCR_REPLAY_EVENT_END has to be fed!
*/
int LCR_replayRecordEvent(uint32_t frame, uint8_t input)
{
LCR_LOG2("recording replay event");
if (LCR_replay.achievedTime)
if (LCR_racing.replay.achievedTime)
return 1; // already finished
if (input == LCR_REPLAY_EVENT_END)
{
LCR_replay.achievedTime = frame;
LCR_racing.replay.achievedTime = frame;
LCR_LOG1("replay recording finished");
return 1;
}
@ -354,10 +342,10 @@ int LCR_replayRecordEvent(uint32_t frame, uint8_t input)
uint8_t previousInput = 0;
uint32_t previousFrame = 0;
for (int i = 0; i < LCR_replay.eventCount; ++i)
for (int i = 0; i < LCR_racing.replay.eventCount; ++i)
{
previousInput = LCR_replay.events[i] & 0x0f;
previousFrame += LCR_replay.events[i] >> 4;
previousInput = LCR_racing.replay.events[i] & 0x0f;
previousFrame += LCR_racing.replay.events[i] >> 4;
}
if (input == previousInput)
@ -368,30 +356,30 @@ int LCR_replayRecordEvent(uint32_t frame, uint8_t input)
frame -= previousFrame; // convert to offset
while (frame > 4095 && LCR_replay.eventCount < LCR_SETTING_REPLAY_MAX_SIZE)
while (frame > 4095 && LCR_racing.replay.eventCount < LCR_SETTING_REPLAY_MAX_SIZE)
{
// add intermediate events
frame -= 4095;
previousFrame += 4095;
LCR_replay.events[LCR_replay.eventCount] =
LCR_racing.replay.events[LCR_racing.replay.eventCount] =
(previousFrame << 4) | previousInput;
LCR_replay.eventCount++;
LCR_racing.replay.eventCount++;
}
if (LCR_replay.eventCount >= LCR_SETTING_REPLAY_MAX_SIZE)
if (LCR_racing.replay.eventCount >= LCR_SETTING_REPLAY_MAX_SIZE)
return 0;
LCR_replay.events[LCR_replay.eventCount] = (frame << 4) | (input & 0x0f);
LCR_replay.eventCount++;
LCR_racing.replay.events[LCR_racing.replay.eventCount] = (frame << 4) | (input & 0x0f);
LCR_racing.replay.eventCount++;
#endif
return 1;
}
/**
Helper function for _LCR_racingEnvironmentFunction, returns closest point
on a map block placed at coordinate origin.
Helper function for _LCR_racingEnvironmentFunction, returns closest point on
a map block placed at coordinate origin.
*/
TPE_Vec3 _LCR_racingBlockEnvFunc(TPE_Vec3 point, const uint8_t *block)
{
@ -691,8 +679,8 @@ TPE_Vec3 _LCR_racingBlockEnvFunc(TPE_Vec3 point, const uint8_t *block)
}
/**
For tinyphysicsengine, function that defines the shape of physics world,
returns closest point to any given point in space.
For tinyphysicsengine, function that defines the shape of the static physics
world, returns closest point to any given point in space.
*/
TPE_Vec3 _LCR_racingEnvironmentFunction(TPE_Vec3 point, TPE_Unit maxDist)
{
@ -795,10 +783,10 @@ LCR_GameUnit LCR_racingGetCarSpeedSigned(void)
uint8_t _LCR_racingCollisionHandler(uint16_t b1, uint16_t j1, uint16_t b2,
uint16_t j2, TPE_Vec3 p)
{
// check which wheels are touching the ground.
#if LCR_SETTING_CRASH_SOUNDS
TPE_Unit speed = TPE_vec3Len( // detect crashes
// Detect crashes:
TPE_Unit speed = TPE_vec3Len(
TPE_vec3Project(
TPE_vec3(
LCR_racing.carBody.joints[j1].velocity[0],
@ -811,6 +799,8 @@ uint8_t _LCR_racingCollisionHandler(uint16_t b1, uint16_t j1, uint16_t b2,
(speed >= LCR_CAR_CRASH_SPEED_SMALL);
#endif
// Check which wheels are touching the ground:
if (j1 < 4) // wheel joint?
LCR_racing.wheelCollisions |= 0x01 << j1;