General Programming Guide: Difference between revisions
mNo edit summary |
mNo edit summary |
||
| Line 112: | Line 112: | ||
|None | |None | ||
|Indicates absence of a value. Used for pointers or to declare functions that don't return a value. | |Indicates absence of a value. Used for pointers or to declare functions that don't return a value. | ||
|void MyFunction( | |void MyFunction() | ||
{ // Code } | { // Code } | ||
|} | |} | ||
| Line 164: | Line 164: | ||
<code>Uint32 value2 = 0x6E; // 110</code> | <code>Uint32 value2 = 0x6E; // 110</code> | ||
For integer values, the decimal conversion of a hexadecimal value can be different depending on whether the value is supposed to be signed or not. For example, <code>0xFF00</code> would be <code>65280</code> if unsigned, and <code>-256</code> if signed. | |||
Floating point values of the <code>float</code> type consist of [[wikipedia:Single-precision_floating-point_format|the sign, the exponent and the significand (mantissa) precision]], which can be stored in four bytes. It is hard to tell from the hexadecimal representation what the float's value is, so you can use an online converter such as [https://gregstoll.dyndns.org/~gregstoll/floattohex/ this one], or a tool like a hex editor. However, some floats are easy to recognize when you're looking at hexadecimal data. For example, <code>0x00000000</code> is <code>0.0</code> (if it's a float), and <code>0x3F800000</code> is <code>1.0</code>. Likewise, <code>-1.0</code> is <code>0xBF800000</code>. | Floating point values of the <code>float</code> type consist of [[wikipedia:Single-precision_floating-point_format|the sign, the exponent and the significand (mantissa) precision]], which can all be stored in four bytes. It is hard to tell from the hexadecimal representation what the float's value is, so you can use an online converter such as [https://gregstoll.dyndns.org/~gregstoll/floattohex/ this one], or a tool like a hex editor. However, some floats are easy to recognize when you're looking at hexadecimal data. For example, <code>0x00000000</code> is <code>0.0</code> (if it's a float), and <code>0x3F800000</code> is <code>1.0</code>. Likewise, <code>-1.0</code> is <code>0xBF800000</code>. | ||
=== Pointers === | === Pointers === | ||
Revision as of 13:30, 5 October 2025
This guide is meant for complete beginners without prior knowledge or experience of programming in C.
Note: Some information here is only relevant in the context of creating mods, and may not apply to other programming languages or contexts. Some details and accuracy have been sacrificed for the sake of simplicity.
Comments
Comments in C are preceded by //. Everything after // on the same line will be regarded as a comment, and ignored during compilation.
You can also use /* and */ for multi-line comment blocks like this:
// This is a single-line comment /* This is a multi-line comment. This is the second line of the multi-line comment. This is the last line of the multi-line comment. */
You can disable lines of code by putting // at the start. With /* and */ you can comment out entire code blocks.
Bits and Bytes
Bits are the most basic units of information that can store a binary state: 0 (true) or 1 (false). A byte consists of eight bits, so it can store eight independent binary states.
Data Types
Data and values can be represented in many different ways (types) that determine how much space the data takes. The Sonic Adventure games were built with the Ninja SDK, which has its own names for common types in C. When you write your code, it's recommended to use the Ninja types.
Integer values can be signed or unsigned. Unsigned values have a larger range but cannot be below zero.
Non-integer values are stored with single (around 7 digits) or double (around 16-17 digits) precision.
| C type | Ninja type | Size in bits | Size in bytes | Range | Description | Example declaration |
|---|---|---|---|---|---|---|
| signed char | Sint8 | 8 | 1 | -128 to 127 | Signed 8-bit integer. | Sint8 myvalue = -5; |
| unsigned char | Uint8 | 8 | 1 | 0 to 255 | Unsigned 8-bit integer. | Uint8 myvalue = 255; |
| __int16 | Sint16 | 16 | 2 | -32768 to 32767 | Signed 16-bit integer. | Sint16 myvalue = -1000; |
| unsigned __int16 | Uint16 | 16 | 2 | 0 to 65535 | Unsigned 16-bit integer. | Uint16 myvalue = 33000; |
| int | Sint32 | 32 | 4 | -2147483648 to 2147483647 | Signed 32-bit integer. | Sint16 myvalue = -80000; |
| unsigned int | Uint32 | 32 | 4 | 0 to 4294967295 | Unsigned 32-bit integer. | Uint16 myvalue = 900000; |
| bool | Bool | 8 or 32 | 1 or 4 | Either True or False | A boolean value can only be true or false. Note that the size of the boolean type can be different depending on the environment.
For modding purposes, the C |
Bool myflag = true; |
| float | Float | 32 | 4 | 1.2E-38 to 3.4E+38 | 32-bit floating point with single precision. Can accommodate up to 7 digits after or before a decimal point.
Make sure to put |
Float myvalue = 100.0f; |
| double | Double | 64 | 8 | 1.7E-308 to 1.7E+308 | 64-bit floating point with double precision. Can accommodate up to 16 to 17 digits after or before a decimal point. | Double myvalue = 123.123456789; |
| void | Void | 32 | 4 | None | Indicates absence of a value. Used for pointers or to declare functions that don't return a value. | void MyFunction()
{ // Code } |
Note: Because of the way floating point values are stored, they cannot always be converted exactly, and you should not use exact comparisons involving float and double types. For example, 0.1f + 0.2f would not be equal to 0.3f, often you would see values such as 0.699999... instead of 0.7 etc.
Decimal and Hexadecimal Representations
When working with data, it is often useful to represent values in the hexadecimal, rather than decimal, format. In the hexadecimal format, a single digit can be from 0 to 15, with A~F representing 10-15.
| Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hexadecimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | 10 |
In the hexadecimal format, the decimal value 22 would be 16, and the decimal value 16 would be 10. To help distinguish hexadecimal values, the 0x prefix is used:
Uint32 value1 = 0xE; // 14
Uint32 value2 = 0x6E; // 110
For integer values, the decimal conversion of a hexadecimal value can be different depending on whether the value is supposed to be signed or not. For example, 0xFF00 would be 65280 if unsigned, and -256 if signed.
Floating point values of the float type consist of the sign, the exponent and the significand (mantissa) precision, which can all be stored in four bytes. It is hard to tell from the hexadecimal representation what the float's value is, so you can use an online converter such as this one, or a tool like a hex editor. However, some floats are easy to recognize when you're looking at hexadecimal data. For example, 0x00000000 is 0.0 (if it's a float), and 0x3F800000 is 1.0. Likewise, -1.0 is 0xBF800000.
Pointers
A pointer is a variable that stores the memory address of another variable. To put it simply, "pointer to 'myvalue'" means "the location of 'myvalue'".
Pointers are declared with the * operator:
Uint32 value = 80000; // Declare the value
Uint32* ptr_value = &value; // Set the pointer 'ptr_value' to the memory address of 'value'
To retrieve or modify the value referenced (pointed to) by a pointer, you need to dereference it using the * operator.
To retrieve the address of the value, you can use the & operator.
Uint32 value = 80000; // Declare the value
Uint32* ptr_value = &value; // Set the pointer 'ptr_value' to the memory address of 'value'
*ptr_value = 75000; // Change 'value' from 80000 to 75000 by dereferencing the pointer.
Uint32 newvalue = *ptr_value; // Declare a new variable and set it to the value referenced by the pointer.
Uint32 address = &ptr_value; // Declare a new variable and set it to the address of the value referenced by the pointer.
In the example above, the type of the value referenced by the pointer (Uint32) is known, so the pointer is declared as Uint32*. You can also declare pointers with void* without specifying the data type. To manipulate data referenced by void pointers, you will need to cast them to other types (see below).
Arrays
Arrays are groups of items of the same type. Use square brackets to define an array of a specified type, and curly brackets for its contents:
Uint32 myarray[] = { 1000, 2000, 3000, 4000, 5000 }; // Defines an array of five unsigned integers
Uint32 cleararray[20] = { }; // Defines an array of 20 unsigned integers that are all 0
To access individual items (members) of an array, use the item index in square brackets after the array's name. Array indices are 0-based, which means the first item will have index 0, the second item will be 1 and so on.
Uint32 myarray[] = { 1000, 2000, 3000, 4000, 5000 }; // Defines an array of five unsigned integers
Uint32 myitem = myarray[3]; // The unsigned integer 'myitem' will be the fourth member of the array (4000)
myarray[1] = 0; // Sets the second item of the array (2000) to 0.
Structures
Structures (structs) are custom data types. They can consist of common data types mentioned above, or other data structs.
Let's have a look at a commonly used Ninja struct NJS_POINT3. A point consists of three floats for its X, Y and Z.
struct NJS_POINT3
{
Float x;
Float y;
Float z;
};
NJS_POINT3 mypoint = { 1.0f, 0.0f, -1.0f }; // Defines a point located at X = 1.0, Y = 0, Z = -1
Individual items inside the struct are called members or fields. You can access individual members of a struct using the . operator.
NJS_POINT3 mypoint = { 1.0f, 0.0f, -1.0f }; // Defines a point located at X = 1.0, Y = 0, Z = -1
mypoint.z = 2.0f; // Sets the point's Z value to 2.0
Float x_value = mypoint.x; // Defines a separate value that equals the point's X value
Casts
Type casting means converting between data types. In mods, sometimes you need to access certain data types as if they were a different type. For example, you can divide two integers to get a result with a decimal point like so:
int value1 = 5;
int value2 = 2;
float nocast = value1/value2 + 1.0f;
float cast = (float)value1/value2 + 1.0f;
// The value of 'nocast' will be 3.0f
// The value of 'cast' will be 3.5f
Casts are often used with pointers:
void* mymem = MAlloc(4)); // Calls the function MAlloc to allocate 4 bytes of memory and set the 'mymem' pointer to the location of those bytes.
Uint32* myvalptr = (Uint32*)mymem; // Declares a pointer 'myvalptr' as a pointer to an unsigned 32-bit (4-byte) integer.
Defines
You can use #define for constants (values that don't change). For example, if you have a "maximum number" of something that is often used in comparisons, you can define it like this:
#define MAX_NUMBER 32
With this, instead of writing 32 every time you do a comparison, you can compare to MAX_NUMBER. This is also convenient when you want to change this constant without rewriting all comparisons - just change the #define line and it will reflect in all of your code.
Macros
A macro is a snippet of code that has a name. You can use macros if you have a lot of repeating code to make it look cleaner and to avoid excessive rewriting when you make edits to it. Macros are created with #define.
For example, let's look at the following code:
if (angle_x != 0)
njRotateX(NULL, angle_x);
if (angle_y != 0)
njRotateY(NULL, angle_y);
if (angle_z != 0)
njRotateZ(NULL, angle_z);
This code compares three angle variables (X, Y and Z) against zero, and if they are not zero, it calls one of the Ninja njRotate functions for either X, Y or Z. This is often used in model rendering code. However, if you have to render a lot of models and perform these rotations for each model, it quickly becomes convoluted. To simplify this code, we can set up macros to perform the check against zero and rotate accodringly:
#define ROTATEX(m, ang) if (ang != 0) njRotateX(m, SHORT_ANG(ang));
#define ROTATEY(m, ang) if (ang != 0) njRotateY(m, SHORT_ANG(ang));
#define ROTATEZ(m, ang) if (ang != 0) njRotateZ(m, SHORT_ANG(ang));
With the use of these three macros the code becomes cleaner:
ROTATEX(NULL, angle_x);
ROTATEY(NULL, angle_y);
ROTATEZ(NULL, angle_z);
Enums
Enums are a custom data type that contains named integer constants. You can use enums for lists of different states, such as stopped, moving, finished etc. A commonly used enum is for "modes" used to program behavior of objects and enemies in SADX.
enum
{
MODE_INIT = 0,
MODE_MAIN, // = 1
MODE_DEBUG, // = 2
MODE_END // = 3
};
With this, instead of using 0, 1, 2 and 3 you can use MODE_INIT, MODE_MAIN etc.
Uint32 mode = MODE_INIT;
if (something_happened)
mode = MODE_MAIN;
Functions
A function is what makes things happen in your mod. Functions can have arguments (input) and return values (output). The function below returns a sum of two values. Its arguments are value1 and value2, both Uint32, and the return value is also Uint32.
Uint32 CalculateTwoNumber(Uint32 value1, Uint32 value2)
{
return value1 + value2;
}
Functions can call other functions. There can also be functions without arguments and/or without return values. If a function doesn't return a value, void is used, and the return statement is not required. Below is a function that doesn't take any arguments, calls another function and doesn't return anything.
void PrintSomething()
{
PrintDebug("This is a test");
}
A fully specified function with code like in examples above is called a function prototype. If you want to reuse the function in a different source file (see below), you can use shortened definitions known as function declarations:
Uint32 CalculateTwoNumbers(Uint32 value1, Uint32 value2);
void PrintSomething();
Conditions (using if and else)
For conditions, if, else if and else statements are used. The condition itself is enclosed in brackets. Here is a simple function that compares values and outputs different text based on the result of comparison.
void CompareTwoValues(Uint32 value1, Uint32 value2)
{
if (value1 == value2)
PrintDebug("Values are equal");
else if (value1 > value2)
PrintDebug("Value 1 is bigger");
else
PrintDebug("Value 2 is bigger");
}
Note that == is used instead of = to check if the values are equal. Do not use = for comparisons. Also, do not use == or != for comparing floating point values.
Apart from the operators in the function above, for arithmetic comparisons you can also use "greater or equal" >=, "lower" <, "lower or equal" <= and "not equal" != .
For conditions with multiple comparisons, you can use the AND && and OR || operators:
if (value1 > 5 && value2 < 3) // The condition is true if value1 is bigger than 5 and value2 is less than 3
if (value1 > 5 || value2 < 3) // The condition is true if either value1 is bigger than 5 or value2 is less than 3
if (value1 > value2 || value3 < value4) // The condition is true if value1 is bigger than value2 OR value3 is bigger than value4
if (value1 > value2 && value3 < value4) // The condition is true if value1 is bigger than value2 AND value3 is bigger than value4
Switch statements
If you have to compare the same variable against different values multiple times, switch statements are a great way to improve readability of your code.
Let's look at the following function:
Uint32 test_a(Uint32 a)
{
if (a == 0)
return 1;
else if (a == 1)
return 2;
else if (a == 2)
return 5;
else if (a == 10)
return 7;
else
return 0;
}
There are many comparisons of a. Instead of repeating else if, we can use a switch. Each possible value has the corresponding code in each case, and the default case is for values that have not been covered in other cases.
Uint32 test_a(Uint32 a)
{
switch (a)
{
case 0:
return 1;
case 1:
return 2;
case 2:
return 5;
case 10:
return 7;
default:
return 0;
}
}
Using switches together with enums make the code much easier to read:
switch (mode)
{
case MODE_INIT:
return RESULT_NOTMOVING;
case MODE_MOVE:
return RESULT_MOVING;
case MOVE_STOP:
return RESULT_STOPPED;
case MOVE_END:
return RESULT_ENDED;
default:
return RESULT_NONE;
}
Using break and fall through cases
In the previous examples, the function returned at the end of each case. However, often you need to continue executing the function after the switch statement. To finish the case, you add a break; statement. If you don't add it, the case will fall through, and the code will continue executing for the next case. You can combine break and fall through cases to create branching code.
Uint32 value = RESULT_NONE;
switch (mode)
{
case MODE_INIT:
case MOVE_END:
case MOVE_STOP:
value = RESULT_NOTMOVING;
break;
case MODE_MOVE:
value = RESULT_MOVING;
break;
default:
value = RESULT_NONE;
break;
}
if (value == RESULT_MOVING)
PrintDebug("Moving!");
In the example above, in MODE_INIT, MODE_END and MODE_STOP the value will be RESULT_NOTMOVING, but in MODE_MOVING the value will be RESULT_MOVING.
Source and Header Files
Source files have a .c or .cpp extension. They contain definitions of variables and function prototypes.
Header files have an .h extension. They contain declarations of variables and functions.
In other words, source files have the actual implementation of your code, while header files contain the interface to that implementation. As such, source and header files often come in pairs sharing the same name, e.g. mod.cpp and mod.h.
Using headers
To use the interface from one source file in a different source file, you need to #include the header. For example, to enable the use of SADX Mod Loader types, functions etc. in your mod, you need to add the line #include <SADXModLoader.h> at the top of your .cpp file.
#include can be used with angle brackets (#include <file.h>) or quotation marks (#include "file.h"). Angle brackets mean the compiler will search for the header in the include path list for your project (set up in project properties in Visual Studio). Quotation marks mean the compiler will start looking for the header in the same folder as your source file, and if the header isn't there it will look in the include path list.
You can use relative paths with #include. For example, #include "../header.h" will look for the header header.h in the parent folder of the source file, and #include "test/header2.h" will look for header2.h in the "test" subfolder.
Using #pragma once
Depending on the contents of your header, including the same header in multiple source files can lead to a compilation error. To avoid such errors, add #pragma once at the top of the header. This directive tells the compiler to only include that header once in a single compilation.