A 3D sandbox game by Java.
Discuss the things we need to identify in game.
Name | What does it referring? | How to identify? | Why? |
---|---|---|---|
Action | The abstract action, like command, but this should include the action like "player move forward" | By string id (player.move.forward) | We need to bind key or command or other input way to these to control game |
Game Object Type | The type of ingame object, like block prototype, item prototype, entity type. | By semantic string id (block.stone) | Majorly used for (de)serialization on network or disk |
Game Object | The important things ingame, mostly the object under the world life cycle (entity, block, item) | By hierarchy structure string. (block requires location, entity requires id) e.g. /<dimension id>/<world id>/<position> | We need to have a universal way to refer an ingame object which provides a easy way to communicate between client and server |
Resource | The game resource in disk (or remote) | By its string dir | We need to loadOrder resources obviously |
We have to manage these things differently though.
The Actions
and GameObjectType
should be managed by individual registries.
The Resource
should managed by ResourceManager
. It should provide the basic level of caching (cache bytes).
The resource will transform to other form (Texture, Model, Sound, Animation) and linked with its location.
After the loading stage of a game, all the resource transformed out. The raw bytes cache should be cleared.
The GameObject
should managed by its parent. We don't need to do anything special to it.
Just process the dir to get dimension, get world, and get block or entity.
The ideal way to register a thing is only providing its id without any redundant information. The Mod should not consider too much about conflicting with other Mod.
Registry<BlockObject> registry;
registry.register(block.setRegistryName('air')); // infer the query full dir is <current modId>.block.air
Registry<ItemObject> itemRegistry;
itemRegistry.register(item.setRegisterName('stone')); // infer the query full dir is <current modId>.item.stone
RegistryManager manager;
manager.register(block.setRegistryName('stone')); // infer the query full dir is <current modId>.block.stone
// when we want to get a registered object
manager.get('unknowndomain.block.stone'); // get the stone block
I suggest we all use the snake case (split word with low_dash) for register name.
The name should not contain any dot/period (.)
There could be sub-named blocks and items. For block, it might be produced by combining properties.
- initialize glfw opengl, window and other hook
- initialize resource manager, pull default resource source
- initialize mod manager
- initialize default renderer, which will require loadOrder default textures and objects
- initialize player profile, login information (GUI show to let player login if there is no local cached profile)
- pull the resources/mod manifest from server
- check local if they exist
- download missing mod and resource
- initialize action manager
- initialize keybinding, requiring action manager
- initialize game context
- loadOrder all mods by mod manager
- mod register all block/item/entity
- resource manager loads all required resources by mod
- use custom mod resource process pipeline to process resource
- physics and collision
- use joml AABB to deal with bonding
- maybe a general manager under
World
to manage physics?
- raycasting to pick block or entity
- use joml Ray to deal with picking
- logic/render object state management and update cycle
- naive idea is that we keep two different form of data in game and renderer thread.
- the data in logic thread are changed by event (the event could toggle by network/user input)
- after the logic thread receive event, it should emit the change which only exposed to renderer thread
- the data in renderer thread only receives the change provide by logic thread; ideally it won't query logic thread data by it self
- face culling
A block model A has N vertices, then it vertex length is N * 3
[x0 y0 z0 x1 y1 z1 ... xn yn zn]
Bake the chunk as combination of block model:
blockAt (0,0,0) (0,0,1) ....
[x0 y0 z0 x1 y1 z1 ... xn yn zn] [x0 y0 z0 x1 y1 z1 ... xn yn zn] ...
Suppose len(m)
is the vertices count of model m
.
We maintains a summed list L
where L[x] = L[x - 1] + len(model at index x)
When a block is changed in chunk, we update the render chunk data by swap the block vertex:
Suppose the block at position (x, y, z)
changed,
float[] newVertices; // passed by param
int index = (x & 0xF) < 8 | (y & 0xF) < 4 | (z & 0xF);
int leftSum = L[index - 1];
int rightSum = L[index];
int totalLength = L[(0xF) < 8 | (0xF) < 4 | (0xF)];
int newRightSum = leftSum + newVertices.length;
int oldVerticesCount = rightSum - leftSum;
// these are not real gl commands
// shift the right vertices
glCopy(rightSum, newRightSum, totalLength - rightSum);
// upload the new block vertices to the correct position
glUpload(newVertices, leftSum);