├── .gitignore ├── LICENSE ├── README.md ├── c4.nimble ├── c4 ├── entities.nim ├── examples │ └── ping-pong │ │ ├── project.nim │ │ ├── project.nimble │ │ ├── project.nims │ │ └── src │ │ ├── messages.nim │ │ ├── scenarios │ │ ├── connection.nim │ │ ├── movement.nim │ │ └── start.nim │ │ └── systems │ │ ├── input.nim │ │ ├── network.nim │ │ ├── physics.nim │ │ └── video.nim ├── lib │ ├── enet │ │ ├── README.md │ │ ├── enet.nim │ │ └── enet.nimble │ ├── ode │ │ ├── README.md │ │ └── ode.nim │ └── ogre │ │ ├── README.md │ │ ├── ogre.nim │ │ └── ogre.nimble ├── logging.nim ├── loop.nim ├── messages.nim ├── processes.nim ├── sugar.nim ├── systems.nim ├── systems │ ├── input │ │ └── sdl.nim │ ├── network │ │ └── net.nim │ ├── physics │ │ ├── ode.nim │ │ └── simple.nim │ └── video │ │ ├── ogre.nim │ │ └── sdl.nim ├── templates │ ├── 2d │ │ ├── project.nim │ │ ├── project.nimble │ │ ├── project.nims │ │ └── src │ │ │ ├── messages.nim │ │ │ ├── scenarios │ │ │ ├── collision.nim │ │ │ ├── connection.nim │ │ │ └── movement.nim │ │ │ └── systems │ │ │ ├── input.nim │ │ │ ├── network.nim │ │ │ ├── physics.nim │ │ │ └── video.nim │ ├── action │ │ ├── project.nim │ │ ├── project.nimble │ │ ├── project.nims │ │ └── src │ │ │ ├── messages.nim │ │ │ ├── scenarios │ │ │ ├── connection.nim │ │ │ ├── entity.nim │ │ │ ├── impersonation.nim │ │ │ ├── init.nim │ │ │ ├── player_actions.nim │ │ │ └── position.nim │ │ │ └── systems │ │ │ ├── input.nim │ │ │ ├── network.nim │ │ │ ├── physics.nim │ │ │ └── video.nim │ └── base │ │ ├── project.nim │ │ ├── project.nimble │ │ ├── project.nims │ │ └── src │ │ ├── messages.nim │ │ ├── scenarios │ │ └── init.nim │ │ └── systems │ │ ├── input.nim │ │ ├── network.nim │ │ ├── physics.nim │ │ └── video.nim ├── threads.nim └── utils │ ├── floats.nim │ ├── loading.nim │ ├── loglevel.nim │ └── stringify.nim ├── docs └── tutorials │ ├── 01 - project setup │ ├── readme.md │ └── src │ │ └── main.nim │ ├── 02 - messages │ ├── readme.md │ └── src │ │ └── main.nim │ ├── 03 - processes and threads │ ├── readme.md │ └── src │ │ ├── processes_and_threads.nim │ │ ├── processes_creation.nim │ │ ├── threads_communication.nim │ │ └── threads_creation.nim │ ├── 04 - ecs │ ├── readme.md │ └── src │ │ ├── components.nim │ │ └── entities.nim │ ├── 05 - systems │ ├── readme.md │ └── src │ │ ├── main.nim │ │ └── systems │ │ └── fps.nim │ ├── 06 - video system │ ├── readme.md │ └── src │ │ ├── 2d │ │ ├── main.nim │ │ └── systems │ │ │ └── video.nim │ │ └── 3d │ │ ├── consts.nim │ │ ├── main.nim │ │ ├── messages.nim │ │ ├── nim.cfg │ │ ├── plugins.cfg │ │ └── systems │ │ └── video.nim │ ├── 07 - input system │ ├── readme.md │ └── src │ │ ├── main.nim │ │ ├── messages.nim │ │ └── systems │ │ ├── input.nim │ │ └── video.nim │ ├── 08 - physics system │ ├── readme.md │ └── src │ │ ├── main.nim │ │ └── systems │ │ └── physics.nim │ ├── 09 - network system │ ├── readme.md │ └── src │ │ ├── main.nim │ │ ├── systems │ │ └── input.nim │ │ └── threads.nim │ ├── 10 - simple 2d game │ ├── readme.md │ └── src │ │ ├── main.nim │ │ ├── messages.nim │ │ ├── scenarios │ │ └── master.nim │ │ ├── systems │ │ ├── input.nim │ │ ├── network.nim │ │ ├── physics.nim │ │ └── video.nim │ │ └── threads.nim │ └── 11 - simple 3d game │ ├── readme.md │ └── src │ ├── main.nim │ ├── messages.nim │ ├── nim.cfg │ ├── plugins.cfg │ ├── scenarios │ ├── entity_create.nim │ ├── entity_move.nim │ ├── hello.nim │ └── player.nim │ ├── systems │ ├── input.nim │ ├── network.nim │ ├── physics.nim │ └── video.nim │ ├── threads.nim │ └── utils.nim └── nim.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | ### Above combination will ignore all files without extension ### 11 | 12 | nimcache 13 | _* 14 | *.log 15 | build 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cat 400 2 | 3 | "Cat 400" (c4) is a game framework for Nim programming language. 4 | 5 | ## Brief overview 6 | 7 | "Cat 400" is a cross-platform framework designed to provide nice experience in game development. Written in nim language, it benefits from the language's elegance and expressiveness, as well as compilation to fast native code. 8 | 9 | Core of "Cat 400" is platform-independent and may be run on every target platform supported by nim. However, default systems (which are optional) have external dependencies which may restrict their usage on some platforms. 10 | 11 | # Key features 12 | 13 | - client-server multithreading architecture (even for single-player games) with network support 14 | - modularity: all code is split into "systems" (video/user input/networking etc) which work independently 15 | - systems communicate only by sending messages, thus avoiding tangled code 16 | - ECS (entity-component-system) with custom user components support 17 | - simple overwriting of existing systems and ability to create custom systems 18 | - templates which include some reasonable defaults for specific game genre 19 | 20 | # So why another game framework? 21 | 22 | I've made a research about game engines and frameworks for Nim, and none of them had solid and thoughtful design. It's super easy to display something on screen, and many people think that opportunity for a program to display cubes on screen makes it a game engine. It doesn't. When I see another game framework, I read its documentation (or code, if there are no docs at all) and try to find answers to these questions: 23 | 1) What about physics, i.e. collisions, collision shapes, force, velocity, position etc? 24 | 2) Can I do 3D graphics? 25 | 3) What if I want multiplayer over network? 26 | 4) How do I map user input to specific actions? 27 | 5) How do I occupy all cores effectively? 28 | 6) What about logging? How to debug? 29 | 7) How easy it is to create large project with thousands lines of code? How does the engine help me not get lost? 30 | 31 | Usually many of these questions aren't covered at all, but for `Cat-400` I do my best to address all of them and beyond. 32 | 33 | ## Is 2D/3D supported? 34 | 35 | Yes, 2D via SDL, 3D via SDL+Ogre3d. Also note that `C4` is a _framework_, which means that you can use any backend for any of your systems. So, unlike game _engines_, you can make a 2D, 3D or even 4D ([wat?!](https://www.youtube.com/watch?v=0t4aKJuKP0Q)) game using any graphics library of your choice. 36 | 37 | ## GUI / Game Engine 38 | 39 | `Cat 400` is not a game engine (and will never be), and everything is quite low-level - there's no gui and every aspect of game is done within source code. No level editor, too. 40 | 41 | Feel free to create high-level tools on top of `Cat 400` and share them with community if you have such an opportunity. 42 | 43 | ### But where do I write code? 44 | 45 | I know many game engines (for example, Godot) have integrated basic code editors - but they probably haven't heard about IDEs which 1) are designed for writing code, 2) already exist, are mature and just work, and 3) suit better for coding cause have extra features like plugins. So, open your favorite IDE which supports Nim and start coding a game. 46 | 47 | ### But where do I edit graphics? 48 | 49 | I know many game engines (for example, Godot) have integrated basic mesh editors - but they probably haven't heard about 3D modeling software which 1) are designed for creating 3D models, 2) already exist, are mature and just work, and 3) suit better for modeling cause have extra features like plugins. So, open your favorite 3D modeling software and start creating models. 50 | 51 | ## Documentation 52 | 53 | > WARNING! Due to active development documentation is very outdated. It will be fixed once framework's API is stable. 54 | 55 | ### Tutorials 56 | 57 | Visit [docs/tutorials](docs/tutorials/) folder - it's the best place to learn `Cat 400`. 58 | 59 | ### Reference generation 60 | 61 | In order to make repo clean, autogenerated reference is not included. You may generate it yourself: from repo root run 62 | 63 | ``` 64 | nimble genDocs 65 | ``` 66 | 67 | Generated reference files will be located in `docs/ref` folder. 68 | 69 | ### Examples 70 | 71 | All examples are available in [examples folder](c4/examples). 72 | 73 | You may also check [templates folder](c4/templates). Templates are just app skeletons which contain initialization and minimal game environment, like a movable player and couple of enemies. 74 | 75 | ## Submodules 76 | 77 | Although these modules are part of `Cat-400`, they may be used separately in any project. 78 | 79 | [`c4.entities` module](c4/entities.nim) - entity-component system, allowing to create lightweight entities and attach any user-defined components to them, with some basic CRUD operations. 80 | 81 | [`c4.messages` module](c4/messages.nim) - `Message` type and any user-defined subtypes, which may be packed and unpacked using msgpack, correctly preserving type information. 82 | 83 | [`c4.threads` module](c4/threads.nim) - module for spawning named threads and sending messages between them; allows programmer to focus on his multithreaded app, not on settings up connection between threads. 84 | 85 | ## Wrappers 86 | 87 | There are several wrappers which are subpackages of "Cat-400" and may be installed and used separately, see [lib folder](c4/lib). 88 | 89 | ## Statistics 90 | 91 | [![Stargazers over time](https://starchart.cc/c0ntribut0r/cat-400.svg)](https://starchart.cc/c0ntribut0r/cat-400) 92 | -------------------------------------------------------------------------------- /c4.nimble: -------------------------------------------------------------------------------- 1 | import strutils 2 | import strformat 3 | import os 4 | 5 | version = "0.2.2" 6 | author = "c0ntribut0r" 7 | description = "Game framework" 8 | license = "MPL-2.0" 9 | 10 | # srcDir = "c4" 11 | # installDirs = @["c4"] 12 | # installExt = @["nim", "nims", "nimble", "txt"] 13 | skipDirs = @["docs"] 14 | 15 | # Dependencies 16 | requires "nim >= 2.0" 17 | requires "msgpack4nim == 0.4.4" 18 | requires "chronicles == 0.10.3" 19 | 20 | 21 | proc dirGenDocs(src, dst: string) = 22 | mkDir dst 23 | 24 | for file in src.listFiles: 25 | let (dir, name, ext) = file.splitFile() 26 | 27 | if ext == ".nim" and not name.startsWith("_"): 28 | echo &"Processing {file}" 29 | let 30 | destDir = dst / dir.tailDir 31 | destFile = destDir / name.addFileExt("html") 32 | 33 | mkDir destDir 34 | discard staticExec(&"nim doc0 -o={destFile} {file}") 35 | 36 | for dir in src.listDirs: 37 | let (head, tail) = dir.splitPath() 38 | if not tail.startsWith("_") and tail != nimcacheDir(): 39 | dirGenDocs(dir, dst) 40 | 41 | task genDocs, "Generate doc files": 42 | const docsDir = "docs" / "ref" 43 | docsDir.rmDir() 44 | dirGenDocs("c4", docsDir) 45 | echo &"Generated documetation at {docsDir}" 46 | -------------------------------------------------------------------------------- /c4/entities.nim: -------------------------------------------------------------------------------- 1 | ## This module contains ECS (Entity-Component-System) implementation. 2 | 3 | import tables 4 | export tables 5 | 6 | import ./logging 7 | 8 | 9 | # uint doesn't check boundaries, thus int; set[int32] won't compile, thus int16 10 | type Entity* = int16 ## Entity is just an int which may have components of any type. Zero entity is reserved as "not initialized". Please be careful with threading: all threads share the same entities registry, so if you create an entity in one thread, it will be visible in another thread. However, components aren't shared between threads, so you can't create a component in one thread and access it in another thread (which is not recommended anyway). 11 | 12 | 13 | var entities: set[Entity] 14 | 15 | # ---- Entity ---- 16 | proc isInitialized*(self: Entity): bool = 17 | ## Checks whether entity was initialized using `newEntity()`. 18 | self != 0 19 | 20 | proc newEntity*(): Entity = 21 | ## Return new Entity or raise error if limit exceeded 22 | result = low(Entity) 23 | while result in entities or not result.isInitialized: 24 | result += 1 # TODO: pretty dumb, use random or `lastUsedID` instead 25 | 26 | entities.incl(result) # add entity to global entities registry 27 | debug "created new entity", result 28 | 29 | iterator iterEntities*(): Entity = 30 | for entity in entities: 31 | yield entity 32 | 33 | proc getNumEntities*(): int = 34 | entities.len 35 | 36 | # ---- Components ---- 37 | 38 | # Here goes a hacky code. 39 | # Entity may have as much components as desired. For each type (component) there is a separate table ``Table[Entity, ]``. This table is created automatically by compiler using {.global.} pragma. 40 | # However, we want to delete all components when Entity is deleted. Since components tables are created automatically, we don't have a list of tables to delete from. The only way to know which tables require deletion of components is to create a component destructor for that specific table when it is initialised, and add each destructor to a sequence. 41 | # When Entity is deleted, call all destructors from that sequence. 42 | # The idea is quite simple, but ``{.global.}`` variables have a bit complicated behaviour. In order to make it working we need to make destructors sequence ``{.global.}`` too. 43 | 44 | var seenTables {.threadvar.}: seq[pointer] 45 | var destructors {.threadvar.}: seq[proc(entity: Entity)] 46 | 47 | proc getComponents*(T: typedesc): var Table[Entity, T] = 48 | ## Returns a table of components of specific type ``T`` (``Table[Entity, T]``) 49 | var table {.global, threadvar.}: Table[Entity, T] # https://github.com/nim-lang/Nim/issues/17552 50 | # That's what all nim is about: there is some feature like {.global.} which is documented 51 | # and should work, but after some version update you find out it doesn't; after some googling 52 | # you find some post on nim forum which says that this feature works but only for primitive types, 53 | # and for complex types it works ONLY if you split declaration and initialization. Previously 54 | # you had some additional logic executed only once during initialization, but now you can't do 55 | # it. You start googling how to create your subtype with custom initialization and guess what? 56 | # You cannot! Cause in nim there are no "constructors", everyone can initialize whatever he wants 57 | # in whatever way he wants. So you end up having a global variable of "seen", or "initialized" tables, 58 | # so that when you get a new one, you execute that "only once" logic. And you pray, you pray that 59 | # it will work at least a year until Araq decides to change something again, I dunno, implement 60 | # an 11th garbage collecting algo or some another cool feature like 61 | # "proc ~(*)@$@ from nullptr by nilvar as ptrref {.fuckoff:[@wat].}". I just wanna write code 62 | # and be sure that the code does what the documentation says! But that bug is 2yo now. 63 | let tableAddr = addr(table) 64 | if tableAddr notin seenTables: 65 | seenTables.add(tableAddr) 66 | destructors.add( 67 | proc(entity: Entity) = entity.del(T) 68 | ) 69 | return table 70 | 71 | proc delete*(entity: Entity) = 72 | ## Delete the Entity and all its components. Each component will be deleted as well. 73 | for destructor in destructors: 74 | destructor(entity) 75 | 76 | if entity notin entities: 77 | warn "deleting non-existent entity", entity 78 | entities.excl(entity) # will not alert if entity does not exist 79 | 80 | proc flush*() = 81 | ## Removes all entities 82 | for entity in entities: 83 | entity.delete() 84 | 85 | # ---- CRUD for components ---- 86 | template has*(entity: Entity, T: typedesc): bool = 87 | ## Checks whether ``entity`` has component of type ``T`` 88 | assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`" 89 | 90 | getComponents(T).hasKey(entity) 91 | 92 | template del*(entity: Entity, T: typedesc) = 93 | ## Deletes component ``T`` from ``entity``, or does nothing if ``entity`` doesn't have such a component 94 | assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`" 95 | 96 | getComponents(T).del(entity) 97 | 98 | template `[]`*(entity: Entity, T: typedesc): var typed = 99 | ## Returns ``T`` component for ``entity``. Make sure the component exists before retrieving it. 100 | assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`" 101 | 102 | tables.`[]`(getComponents(T), entity) 103 | 104 | template `[]=`*(entity: Entity, T: typedesc, value: T) = 105 | ## Attaches new component ``T`` to an ``entity``. Previous component (if exists) will be deleted. 106 | assert entity.isInitialized, "Entity is not initialized, possibly forgot to call `newEntity()`" 107 | 108 | entity.del(T) 109 | tables.`[]=`(getComponents(T), entity, value) # N.B. non-ref ``value`` is copied! 110 | 111 | 112 | when isMainModule: 113 | import unittest 114 | import c4/threads 115 | 116 | type 117 | PhysicsComponent = object 118 | value: int 119 | 120 | suite "Entities tests": 121 | test "Undefined entity": 122 | expect AssertionDefect: 123 | var entity: Entity 124 | entity[PhysicsComponent] = PhysicsComponent(value: 5) 125 | 126 | test "Basic usage": 127 | let 128 | entity1 = newEntity() 129 | entity2 = newEntity() 130 | 131 | entity1[PhysicsComponent] = PhysicsComponent(value: 3) 132 | let componentTableAddr1 = getComponents(PhysicsComponent).addr 133 | entity2[PhysicsComponent] = PhysicsComponent(value: 10) 134 | let componentTableAddr2 = getComponents(PhysicsComponent).addr 135 | 136 | check: 137 | entity1[PhysicsComponent].value == 3 138 | componentTableAddr1 == componentTableAddr2 139 | 140 | entity1.del(PhysicsComponent) 141 | check: 142 | not entity1.has(PhysicsComponent) 143 | entity1.delete() 144 | check: 145 | entity1 notin entities 146 | 147 | entity2.del(PhysicsComponent) 148 | entity2.delete() 149 | 150 | test "Auto-destruction of components": 151 | let entity = newEntity() 152 | 153 | entity[PhysicsComponent] = PhysicsComponent() 154 | entity.delete() 155 | 156 | check: 157 | not entity.has(PhysicsComponent) 158 | 159 | test "Multithreading support": 160 | discard newEntity() 161 | 162 | spawnThread ThreadID(1): 163 | let entity2 = newEntity() 164 | entity2[PhysicsComponent] = PhysicsComponent(value: 5) 165 | check: 166 | getNumEntities() == 2 167 | 168 | joinActiveThreads() 169 | 170 | spawnThread ThreadID(2): 171 | let entity3 = newEntity() 172 | entity3[int] = 3 173 | let entity4 = newEntity() 174 | entity4[string] = "name" 175 | check: 176 | getNumEntities() == 4 177 | 178 | joinActiveThreads() 179 | check: 180 | getNumEntities() == 4 181 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/project.nim: -------------------------------------------------------------------------------- 1 | import logging 2 | import net 3 | import strformat 4 | 5 | import c4/processes 6 | import c4/threads 7 | import c4/systems/network/enet 8 | import c4/systems/physics/simple 9 | import c4/systems/input/sdl 10 | import c4/systems/video/sdl as sdlvideo 11 | import c4/utils/loglevel 12 | 13 | import src/systems/[network, physics, input, video] 14 | import src/scenarios/[connection, movement, start] 15 | 16 | 17 | when isMainModule: 18 | run("server"): 19 | spawn("network"): 20 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] server $levelname: ")) 21 | let network = new(ServerNetworkSystem) 22 | if not waitAvailable("physics"): 23 | raise newException(LibraryError, &"Physics system unavailable") 24 | network.init(port=Port(9000)) 25 | network.run() 26 | network.dispose() 27 | 28 | spawn("physics"): 29 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] physics $levelname: ")) 30 | let physics = new(PhysicsSystem) 31 | physics.init() 32 | physics.run() 33 | physics.dispose() 34 | 35 | joinAll() 36 | 37 | run("client"): 38 | spawn("network"): 39 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] client $levelname: ")) 40 | let network = new(ClientNetworkSystem) 41 | network.init() 42 | if not waitAvailable("input") or not waitAvailable("video"): 43 | raise newException(LibraryError, &"Input or Video system unavailable") 44 | network.connect(host="localhost", port=Port(9000)) 45 | network.run() 46 | network.dispose() 47 | 48 | spawn("video"): 49 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] video $levelname: ")) 50 | let video = new(VideoSystem) 51 | video.init(windowX=300, windowY=300, windowWidth=640, windowHeight=640) 52 | video.run() 53 | video.dispose() 54 | 55 | spawn("input"): 56 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] input $levelname: ")) 57 | discard waitAvailable("video") 58 | let input = new(InputSystem) 59 | input.init() 60 | input.run() 61 | input.dispose() 62 | 63 | joinAll() 64 | 65 | dieTogether() 66 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/project.nimble: -------------------------------------------------------------------------------- 1 | 2 | version = "0.1" 3 | author = "Anonymous" 4 | license = "MIT" 5 | 6 | skipDirs = @["build"] 7 | requires "nim >= 1.1.1" 8 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/project.nims: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | include "c4/lib/ogre/ogre.nims" 4 | include "c4/lib/ogre/ogre_sdl.nims" 5 | 6 | const buildDir = thisDir() / "build" 7 | 8 | switch("threads", "on") 9 | switch("multimethods", "on") 10 | switch("nimcache", buildDir / "nimcache") 11 | switch("out", buildDir / projectName()) 12 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/messages 2 | import c4/systems/network/enet 3 | 4 | 5 | type EntityKind* = enum 6 | wall, gate, paddle, ball 7 | 8 | type CreateTypedEntityMessage* = object of CreateEntityMessage 9 | kind*: EntityKind 10 | register CreateTypedEntityMessage 11 | 12 | 13 | type SetDimensionMessage* = object of EntityMessage 14 | ## Tells client size of object 15 | width*: float 16 | height*: float 17 | register SetDimensionMessage 18 | 19 | type SetPositionMessage* = object of EntityMessage 20 | ## Tells client where specific entity should be located 21 | x*: float 22 | y*: float 23 | register SetPositionMessage 24 | 25 | type MovementDirection* = enum 26 | left, right 27 | 28 | type MoveMessage* = object of NetworkMessage 29 | ## Client sends to server when arrow is pressed 30 | direction*: MovementDirection 31 | register MoveMessage 32 | 33 | type StartGameMessage* = object of NetworkMessage 34 | register StartGameMessage 35 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/scenarios/connection.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import net 3 | 4 | import sdl2/sdl as sdllib 5 | 6 | import c4/sugar 7 | import c4/entities 8 | import c4/threads 9 | import c4/systems/network/enet 10 | import c4/systems/physics/simple 11 | import c4/systems/video/sdl 12 | 13 | import ../systems/network 14 | import ../systems/physics 15 | import ../systems/video 16 | import ../messages 17 | 18 | 19 | # New client connection handling 20 | 21 | method processLocal*(self: ref ServerNetworkSystem, message: ref ConnectionOpenedMessage) = 22 | # client just connected - send this info to physics system 23 | message.send("physics") 24 | 25 | 26 | proc getEntityDescribingMessages(self: Entity, kind: EntityKind): seq[ref EntityMessage] = 27 | # helper to send all entity info over network 28 | result.add((ref CreateTypedEntityMessage)( 29 | entity: self, 30 | kind: kind, 31 | )) 32 | result.add((ref SetDimensionMessage)( 33 | entity: self, 34 | width: self[ref Physics].width, 35 | height: self[ref Physics].height, 36 | )) 37 | result.add((ref SetPositionMessage)( 38 | entity: self, 39 | x: self[ref Physics].position.x, 40 | y: self[ref Physics].position.y, 41 | )) 42 | 43 | 44 | method process*(self: ref PhysicsSystem, message: ref ConnectionOpenedMessage) = 45 | # send world info to newly connected client 46 | for msg in self.ball.getEntityDescribingMessages(ball): 47 | msg.peer = message.peer 48 | msg.send("network") 49 | 50 | for entity in self.paddles: 51 | for msg in entity.getEntityDescribingMessages(paddle): 52 | msg.peer = message.peer 53 | msg.send("network") 54 | 55 | for entity in self.gates: 56 | for msg in entity.getEntityDescribingMessages(gate): 57 | msg.peer = message.peer 58 | msg.send("network") 59 | 60 | for entity in self.walls: 61 | for msg in entity.getEntityDescribingMessages(wall): 62 | msg.peer = message.peer 63 | msg.send("network") 64 | 65 | 66 | # then all local messages are by default sent to corresponding peers 67 | 68 | 69 | method processRemote*(self: ref ClientNetworkSystem, message: ref CreateTypedEntityMessage) = 70 | # when entity is created, draw it on screen 71 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) # create entity, generate mapping 72 | let color = case message.kind 73 | of wall: wallColor 74 | of gate: wallColor 75 | of paddle: paddleColor 76 | of ball: ballColor 77 | 78 | message.entity[ref Video] = (ref Video)(color: color) 79 | 80 | 81 | method processRemote*(self: ref ClientNetworkSystem, message: ref SetDimensionMessage) = 82 | try: 83 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) 84 | except KeyError: 85 | return 86 | 87 | let video = message.entity[ref Video] 88 | video.width = message.width 89 | video.height = message.height 90 | 91 | 92 | method processRemote*(self: ref ClientNetworkSystem, message: ref SetPositionMessage) = 93 | try: 94 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) 95 | except KeyError: 96 | return 97 | 98 | let video = message.entity[ref Video] 99 | video.x = message.x 100 | video.y = message.y 101 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/scenarios/movement.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import sequtils 3 | import tables 4 | 5 | import c4/threads 6 | import c4/entities 7 | import c4/systems/physics/simple 8 | import c4/systems/network/enet 9 | 10 | import ../systems/[network, physics] 11 | import ../messages 12 | 13 | 14 | method processRemote*(self: ref ServerNetworkSystem, message: ref MoveMessage) = 15 | message.send("physics") 16 | 17 | 18 | method process*(self: ref PhysicsSystem, message: ref MoveMessage) = 19 | let isRemote = not message.peer.isNil 20 | 21 | for entity in toSeq(getComponents(ref Control).pairs).filterIt( 22 | (if isRemote: it[1] of ref PlayerControl else: it[1] of ref AIControl) 23 | ).mapIt(it[0]): 24 | let physics = entity[ref Physics] 25 | 26 | physics.speed = ( 27 | x: (if message.direction == left: -1 else: 1) * paddleMovementSpeed, 28 | y: 0.0, 29 | ) 30 | physics.movementRemains = movementQuant 31 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/scenarios/start.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import math 3 | import random 4 | 5 | import c4/sugar 6 | import c4/entities 7 | import c4/threads 8 | import c4/systems/physics/simple 9 | 10 | import ../messages 11 | import ../systems/network 12 | import ../systems/physics 13 | 14 | 15 | randomize() 16 | 17 | 18 | method processRemote*(self: ref ServerNetworkSystem, message: ref StartGameMessage) = 19 | message.send("physics") 20 | 21 | 22 | method process*(self: ref PhysicsSystem, message: ref StartGameMessage) = 23 | # start game when movement starts 24 | let ballPhysics = self.ball[ref Physics] 25 | if ballPhysics.speed == (0.0, 0.0): 26 | let angle = Pi / 180.0 * (180.float + rand(55..75).float) # rad 27 | ballPhysics.speed = (cos(angle) * ballSpeed, sin(angle) * ballSpeed) 28 | 29 | 30 | proc resetBall*(self: ref PhysicsSystem) = 31 | self.ball[ref Physics].speed = (x: 0.0, y: 0.0) 32 | self.ball[ref Physics].position = (x: 0.5, y: 0.5) 33 | 34 | 35 | method handleCollision*(self: ref PhysicsSystem, entity1: Entity, entity2: Entity) = 36 | procCall self.as(ref SimplePhysicsSystem).handleCollision(entity1, entity2) 37 | 38 | # check that one entity is a ball 39 | if self.ball notin [entity1, entity2]: 40 | return 41 | 42 | if self.gates[0] in [entity1, entity2]: 43 | # gates #2 win 44 | echo "---------- Player wins ----------" 45 | self.resetBall() 46 | 47 | elif self.gates[1] in [entity1, entity2]: 48 | # gates #1 win 49 | echo "---------- AI wins ----------" 50 | self.resetBall() 51 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2/sdl as sdllib 2 | 3 | import c4/sugar 4 | import c4/threads 5 | import c4/systems/input/sdl 6 | 7 | import ../messages 8 | 9 | 10 | type InputSystem* = object of SdlInputSystem 11 | 12 | 13 | method handle*(self: ref InputSystem, event: Event) = 14 | ## Handling of basic event. These are pretty reasonable defaults. 15 | procCall self.as(ref SdlInputSystem).handle(event) 16 | 17 | if event.kind == KEYDOWN and event.key.keysym.sym == K_SPACE: 18 | new(StartGameMessage).send("network") 19 | 20 | 21 | method handle*(self: ref InputSystem, keyboard: ptr array[NUM_SCANCODES.int, uint8]) = 22 | let 23 | leftPressed = keyboard[SCANCODE_LEFT].bool 24 | rightPressed = keyboard[SCANCODE_RIGHT].bool 25 | 26 | if leftPressed and not rightPressed: 27 | (ref MoveMessage)(direction: left).send("network") 28 | elif rightPressed and not leftPressed: 29 | (ref MoveMessage)(direction: right).send("network") 30 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/network/enet 2 | 3 | 4 | type 5 | ServerNetworkSystem* = object of EnetServerNetworkSystem 6 | ClientNetworkSystem* = object of EnetClientNetworkSystem 7 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | import sequtils 3 | 4 | import c4/sugar 5 | import c4/threads 6 | import c4/systems/physics/simple 7 | import c4/entities 8 | 9 | import ../messages 10 | 11 | 12 | const 13 | movementQuant* = 0.03 14 | paddleMovementSpeed* = 0.5 15 | ballSpeed* = 1.0 16 | 17 | type 18 | PhysicsSystem* = object of SimplePhysicsSystem 19 | ball*: Entity 20 | paddles*: array[2, Entity] 21 | gates*: array[2, Entity] 22 | walls*: seq[Entity] 23 | 24 | Physics* = object of SimplePhysics 25 | movementRemains*: float 26 | 27 | Control* {.inheritable.} = object 28 | PlayerControl* = object of Control 29 | AIControl* = object of Control 30 | 31 | 32 | method getComponents*(self: ref PhysicsSystem): Table[Entity, ref SimplePhysics] = 33 | cast[Table[Entity, ref SimplePhysics]](getComponents(ref Physics)) 34 | 35 | method init*(self: ref PhysicsSystem) = 36 | # ball 37 | self.ball = newEntity() 38 | self.ball[ref Physics] = (ref Physics)(position: (x: 0.5, y: 0.5), width: 0.02, height: 0.02) 39 | 40 | # paddles 41 | self.paddles[0] = newEntity() 42 | self.paddles[0][ref Physics] = (ref Physics)(position: (x: 0.5, y: 0.05), width: 0.25, height: 0.01) 43 | self.paddles[0][ref Control] = new(AIControl) 44 | self.paddles[1] = newEntity() 45 | self.paddles[1][ref Physics] = (ref Physics)(position: (x: 0.5, y: 0.95), width: 0.25, height: 0.01) 46 | self.paddles[1][ref Control] = new(PlayerControl) 47 | 48 | # gates 49 | self.gates[0] = newEntity() 50 | self.gates[0][ref Physics] = (ref Physics)(position: (x: 0.5, y: 0.0), width: 1.0, height: 0.02) 51 | self.gates[1] = newEntity() 52 | self.gates[1][ref Physics] = (ref Physics)(position: (x: 0.5, y: 1.0), width: 1.0, height: 0.02) 53 | 54 | # walls 55 | let leftWall = newEntity() 56 | leftWall[ref Physics] = (ref Physics)(position: (x: 0.0, y: 0.5), width: 0.02, height: 1.0) 57 | self.walls.add(leftWall) 58 | 59 | let rightWall = newEntity() 60 | rightWall[ref Physics] = (ref Physics)(position: (x: 1.0, y: 0.5), width: 0.02, height: 1.0) 61 | self.walls.add(rightWall) 62 | 63 | 64 | method update*(self: ref PhysicsSystem, dt: float) = 65 | procCall self.as(ref SimplePhysicsSystem).update(dt) 66 | 67 | for entity in self.paddles: 68 | let physics = entity[ref Physics] 69 | if physics.movementRemains > 0: 70 | physics.movementRemains -= dt 71 | if physics.movementRemains < 0: 72 | physics.movementRemains = 0 73 | physics.speed = (x: 0.0, y: 0.0) 74 | 75 | for entity, physics in getComponents(ref Physics): 76 | if physics.position != physics.previousPosition: 77 | (ref SetPositionMessage)( 78 | entity: entity, 79 | x: physics.position.x, 80 | y: physics.position.y, 81 | ).send("network") 82 | 83 | # simple AI logic 84 | for entity in toSeq(getComponents(ref Control).pairs).filterIt(it[1] of ref AIControl).mapIt(it[0]): 85 | let 86 | entityX = entity[ref Physics].position.x 87 | ballX = self.ball[ref Physics].position.x 88 | 89 | const delta = 0.02 90 | 91 | if abs(entityX - ballX) > delta: 92 | if entityX < ballX: 93 | (ref MoveMessage)(direction: right).send() 94 | else: 95 | (ref MoveMessage)(direction: left).send() 96 | -------------------------------------------------------------------------------- /c4/examples/ping-pong/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import tables 3 | 4 | import sdl2/sdl as sdllib 5 | 6 | import c4/entities 7 | import c4/systems/video/sdl 8 | 9 | 10 | type 11 | VideoSystem* = object of SdlVideoSystem 12 | 13 | Video* = object of SdlVideo 14 | width*: float 15 | height*: float 16 | color*: Color 17 | 18 | 19 | const 20 | backgroundColor* = Color( 21 | r: uint8(uint8.high.float * 0.1), 22 | g: uint8(uint8.high.float * 0.1), 23 | b: uint8(uint8.high.float * 0.1), 24 | a: uint8.high, 25 | ) 26 | wallColor* = Color( 27 | r: uint8(uint8.high.float * 0.3), 28 | g: uint8(uint8.high.float * 0.3), 29 | b: uint8(uint8.high.float * 0.3), 30 | a: uint8.high, 31 | ) 32 | paddleColor* = Color(r: 0, g: uint8.high, b: 0, a: uint8.high) 33 | ballColor* = Color(r: uint8.high, g: 0, b: 0, a: uint8.high) 34 | 35 | 36 | method render*(self: ref VideoSystem, video: ref Video) = 37 | discard self.renderer.setRenderDrawColor(video.color) 38 | 39 | var windowWidth, windowHeight: cint 40 | self.window.getWindowSize(windowWidth.addr, windowHeight.addr) 41 | 42 | var rect = Rect( 43 | x: int(windowWidth.float * (video.x - video.width / 2)), 44 | y: int(windowHeight.float * (video.y - video.height / 2)), 45 | w: int(windowWidth.float * video.width), 46 | h: int(windowHeight.float * video.height), 47 | ) 48 | discard self.renderer.renderFillRect(rect.addr) 49 | 50 | 51 | method update*(self: ref VideoSystem, dt: float) = 52 | if self.renderer.renderClear() != 0: 53 | raise newException(LibraryError, &"Could not clear renderer: {getError()}") 54 | 55 | for video in getComponents(ref Video).values: 56 | self.render(video) 57 | 58 | discard self.renderer.setRenderDrawColor(backgroundColor) 59 | self.renderer.renderPresent() 60 | -------------------------------------------------------------------------------- /c4/lib/enet/README.md: -------------------------------------------------------------------------------- 1 | # Enet 2 | 3 | This is a wrapper for [Enet library](http://enet.bespin.org/). 4 | 5 | ## Installation 6 | 7 | Install directly from git subdir: 8 | 9 | ```sh 10 | nimble install "https://github.com/c0ntribut0r/cat-400?subdir=c4/lib/enet@#head" 11 | ``` 12 | 13 | You may use msgpack4nim to pack your objects into strings before sending over the network. 14 | -------------------------------------------------------------------------------- /c4/lib/enet/enet.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.1" 3 | author = "c0ntribut0r" 4 | description = "Enet network library wrapper" 5 | license = "MIT" 6 | 7 | # Dirs 8 | skipDirs = @["headers"] 9 | 10 | # Dependencies 11 | requires "nim >= 0.17.3" 12 | -------------------------------------------------------------------------------- /c4/lib/ode/README.md: -------------------------------------------------------------------------------- 1 | # Open Dynamics Engine 2 | 3 | This is a wrapper for [OpenDynamicsEngine](http://ode.org/). 4 | 5 | Please note that this wrapper is very dirty and was not tested at all. 6 | 7 | ## Installation 8 | 9 | Install directly from git subdir: 10 | 11 | ```sh 12 | nimble install "https://github.com/c0ntribut0r/cat-400?subdir=c4/lib/ode@#head" 13 | ``` 14 | 15 | Please ensure you set the same precision as ODE compiled library you're going to use (`.so` or `.dll`). To set precision, define either `dIDEDOUBLE` (the default) or `dIDESINGLE` for nim compiler. 16 | 17 | ## License 18 | 19 | MIT. Do whatever you want. 20 | -------------------------------------------------------------------------------- /c4/lib/ogre/README.md: -------------------------------------------------------------------------------- 1 | # Ogre3D Nim wrapper 2 | 3 | This is a wrapper for [Ogre 3D](https://ogre3d.org/). 4 | 5 | Please note that this wrapper is very dirty and is barely tested. 6 | 7 | ## Installation 8 | 9 | Install directly from git subdir: 10 | 11 | ```sh 12 | nimble install "https://github.com/c0ntribut0r/cat-400?subdir=c4/lib/ogre@#head" 13 | ``` 14 | 15 | ## License 16 | 17 | MIT. Do whatever you want. 18 | -------------------------------------------------------------------------------- /c4/lib/ogre/ogre.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.1" 2 | author = "c0ntribut0r" 3 | description = "Ogre3D bindings for Nim" 4 | license = "MIT" 5 | 6 | skipDirs = @["headers"] 7 | requires "nim >= 1.16" 8 | -------------------------------------------------------------------------------- /c4/logging.nim: -------------------------------------------------------------------------------- 1 | import chronicles 2 | import chronicles/options 3 | export chronicles 4 | 5 | import parseopt 6 | 7 | 8 | template withLog*(severity: LogLevel, eventName: static[string], code: untyped) = 9 | log(instantiationInfo(), severity, eventName) 10 | code 11 | log(instantiationInfo(), severity, eventName & " - done") 12 | 13 | 14 | proc getProcessName(): string = 15 | for kind, key, value in parseopt.getopt(): 16 | if kind == parseopt.cmdLongOption and key == "process": 17 | return value 18 | 19 | "main" 20 | 21 | 22 | publicLogScope: 23 | process = getProcessName() 24 | -------------------------------------------------------------------------------- /c4/loop.nim: -------------------------------------------------------------------------------- 1 | import times 2 | import os 3 | import strutils 4 | 5 | import ./logging 6 | 7 | 8 | type BreakLoopException* = object of CatchableError 9 | 10 | 11 | template loop*(frequency: int, code: untyped) = 12 | let skipSeconds = 1 / frequency 13 | 14 | var 15 | now = epochTime() 16 | lastUpdateTime = now 17 | dt {.inject.}: type(now) 18 | sleepTime: type(now) 19 | 20 | while true: 21 | trace "loop tick" 22 | now = epochTime() 23 | dt = now - lastUpdateTime 24 | lastUpdateTime = now 25 | 26 | try: 27 | code 28 | except BreakLoopException: 29 | break 30 | 31 | now = epochTime() 32 | 33 | if frequency != 0: 34 | sleepTime = lastUpdateTime + skipSeconds - now 35 | if sleepTime > 0: 36 | trace "loop sleep", sleepTime 37 | sleep(int(sleepTime * 1000)) 38 | else: 39 | logging.warn "loop lag", timeTakenPerStep=formatFloat(now - lastUpdateTime, precision=3), desiredFrequency=frequency, maxAllowedTimePerStep=formatFloat(1/frequency, precision=3) 40 | 41 | 42 | when isMainModule: 43 | import unittest 44 | 45 | suite "Loop": 46 | test "Base loop frequency": 47 | var i = 0 48 | loop(30) do: 49 | echo $i & " " & $dt 50 | if i == 0: 51 | assert dt < 0.01 52 | else: 53 | assert dt < 0.035 and dt > 0.025 54 | inc i 55 | if i > 30: 56 | break 57 | 58 | test "Warnings": 59 | var i = 0 60 | loop(30) do: 61 | echo $i 62 | sleep(500) 63 | inc i 64 | if i > 3: 65 | break 66 | -------------------------------------------------------------------------------- /c4/messages.nim: -------------------------------------------------------------------------------- 1 | ## Message is a base unit for communication between systems. 2 | 3 | import hashes 4 | import tables 5 | export tables 6 | import macros 7 | import locks 8 | import typetraits 9 | import msgpack4nim 10 | export msgpack4nim # every module using messages packing must import msgpack4nim 11 | 12 | 13 | type 14 | Message* {.inheritable.} = object 15 | ## Message is an object with minimal required information to describe some event or command. 16 | ## Call `messages.register` on message subtype so that msgpack4nim knows how to (de)serialize it. 17 | ## Example: 18 | ## type CustomMessage = object of Message 19 | ## messages.register(CustomMessage) 20 | 21 | PackProc = proc(message: ref Message): string {.closure.} 22 | UnpackProc = proc(stream: MsgStream): ref Message {.closure.} 23 | 24 | var packTable = initTable[ 25 | uint8, 26 | tuple[ 27 | pack: PackProc, 28 | unpack: UnpackProc, 29 | ], 30 | ]() 31 | let packTablePtr = packTable.addr 32 | var packTableLock: Lock 33 | initLock(packTableLock) 34 | 35 | # -- Message -- 36 | method `$`*(self: ref Message): string {.base.} = "Message" 37 | 38 | method packId*(self: ref Message): uint8 {.base.} = 39 | raise newException(LibraryError, "Trying to pack/unpack base Message type, probably forgot to call `register()` on custom message type") 40 | 41 | proc msgpack*(message: ref Message): string {.gcsafe.} = 42 | ## General method which selects appropriate pack method from pack table according to real message runtime type. 43 | # var packProc: PackProc 44 | # withLock packTableLock: 45 | # packProc = packTablePtr[][message.packId].pack 46 | # result = packProc(message) 47 | 48 | {.gcsafe.}: # im so sorry for this 49 | withLock packTableLock: 50 | try: 51 | result = packTablePtr[][message.packId].pack(message) 52 | except KeyError: 53 | raise newException(LibraryError, "Unknown message type id: " & $message.packId) 54 | 55 | proc msgunpack*(data: string): ref Message {.gcsafe.} = 56 | ## General method which selects appropriate unpack method from pack table according to real message runtime type. 57 | var 58 | packId: uint8 59 | stream = MsgStream.init(data) 60 | 61 | # stream.setPosition(0) # TODO: why was it needed? 62 | stream.unpack(packId) 63 | {.gcsafe.}: 64 | withLock packTableLock: 65 | try: 66 | result = packTablePtr[][packId].unpack(stream) 67 | except KeyError: 68 | raise newException(LibraryError, "Unknown message type id: " & $packId) 69 | 70 | template register*(MessageType: typedesc) = 71 | ## Template for registering pack/unpack procs for specific message type. 72 | ## Without registering, packing/unpacking won't store runtime type information. 73 | var messageId: uint8 74 | 75 | withLock packTableLock: 76 | messageId = uint8(packTable.len) + 1 77 | 78 | packTablePtr[][messageId] = ( 79 | # pack proc 80 | proc(message: ref Message): string {.closure.} = 81 | let packId = messageId 82 | # var stream = MsgStream.init(sizeof(packId) + sizeof(MessageType)) 83 | var stream = MsgStream.init() 84 | 85 | stream.pack packId 86 | stream.pack (ref MessageType) message 87 | 88 | result = stream.data, 89 | 90 | # unpack proc 91 | proc(stream: MsgStream): ref Message {.closure.} = 92 | var temp: ref MessageType 93 | stream.unpack(temp) 94 | result = temp 95 | ) 96 | 97 | method packId*(self: ref MessageType): uint8 = messageId 98 | 99 | method `$`*(self: ref MessageType): string = 100 | result = self[].type.name 101 | for name, value in self[].fieldPairs: 102 | result.add(" " & name & "=[" & $value & "]") 103 | 104 | proc msgpack*(self: ref MessageType): string = ((ref Message)self).msgpack() # required for instant pack 105 | 106 | 107 | when isMainModule: 108 | import unittest 109 | 110 | type 111 | MessageA = object of Message 112 | msg: string 113 | MessageB = object of Message 114 | counter: int8 115 | data: string 116 | is_correct: bool 117 | 118 | method getData(x: ref Message): string {.base.} = "" 119 | method getData(x: ref MessageA): string = x.msg 120 | method getData(x: ref MessageB): string = $x.counter 121 | 122 | register(MessageA) 123 | register(MessageB) 124 | 125 | suite "Messages test": 126 | var 127 | packed: string 128 | unpacked: ref Message 129 | 130 | test "Pack/unpack base Message type": 131 | expect LibraryError: 132 | packed = new(Message).msgpack() 133 | 134 | test "Pack/unpack Message subtypes": 135 | var message: ref Message 136 | 137 | message = (ref MessageA)(msg: "some message") 138 | packed = message.msgpack() 139 | echo "MessageA packed as: " & stringify(packed) 140 | unpacked = packed.msgunpack() 141 | 142 | check: 143 | packed.len == 15 144 | unpacked.getData() == "some message" 145 | 146 | message = (ref MessageB)(counter: 42, data: "some data string", is_correct: true) 147 | packed = message.msgpack() 148 | echo "MessageB packed as: " & stringify(packed) 149 | unpacked = packed.msgunpack() 150 | check: 151 | packed.len == 21 152 | unpacked.getData() == "42" 153 | 154 | test "Instant pack": 155 | let packedInstant = (ref MessageB)(counter: 42).msgpack() 156 | echo "Instant packed as: " & stringify(packedInstant) 157 | 158 | var msg: ref Message = (ref MessageB)(counter: 42) 159 | let packedVariable = msg.msgpack() 160 | echo "Var packed as: " & stringify(packedVariable) 161 | 162 | check: 163 | packedInstant == packedVariable 164 | 165 | test "Inside a thread": 166 | var thread: Thread[void] 167 | 168 | thread.createThread(tp = proc() {.thread.} = 169 | let packed = (ref MessageB)(counter: 42).msgpack() 170 | discard packed.msgunpack() 171 | ) 172 | -------------------------------------------------------------------------------- /c4/processes.nim: -------------------------------------------------------------------------------- 1 | import os 2 | export os 3 | import osproc 4 | import options 5 | export options 6 | import tables 7 | export tables 8 | import parseopt 9 | import sequtils 10 | 11 | import ./logging 12 | 13 | type 14 | ProcessName* = string ## each process must have a unique name; process will be accessible by this name 15 | 16 | 17 | var processes = initTable[ProcessName, Process]() 18 | const mainProcessName* = "master" 19 | 20 | 21 | proc getProcessName(): string = 22 | for kind, key, value in parseopt.getopt(): 23 | if kind == parseopt.cmdLongOption and key == "process": 24 | return value 25 | 26 | mainProcessName 27 | 28 | 29 | let processName*: ProcessName = getProcessName() 30 | 31 | 32 | template spawnProcess*(name: ProcessName, code: untyped) = 33 | ## Runs new process which executes all instructions before this call, plus `code` content. 34 | 35 | debug "process started" 36 | 37 | if processName == mainProcessName: 38 | if processes.hasKey(name): 39 | raise newException(KeyError, "Process with name '" & name & "' already exists") 40 | 41 | debug "spawning child process", childProcess=name 42 | processes[name] = startProcess( 43 | command=getAppFilename(), 44 | args=commandLineParams() & " --process=" & name, 45 | options={poParentStreams}, 46 | ) 47 | 48 | elif processName == name: 49 | try: 50 | code 51 | debug "process finishing" 52 | system.quit() 53 | except Exception as exc: 54 | fatal "process failed", exceptionMessage=exc.msg, stackTrace=exc.getStackTrace() 55 | raise 56 | 57 | proc joinProcesses*(checkInterval: int = 1000) = 58 | ## Monitors existing processes. If one process is not running anymore, terminates all other processes as well. 59 | assert processName == mainProcessName 60 | 61 | while true: 62 | let running = toSeq(processes.values()).filterIt(it.running) 63 | if running.len == 0: 64 | break 65 | 66 | sleep checkInterval 67 | 68 | 69 | proc sync*(checkInterval: int = 1000) = 70 | assert processName == mainProcessName 71 | while toSeq(processes.values()).anyIt(it.running): 72 | sleep checkInterval 73 | 74 | 75 | when isMainModule: 76 | import unittest 77 | 78 | suite "processes": 79 | spawnProcess "process1": 80 | for _ in 0..10: 81 | echo processName 82 | sleep 100 83 | 84 | spawnProcess "process2": 85 | for _ in 0..10: 86 | echo processName 87 | sleep 100 88 | 89 | sync() 90 | 91 | test "run": 92 | assert true 93 | -------------------------------------------------------------------------------- /c4/sugar.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | 4 | template `as`*(obj: typed, T: typedesc): untyped = 5 | T(obj) 6 | 7 | 8 | macro operateOn*(x: typed; calls: untyped) = 9 | # TODO: remove when `operateOn` is merged to master 10 | result = copyNimNode(calls) 11 | expectKind calls, {nnkStmtList, nnkStmtListExpr} 12 | # non-recursive processing because that's exactly what we need here: 13 | for y in calls: 14 | expectKind y, nnkCallKinds 15 | var call = newTree(y.kind) 16 | call.add y[0] 17 | call.add x 18 | for j in 1.. 10: 56 | new(StopMessage).send(video) 57 | raise newException(BreakLoopException, "") 58 | 59 | method update(self: ref VideoSystem, dt: float) = 60 | echo "updating video system" 61 | inc self.i 62 | 63 | method process(self: ref VideoSystem, message: ref StopMessage) = 64 | raise newException(BreakLoopException, "") 65 | 66 | suite "systems": 67 | 68 | test "two systems in threads": 69 | 70 | spawnThread input: 71 | var inputSystem = new(InputSystem) 72 | inputSystem.run(frequency=20) 73 | 74 | spawnThread video: 75 | var videoSystem = new(VideoSystem) 76 | videoSystem.run(frequency=100) 77 | 78 | joinActiveThreads() 79 | assert true 80 | -------------------------------------------------------------------------------- /c4/systems/input/sdl.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | import tables 3 | 4 | import ../../logging 5 | import ../../messages 6 | import ../../systems 7 | import ../../loop 8 | 9 | 10 | type 11 | InputSystem* = object of System 12 | event: Event # temporary storage for event when calling pollEvent() 13 | 14 | InputSystemError* = object of LibraryError 15 | 16 | InputInitMessage* = object of Message 17 | 18 | WindowQuitMessage* = object of Message 19 | 20 | 21 | template handleError*(message: string) = 22 | let error = getError() 23 | fatal message, error 24 | raise newException(InputSystemError, message & ": " & $error) 25 | 26 | 27 | InputInitMessage.register() 28 | WindowQuitMessage.register() 29 | 30 | 31 | method process*(self: ref InputSystem, message: ref InputInitMessage) = 32 | withLog(DEBUG, "initializing input"): 33 | if initSubSystem(INIT_EVENTS) != 0: handleError("failed to initialize events") 34 | 35 | method dispose*(self: ref InputSystem) = 36 | quitSubSystem(INIT_EVENTS) 37 | 38 | 39 | # proc `$`*(event: Event): string = $event.kind 40 | 41 | 42 | 43 | # ---- workflow methods ---- 44 | # method init*(self: ref InputSystem) = 45 | # logging.debug &"Initializing {self[].type.name}" 46 | 47 | # try: 48 | # sleep 500 # wait for SDL VIDEO system to initialize (in case of race condition) 49 | # if wasInit(INIT_VIDEO) == 0: 50 | # # INIT_VIDEO implies INIT_EVENTS -> don't initialize events if video already initialized 51 | # logging.debug "Initializing SDL events" 52 | # if initSubSystem(INIT_EVENTS) != 0: 53 | # raise newException(LibraryError, &"Could not init {self.type.name}: {getError()}") 54 | 55 | # except LibraryError: 56 | # quitSubSystem(INIT_EVENTS) 57 | # logging.fatal(getCurrentExceptionMsg()) 58 | # raise 59 | 60 | method handleEvent*(self: ref InputSystem, event: Event) {.base.} = 61 | case event.kind 62 | of QuitEvent: 63 | raise newException(BreakLoopException, "") 64 | of WINDOWEVENT: 65 | case event.window.event 66 | of WINDOWEVENT_SIZE_CHANGED: 67 | # width: event.window.data1, 68 | # height: event.window.data2, 69 | discard 70 | else: 71 | discard 72 | # of KEYDOWN: 73 | # case event.key.keysym.sym 74 | # of K_c: 75 | # ... 76 | else: 77 | discard 78 | 79 | method handleKeyboardState*( 80 | self: ref InputSystem, 81 | keyboard: ptr array[0 .. SDL_NUM_SCANCODES.int, uint8], 82 | ) {.base.} = 83 | discard 84 | 85 | method update*(self: ref InputSystem, dt: float) = 86 | while pollEvent(self.event) != False32: 87 | self.handleEvent(self.event) 88 | 89 | self.handleKeyboardState(getKeyboardState(nil)) 90 | 91 | 92 | when isMainModule: 93 | import unittest 94 | import ../../threads 95 | 96 | suite "System tests": 97 | test "Running inside thread": 98 | spawnThread ThreadID(1): 99 | let system = new(InputSystem) 100 | system.process(new InputInitMessage) 101 | system.run(frequency=30) 102 | 103 | joinActiveThreads() 104 | -------------------------------------------------------------------------------- /c4/systems/network/net.nim: -------------------------------------------------------------------------------- 1 | import std/tables 2 | import times 3 | 4 | import netty 5 | 6 | import ../../systems 7 | import ../../entities 8 | import ../../messages 9 | import ../../threads 10 | import ../../logging 11 | import ../../sugar 12 | 13 | const 14 | default_port: uint16 = 8765 15 | pingInterval: float64 = 2.0 16 | 17 | type 18 | NetworkSystem* = object of System 19 | reactor: netty.Reactor 20 | 21 | ServerNetworkSystem* = object of NetworkSystem 22 | 23 | ClientNetworkSystem* = object of NetworkSystem 24 | lastPingAttemptTime: float64 = 0.0 25 | 26 | ServerInitMessage* = object of messages.Message 27 | host*: string = "127.0.0.1" 28 | port*: uint16 = defaultPort 29 | 30 | ClientInitMessage* = object of messages.Message 31 | 32 | ConnectMessage* = object of messages.Message 33 | host*: string = "127.0.0.1" 34 | port*: uint16 = defaultPort 35 | 36 | DisconnectMessage* = object of messages.Message 37 | 38 | NetworkMessage* = object of messages.Message 39 | connection*: Connection = nil 40 | 41 | HelloMessage* = object of NetworkMessage 42 | PingMessage* = object of NetworkMessage 43 | 44 | 45 | proc `$`*(self: Connection): string = 46 | if self.isNil: "nil" else: $self.id 47 | 48 | register ConnectMessage 49 | register DisconnectMessage 50 | register NetworkMessage 51 | register HelloMessage 52 | register PingMessage 53 | 54 | 55 | method receive*(self: ref NetworkSystem, message: ref NetworkMessage) {.base, gcsafe.} = 56 | warn "dropping unhandled network message", message 57 | 58 | method receive*(self: ref NetworkSystem, message: ref PingMessage) = 59 | discard 60 | 61 | method update*(self: ref NetworkSystem, delta: float) {.gcsafe.} = 62 | var message: ref messages.Message 63 | 64 | self.reactor.tick() 65 | 66 | for connection in self.reactor.newConnections: 67 | debug "new connection detected", connection 68 | 69 | for connection in self.reactor.deadConnections: 70 | debug "connection closed", connection 71 | 72 | for rawMessage in self.reactor.messages: 73 | try: 74 | message = rawMessage.data.msgunpack() 75 | except Exception as exc: 76 | error "message unpacking failed", rawMessage, exc=exc.msg 77 | continue 78 | 79 | if not (message of (ref NetworkMessage)): 80 | warn "discarding message not of NetworkMessage type", message 81 | continue 82 | 83 | message.as(ref NetworkMessage).connection = rawMessage.conn 84 | debug "received message", message 85 | self.receive(message.as(ref NetworkMessage)) 86 | 87 | method send*(self: ref NetworkSystem, message: ref NetworkMessage) {.base, gcsafe.} = 88 | debug "sending message", message 89 | 90 | let destination = message.connection 91 | 92 | message.connection = nil # no need to pack this field 93 | let payload = message.msgpack() 94 | 95 | if destination.isNil: 96 | for connection in self.reactor.connections: 97 | self.reactor.send(connection, payload) 98 | else: 99 | self.reactor.send(destination, payload) 100 | 101 | method process*(self: ref NetworkSystem, message: ref NetworkMessage) {.gcsafe.} = 102 | self.send(message) 103 | 104 | 105 | # -------------------------------- server -------------------------------- 106 | 107 | method process*(self: ref ServerNetworkSystem, message: ref ServerInitMessage) = 108 | self.reactor = netty.newReactor(message.host, message.port.int) 109 | debug "server network initialized", host=message.host, port=message.port 110 | 111 | 112 | # -------------------------------- client -------------------------------- 113 | 114 | method update*(self: ref ClientNetworkSystem, dt: float) = 115 | procCall self.as(ref NetworkSystem).update(dt) 116 | 117 | for connection in self.reactor.connections: 118 | if epochTime() - max(connection.lastActiveTime, self.lastPingAttemptTime) > pingInterval: 119 | self.lastPingAttemptTime = epochTime() 120 | self.send((ref PingMessage)(connection: connection)) 121 | 122 | 123 | method process*(self: ref ClientNetworkSystem, message: ref ClientInitMessage) = 124 | self.reactor = netty.newReactor() 125 | debug "client network initialized" 126 | 127 | method process*(self: ref ClientNetworkSystem, message: ref ConnectMessage) = 128 | let connection = self.reactor.connect(message.host, message.port.int) 129 | info "connection established", host=message.host, port=message.port 130 | self.send((ref HelloMessage)(connection: connection)) 131 | 132 | method process*(self: ref ClientNetworkSystem, message: ref DisconnectMessage) = 133 | if self.reactor.connections.len == 0: 134 | warn "received disconnect message while not connected" 135 | return 136 | 137 | for connection in self.reactor.connections: 138 | self.reactor.disconnect(connection) 139 | info "disconnected" 140 | -------------------------------------------------------------------------------- /c4/systems/physics/ode.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import system 3 | 4 | import ../../logging 5 | import ../../systems 6 | import ../../messages 7 | import ../../lib/ode/ode 8 | 9 | 10 | const simulationStep = 1 / 30 11 | 12 | type 13 | PhysicsSystem* = object of System 14 | world*: dWorldID 15 | space*: dSpaceID 16 | nearCallback*: dNearCallback 17 | 18 | contactGroup: dJointGroupID 19 | simulationStepRemains: float 20 | 21 | Physics* = object of RootObj 22 | body*: dBodyID 23 | 24 | PhysicsInitMessage* = object of Message 25 | 26 | 27 | proc nearCallback(data: pointer, geom1: dGeomID, geom2: dGeomID) = 28 | let 29 | self = cast[ptr PhysicsSystem](data)[] 30 | body1 = geom1.geomGetBody() 31 | body2 = geom2.geomGetBody() 32 | 33 | const maxContacts = 4 34 | var contact {.global.}: array[maxContacts, dContact] 35 | for i in 0.. other.bottomRight.x or other.topLeft.x > self.bottomRight.x: 32 | return false 33 | 34 | if self.topLeft.y < other.bottomRight.y or other.topLeft.y < self.bottomRight.y: 35 | return false 36 | 37 | true 38 | 39 | proc `+`*(v1: Vector, v2: Vector): Vector = 40 | result.x = v1.x + v2.x 41 | result.y = v1.y + v2.y 42 | 43 | proc `*`*(v: Vector, mul: float): Vector = 44 | result.x = v.x * mul 45 | result.y = v.y * mul 46 | 47 | 48 | method handleCollision*(self: ref PhysicsSystem, physics1: ref Physics, physics2: ref Physics) {.base, gcsafe.} = 49 | debug "collision happened", position1=physics1.position, position2=physics2.position 50 | 51 | let horizontalDist = min( 52 | abs(physics1[].bottomRight.y - physics2[].topLeft.y), 53 | abs(physics1[].topLeft.y - physics2[].bottomRight.y) 54 | ) 55 | let verticalDist = min( 56 | abs(physics1[].bottomRight.x - physics2[].topLeft.x), 57 | abs(physics1[].topLeft.x - physics2[].bottomRight.x) 58 | ) 59 | 60 | if horizontalDist < verticalDist: 61 | # objects are collided using their horizontal edges 62 | physics1.velocity = (physics1.velocity.x, -physics1.velocity.y) 63 | physics2.velocity = (physics2.velocity.x, -physics2.velocity.y) 64 | 65 | else: 66 | # objects are collided using their vertical edges 67 | physics1.velocity = (-physics1.velocity.x, physics1.velocity.y) 68 | physics2.velocity = (-physics2.velocity.x, physics2.velocity.y) 69 | 70 | physics1.position = physics1.previousPosition 71 | physics2.position = physics2.previousPosition 72 | 73 | 74 | method update*(self: ref Physics, dt: float) {.base, gcsafe.} = 75 | # calculate new position for every Physics instance 76 | self.previousPosition = self.position 77 | self.position = self.position + self.velocity * dt 78 | 79 | method update*(self: ref PhysicsSystem, dt: float) {.gcsafe.} = 80 | let components = getComponents(ref Physics) 81 | 82 | for entity, physics in components: 83 | physics.update(dt) 84 | 85 | let 86 | entities = toSeq(components.keys) 87 | length = entities.len 88 | 89 | for i in 0..= 1.1.1" 8 | -------------------------------------------------------------------------------- /c4/templates/2d/project.nims: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | include "c4/lib/ogre/ogre.nims" 4 | include "c4/lib/ogre/ogre_sdl.nims" 5 | 6 | const buildDir = thisDir() / "build" 7 | 8 | switch("threads", "on") 9 | switch("multimethods", "on") 10 | switch("nimcache", buildDir / "nimcache") 11 | switch("out", buildDir / projectName()) 12 | -------------------------------------------------------------------------------- /c4/templates/2d/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/entities 2 | import c4/messages 3 | import c4/systems/network/enet 4 | import c4/systems/physics/simple 5 | 6 | 7 | type EntityKind* = enum 8 | wall, player, enemy 9 | 10 | type CreateTypedEntityMessage* = object of CreateEntityMessage 11 | kind*: EntityKind 12 | register CreateTypedEntityMessage 13 | 14 | 15 | type SetDimensionMessage* = object of EntityMessage 16 | ## Tells client size of object 17 | width*: float 18 | height*: float 19 | register SetDimensionMessage 20 | 21 | type SetPositionMessage* = object of EntityMessage 22 | ## Tells client where specific entity should be located 23 | x*: float 24 | y*: float 25 | register SetPositionMessage 26 | 27 | type MoveMessage* = object of NetworkMessage 28 | ## Client sends to server when arrow is pressed 29 | entity*: Entity 30 | direction*: float # just angle in rad 31 | register MoveMessage 32 | 33 | type StartGameMessage* = object of NetworkMessage 34 | register StartGameMessage 35 | -------------------------------------------------------------------------------- /c4/templates/2d/src/scenarios/collision.nim: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | import c4/sugar 5 | import c4/entities 6 | import c4/systems/physics/simple 7 | 8 | import ../systems/physics 9 | 10 | 11 | randomize() 12 | 13 | 14 | method handleCollision*(self: ref PhysicsSystem, entity1: Entity, entity2: Entity) = 15 | if self.player in [entity1, entity2] and (entity1 in self.enemies or entity2 in self.enemies): 16 | logging.info "---- Enemies caught player! ----" 17 | self.player[ref Physics].position = (x: 0.5, y: 0.5) 18 | for enemy in self.enemies: 19 | enemy[ref Physics].position = (x: rand(100).float / 100.0, y: rand(100).float / 100.0) 20 | 21 | elif entity1 in self.enemies and entity2 in self.enemies: 22 | discard 23 | 24 | else: 25 | procCall self.as(ref SimplePhysicsSystem).handleCollision(entity1, entity2) 26 | -------------------------------------------------------------------------------- /c4/templates/2d/src/scenarios/connection.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import logging 3 | import net 4 | import strformat 5 | 6 | import sdl2/sdl as sdllib 7 | 8 | import c4/sugar 9 | import c4/entities 10 | import c4/threads 11 | import c4/systems/network/enet 12 | import c4/systems/physics/simple 13 | import c4/systems/video/sdl 14 | 15 | import ../systems/network 16 | import ../systems/physics 17 | import ../systems/video 18 | import ../messages 19 | 20 | 21 | # New client connection handling 22 | 23 | method processLocal*(self: ref ServerNetworkSystem, message: ref ConnectionOpenedMessage) = 24 | # client just connected - send this info to physics system 25 | message.send("physics") 26 | 27 | 28 | proc getEntityDescribingMessages(self: Entity, kind: EntityKind): seq[ref EntityMessage] = 29 | # helper to send all entity info over network 30 | result.add((ref CreateTypedEntityMessage)( 31 | entity: self, 32 | kind: kind, 33 | )) 34 | result.add((ref SetDimensionMessage)( 35 | entity: self, 36 | width: self[ref Physics].width, 37 | height: self[ref Physics].height, 38 | )) 39 | result.add((ref SetPositionMessage)( 40 | entity: self, 41 | x: self[ref Physics].position.x, 42 | y: self[ref Physics].position.y, 43 | )) 44 | 45 | 46 | method process*(self: ref PhysicsSystem, message: ref ConnectionOpenedMessage) = 47 | # send world info to newly connected client 48 | for msg in self.player.getEntityDescribingMessages(player): 49 | msg.peer = message.peer 50 | msg.send("network") 51 | 52 | for entity in self.enemies: 53 | for msg in entity.getEntityDescribingMessages(enemy): 54 | msg.peer = message.peer 55 | msg.send("network") 56 | 57 | for entity in self.walls: 58 | for msg in entity.getEntityDescribingMessages(wall): 59 | msg.peer = message.peer 60 | msg.send("network") 61 | 62 | 63 | # then all local messages are by default sent to corresponding peers 64 | 65 | 66 | method processRemote*(self: ref ClientNetworkSystem, message: ref CreateTypedEntityMessage) = 67 | # when entity is created, draw it on screen 68 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) # create entity, generate mapping 69 | let color = case message.kind 70 | of wall: wallColor 71 | of player: playerColor 72 | of enemy: enemyColor 73 | 74 | message.entity[ref Video] = (ref Video)(color: color) 75 | 76 | 77 | method processRemote*(self: ref ClientNetworkSystem, message: ref SetDimensionMessage) = 78 | try: 79 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) 80 | except KeyError: 81 | return 82 | 83 | let video = message.entity[ref Video] 84 | video.width = message.width 85 | video.height = message.height 86 | 87 | 88 | method processRemote*(self: ref ClientNetworkSystem, message: ref SetPositionMessage) = 89 | try: 90 | procCall self.as(ref EnetClientNetworkSystem).processRemote(message) 91 | except KeyError: 92 | return 93 | 94 | let video = message.entity[ref Video] 95 | video.x = message.x 96 | video.y = message.y 97 | -------------------------------------------------------------------------------- /c4/templates/2d/src/scenarios/movement.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import sequtils 3 | import tables 4 | import math 5 | 6 | import c4/threads 7 | import c4/entities 8 | import c4/systems/physics/simple 9 | import c4/systems/network/enet 10 | 11 | import ../systems/[network, physics] 12 | import ../messages 13 | 14 | 15 | method processRemote*(self: ref ServerNetworkSystem, message: ref MoveMessage) = 16 | message.entity = 0 # dismiss entity when message is non-local 17 | message.send("physics") 18 | 19 | 20 | method process*(self: ref PhysicsSystem, message: ref MoveMessage) = 21 | if not message.entity.isInitialized: 22 | message.entity = self.player 23 | 24 | let physics = message.entity[ref Physics] 25 | 26 | physics.speed = ( 27 | x: cos(message.direction) * movementSpeed, 28 | y: sin(message.direction) * movementSpeed, 29 | ) 30 | physics.movementRemains = movementQuant 31 | -------------------------------------------------------------------------------- /c4/templates/2d/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2/sdl as sdllib 2 | import math 3 | 4 | import c4/sugar 5 | import c4/threads 6 | import c4/systems/input/sdl 7 | 8 | import ../messages 9 | 10 | 11 | type InputSystem* = object of SdlInputSystem 12 | 13 | 14 | method handle*(self: ref InputSystem, event: Event) = 15 | ## Handling of basic event. These are pretty reasonable defaults. 16 | procCall self.as(ref SdlInputSystem).handle(event) 17 | 18 | if event.kind == KEYDOWN and event.key.keysym.sym == K_SPACE: 19 | new(StartGameMessage).send("network") 20 | 21 | 22 | method handle*(self: ref InputSystem, keyboard: ptr array[NUM_SCANCODES.int, uint8]) = 23 | var vector = (x: 0.0, y: 0.0) 24 | 25 | if keyboard[SCANCODE_LEFT].bool: 26 | vector = (x: vector.x - 1.0, y: vector.y + 0.0) 27 | 28 | if keyboard[SCANCODE_RIGHT].bool: 29 | vector = (x: vector. x + 1.0, y: vector.y + 0.0) 30 | 31 | if keyboard[SCANCODE_UP].bool: 32 | vector = (x: vector.x + 0.0, y: vector.y + 1.0) 33 | 34 | if keyboard[SCANCODE_DOWN].bool: 35 | vector = (x: vector.x + 0.0, y: vector.y - 1.0) 36 | 37 | if vector == (x: 0.0, y: 0.0): 38 | return 39 | 40 | let angle = arctan2(vector.y, vector.x) 41 | (ref MoveMessage)(direction: angle).send("network") 42 | -------------------------------------------------------------------------------- /c4/templates/2d/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/network/enet 2 | 3 | import ../messages 4 | 5 | 6 | type 7 | ServerNetworkSystem* = object of EnetServerNetworkSystem 8 | ClientNetworkSystem* = object of EnetClientNetworkSystem 9 | -------------------------------------------------------------------------------- /c4/templates/2d/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | import strformat 3 | import math 4 | import sequtils 5 | 6 | import c4/sugar 7 | import c4/threads 8 | import c4/systems/physics/simple 9 | import c4/entities 10 | 11 | import ../messages 12 | 13 | 14 | const 15 | movementQuant* = 1/30 # synced with physics FPS 16 | movementSpeed* = 0.5 17 | numEnemies = 4 18 | 19 | type 20 | PhysicsSystem* = object of SimplePhysicsSystem 21 | player*: Entity 22 | enemies*: seq[Entity] 23 | walls*: seq[Entity] 24 | 25 | Physics* = object of SimplePhysics 26 | movementRemains*: float 27 | 28 | Control* {.inheritable.} = object 29 | PlayerControl* = object of Control 30 | AIControl* = object of Control 31 | 32 | 33 | method getComponents*(self: ref PhysicsSystem): Table[Entity, ref SimplePhysics] = 34 | cast[Table[Entity, ref SimplePhysics]](getComponents(ref Physics)) 35 | 36 | method init*(self: ref PhysicsSystem) = 37 | # player 38 | self.player = newEntity() 39 | self.player[ref Physics] = (ref Physics)(position: (x: 0.5, y: 0.9), width: 0.02, height: 0.02) 40 | self.player[ref Control] = new(PlayerControl) 41 | 42 | # enemies 43 | 44 | for i in 0.. 0: 78 | physics.movementRemains -= dt 79 | if physics.movementRemains < 0: 80 | physics.movementRemains = 0 81 | physics.speed = (x: 0.0, y: 0.0) 82 | 83 | for entity, physics in getComponents(ref Physics): 84 | if physics.position != physics.previousPosition: 85 | (ref SetPositionMessage)( 86 | entity: entity, 87 | x: physics.position.x, 88 | y: physics.position.y, 89 | ).send("network") 90 | 91 | # simple AI logic 92 | for entity in toSeq(getComponents(ref Control).pairs).filterIt(it[1] of ref AIControl).mapIt(it[0]): 93 | let 94 | entityPhysics = entity[ref Physics] 95 | playerPhysics = self.player[ref Physics] 96 | 97 | if playerPhysics.speed == (x: 0.0, y: 0.0): 98 | return 99 | 100 | let 101 | delta = 0.01 102 | xPlayerDelta = playerPhysics.position.x - entityPhysics.position.x 103 | yPlayerDelta = playerPhysics.position.y - entityPhysics.position.y 104 | 105 | if abs(xPlayerDelta) < delta and abs(yPlayerDelta) < delta: 106 | continue 107 | 108 | let angle = arctan2(yPlayerDelta, xPlayerDelta) 109 | (ref MoveMessage)(entity: entity, direction: angle).send() 110 | -------------------------------------------------------------------------------- /c4/templates/2d/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import sequtils 3 | import tables 4 | import logging 5 | 6 | import sdl2/sdl as sdllib 7 | 8 | import c4/entities 9 | import c4/systems/video/sdl 10 | import c4/threads 11 | 12 | import ../messages 13 | 14 | 15 | type 16 | VideoSystem* = object of SdlVideoSystem 17 | 18 | Video* = object of SdlVideo 19 | width*: float 20 | height*: float 21 | color*: Color 22 | 23 | 24 | const 25 | backgroundColor* = Color( 26 | r: uint8(uint8.high.float * 0.1), 27 | g: uint8(uint8.high.float * 0.1), 28 | b: uint8(uint8.high.float * 0.1), 29 | a: uint8.high, 30 | ) 31 | wallColor* = Color( 32 | r: uint8(uint8.high.float * 0.3), 33 | g: uint8(uint8.high.float * 0.3), 34 | b: uint8(uint8.high.float * 0.3), 35 | a: uint8.high, 36 | ) 37 | playerColor* = Color(r: 0, g: uint8.high, b: 0, a: uint8.high) 38 | enemyColor* = Color(r: uint8.high, g: 0, b: 0, a: uint8.high) 39 | 40 | 41 | method render*(self: ref VideoSystem, video: ref Video) = 42 | discard self.renderer.setRenderDrawColor(video.color) 43 | 44 | var windowWidth, windowHeight: cint 45 | self.window.getWindowSize(windowWidth.addr, windowHeight.addr) 46 | 47 | var rect = Rect( 48 | x: int(windowWidth.float * (video.x - video.width / 2)), 49 | # sdl's (0, 0) is on top left corner, positive y is down -> mirror it: 50 | y: int(windowHeight.float * (1 - video.y - video.height / 2)), 51 | w: int(windowWidth.float * video.width), 52 | h: int(windowHeight.float * video.height), 53 | ) 54 | discard self.renderer.renderFillRect(rect.addr) 55 | 56 | 57 | method update*(self: ref VideoSystem, dt: float) = 58 | if self.renderer.renderClear() != 0: 59 | raise newException(LibraryError, &"Could not clear renderer: {getError()}") 60 | 61 | for video in getComponents(ref Video).values: 62 | self.render(video) 63 | 64 | discard self.renderer.setRenderDrawColor(backgroundColor) 65 | self.renderer.renderPresent() 66 | -------------------------------------------------------------------------------- /c4/templates/action/project.nim: -------------------------------------------------------------------------------- 1 | import c4/processes 2 | import c4/threads 3 | 4 | import src/systems/physics 5 | import src/systems/input 6 | import src/systems/video 7 | import src/systems/network 8 | 9 | import src/scenarios/init 10 | import src/scenarios/connection 11 | import src/scenarios/entity 12 | import src/scenarios/impersonation 13 | import src/scenarios/player_actions 14 | import src/scenarios/position 15 | 16 | 17 | when isMainModule: 18 | run("server"): 19 | spawn("network"): 20 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] server $levelname: ")) 21 | let network = new(ServerNetworkSystem) 22 | network.init(port=Port(9000)) 23 | network.run() 24 | network.dispose() 25 | 26 | spawn("physics"): 27 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] physics $levelname: ")) 28 | let physics = new(PhysicsSystem) 29 | physics.init() 30 | physics.run() 31 | physics.dispose() 32 | 33 | joinAll() 34 | 35 | run("client"): 36 | spawn("network"): 37 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] client $levelname: ")) 38 | let network = new(ClientNetworkSystem) 39 | network.init() 40 | network.connect(host="localhost", port=Port(9000)) 41 | network.run() 42 | network.dispose() 43 | 44 | spawn("input"): 45 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] input $levelname: ")) 46 | var input = new(InputSystem) 47 | input.init() 48 | input.run() 49 | input.dispose() 50 | 51 | spawn("video"): 52 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] video $levelname: ")) 53 | let video = new(VideoSystem) 54 | video.init() 55 | video.run() 56 | video.dispose() 57 | 58 | joinAll() 59 | 60 | processes.dieTogether() 61 | -------------------------------------------------------------------------------- /c4/templates/action/project.nimble: -------------------------------------------------------------------------------- 1 | import strutils 2 | import distros 3 | import ospaths 4 | import strformat 5 | 6 | 7 | version = "0.1" 8 | author = "Anonymous" 9 | license = "MIT" 10 | 11 | skipDirs = @["build"] 12 | 13 | requires "nim >= 1.1.1" 14 | requires "sdl2_nim >= 2.0.8" 15 | when defined(linux): 16 | requires "x11 >= 1.1" 17 | 18 | when defined(nimdistros): 19 | import distros 20 | 21 | if detectOs(ArchLinux): 22 | foreignDep "sdl" 23 | foreignDep "enet" 24 | foreignDep "ogre" 25 | foreignDep "ode" 26 | 27 | 28 | proc copyDir(src, dst: string) = 29 | mkDir(dst) 30 | 31 | for file in src.listFiles: 32 | echo dst / file.extractFilename 33 | file.cpFile(dst / file.extractFilename) 34 | 35 | for dir in src.listDirs: 36 | dir.copyDir(dst / dir.splitPath[1]) 37 | 38 | 39 | task collectAssets, "Put all assets into build folder": 40 | let 41 | assetsSrc = thisDir() / "assets" 42 | assetsDst = buildDir / "assets" 43 | 44 | if not dirExists(assetsSrc): 45 | echo &"Assets source dir does not exist: {assetsSrc}" 46 | return 47 | 48 | echo &"Collecting assets into {assetsDst}" 49 | copyDir(assetsSrc, assetsDst) 50 | -------------------------------------------------------------------------------- /c4/templates/action/project.nims: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | include "c4/lib/ogre/ogre.nims" 4 | include "c4/lib/ogre/ogre_sdl.nims" 5 | 6 | const buildDir = thisDir() / "build" 7 | 8 | switch("threads", "on") 9 | switch("multimethods", "on") 10 | switch("nimcache", buildDir / "nimcache") 11 | switch("out", buildDir / projectName()) 12 | -------------------------------------------------------------------------------- /c4/templates/action/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/messages 2 | import c4/entities 3 | import c4/systems/network/enet 4 | 5 | 6 | type 7 | EntityMessage* = object of NetworkMessage 8 | entity*: Entity 9 | 10 | CreatePlayerEntityMessage* = object of EntityMessage 11 | CreateBoxEntityMessage* = object of EntityMessage 12 | CreatePlaneEntityMessage* = object of EntityMessage 13 | 14 | register CreatePlayerEntityMessage 15 | register CreateBoxEntityMessage 16 | register CreatePlaneEntityMessage 17 | 18 | 19 | type PlayerMoveMessage* = object of Message 20 | # message for defining player's movement direction; the movement direction is relative to player's sight direction. 21 | yaw*: float ## Angle (in radians) around Y axis. 22 | pitch*: float ## Angle (in radians) around X axis. 23 | register PlayerMoveMessage 24 | 25 | 26 | type PlayerRotateMessage* = object of Message 27 | ## Message for defining player's rotation. See ``MoveMessage`` for reference. 28 | yaw*: float ## Angle (in radians) around Y axis. 29 | pitch*: float ## Angle (in radians) around X axis. 30 | register PlayerRotateMessage 31 | 32 | 33 | type SetPositionMessage* = object of EntityMessage 34 | ## Send this message to client in order to update object's position. 35 | x*, y*, z*: float 36 | register SetPositionMessage 37 | 38 | 39 | type 40 | Quaternion* = array[4, float] # w, x, y, z 41 | SetRotationMessage* = object of EntityMessage 42 | ## Send this message to client in order to update object's rotation. 43 | quaternion*: Quaternion # rotation quaternion 44 | register SetRotationMessage 45 | 46 | 47 | type SyncPositionMessage* = object of SetPositionMessage 48 | ## Reliable version of SetPositionMessage 49 | discard 50 | register SyncPositionMessage 51 | 52 | 53 | type SyncRotationMessage* = object of SetRotationMessage 54 | ## Reliable version of SetRotationMessage 55 | discard 56 | register SyncRotationMessage 57 | 58 | 59 | type ImpersonationMessage* = object of EntityMessage 60 | ## A signal for client to occupy selected entity 61 | discard 62 | register ImpersonationMessage 63 | 64 | 65 | type ResetSceneMessage* = object of Message # This message will reset physics system to initial state, so that we can play again 66 | 67 | # Always ``register`` Message subtypes. If not registered, network system won't have a knowledge on how to serialize the message, which will lead to sending pure ``Message`` instead of your subtype. 68 | register ResetSceneMessage 69 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/connection.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | # TODO: move specific messages here? 4 | import logging 5 | import tables 6 | import strformat 7 | 8 | import c4/sugar 9 | import c4/lib/ode/ode as odelib 10 | import c4/messages as c4messages 11 | import c4/systems 12 | import c4/systems/network/enet 13 | import c4/systems/physics/ode 14 | import c4/entities 15 | 16 | import ../systems/network 17 | import ../systems/physics 18 | import ../systems/video 19 | import ../messages 20 | 21 | 22 | method process*(self: ref network.ClientNetworkSystem, message: ref ConnectionOpenedMessage) = 23 | ## Prepare scene on client side, that's why we send this message to video system. 24 | message.send("video") 25 | 26 | 27 | method process*(self: ref network.ServerNetworkSystem, message: ref ConnectionOpenedMessage) = 28 | ## When new peer connects, we want to create a corresponding entity, thus we forward this message to physics system. 29 | message.send("physics") 30 | 31 | 32 | method process*(self: ref physics.PhysicsSystem, message: ref ConnectionOpenedMessage) = 33 | ## When new peer connects, we want to create a corresponding Entity for him. 34 | ## We also need to send all world information to new peer. 35 | 36 | let player = newEntity() # create new Entity 37 | let phys = new(BoxPhysics) 38 | self.init(phys) 39 | player[ref physics.Physics] = phys 40 | player[ref physics.Physics].body.bodySetPosition(0.0, 1.0, -5.0) 41 | 42 | # send new entity to all peers 43 | (ref CreatePlayerEntityMessage)(entity: player).send("network") 44 | 45 | let position = player[ref physics.Physics].body.bodyGetPosition()[] 46 | (ref SyncPositionMessage)(entity: player, x: position[0], y: position[1], z: position[2]).send("network") 47 | 48 | let rotation = player[ref physics.Physics].body.bodyGetQuaternion()[] 49 | (ref SyncRotationMessage)(entity: player, quaternion: rotation).send("network") 50 | 51 | # send impersonation message to new peer 52 | self.impersonationsMap[message.peer] = player # add it to mapping 53 | (ref ImpersonationMessage)(entity: player, recipient: message.peer).send("network") 54 | 55 | # send all scene data to new peer 56 | logging.debug &"Sending all scene data to peer {$(message.peer[])}" 57 | 58 | (ref CreatePlaneEntityMessage)(entity: self.plane, recipient: message.peer).send("network") 59 | let planePosition = self.plane[ref physics.Physics].body.bodyGetPosition()[] 60 | (ref SyncPositionMessage)(entity: self.plane, x: planePosition[0], y: planePosition[1], z: planePosition[2], recipient: message.peer).send("network") 61 | 62 | for box in self.boxes: 63 | (ref CreateBoxEntityMessage)(entity: box, recipient: message.peer).send("network") 64 | 65 | let physics = box[ref physics.Physics] 66 | 67 | let position = physics.body.bodyGetPosition()[] 68 | (ref SyncPositionMessage)(entity: box, x: position[0], y: position[1], z: position[2], recipient: message.peer).send("network") 69 | 70 | let rotation = physics.body.bodyGetQuaternion()[] 71 | (ref SyncRotationMessage)(entity: box, quaternion: rotation, recipient: message.peer).send("network") # TODO: make `recipient` attrubute of `send()`? 72 | 73 | 74 | method process*(self: ref network.ClientNetworkSystem, message: ref ConnectionClosedMessage) = 75 | ## Forward this message to video system in order to unload the scene 76 | procCall self.as(ref enet.ClientNetworkSystem).process(message) # trigger mappings 77 | message.send("video") 78 | 79 | logging.debug "Flushing local entities" 80 | for entity in entities.items(): 81 | if entity.has(ref physics.Physics): 82 | entity[ref physics.Physics].dispose() 83 | entities.flush() 84 | 85 | 86 | method process*(self: ref network.ServerNetworkSystem, message: ref ConnectionClosedMessage) = 87 | ## When peer disconnects, we want to delete corresponding entity, thus we forward this message to physics system. 88 | procCall self.as(ref enet.ServerNetworkSystem).process(message) # trigger mappings 89 | message.send("physics") 90 | 91 | 92 | method process*(self: ref physics.PhysicsSystem, message: ref ConnectionClosedMessage) = 93 | ## When peer disconnects, we want to remove a corresponding Entity. 94 | logging.debug &"Removing entity" 95 | let entity = self.impersonationsMap[message.peer] 96 | 97 | if entity.has(ref physics.Physics): 98 | entity[ref physics.Physics].dispose() 99 | entity.delete() 100 | (ref DeleteEntityMessage)(entity: entity).send("network") 101 | 102 | self.impersonationsMap.del(message.peer) # exclude peer's Entity from mapping 103 | 104 | 105 | method process*(self: ref VideoSystem, message: ref SystemReadyMessage) = 106 | # connect to server as soon as video system is loaded 107 | (ref ConnectMessage)(address: ("localhost", 11477'u16)).send("network") 108 | 109 | 110 | method process*(self: ref VideoSystem, message: ref SystemQuitMessage) = 111 | # disconnect as soon as video system is unloaded 112 | new(DisconnectMessage).send("network") 113 | 114 | 115 | method process*(self: ref VideoSystem, message: ref ConnectionOpenedMessage) = 116 | ## Load skybox when connection is established 117 | logging.debug "Loading skybox" 118 | 119 | 120 | method process*(self: ref VideoSystem, message: ref ConnectionClosedMessage) = 121 | ## Unload everything when connection is closed 122 | logging.debug "Unloading skybox" 123 | 124 | # self.skybox.removeNode() 125 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/entity.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import c4/sugar 4 | import c4/entities 5 | import c4/systems 6 | import c4/systems/network/enet 7 | import c4/systems/video/ogre 8 | 9 | import ../systems/network 10 | import ../systems/video 11 | import ../messages 12 | 13 | 14 | method process(self: ref network.ClientNetworkSystem, message: ref CreateEntityMessage) = 15 | ## Sends message to video system 16 | procCall self.as(ref enet.ClientNetworkSystem).process(message) # generate remote->local entity mapping 17 | message.send("video") 18 | 19 | 20 | method process*(self: ref video.VideoSystem, message: ref CreateEntityMessage) = 21 | # this will capture all `CreateEntity` messages by default and create box graphics 22 | let video = new(BoxVideo) 23 | self.init(video) 24 | message.entity[ref Video] = video 25 | 26 | 27 | method process*(self: ref video.VideoSystem, message: ref CreatePlaneEntityMessage) = 28 | let video = new(PlaneVideo) 29 | self.init(video) 30 | message.entity[ref Video] = video 31 | 32 | 33 | 34 | method process(self: ref network.ClientNetworkSystem, message: ref DeleteEntityMessage) = 35 | ## Deletes an entity when server asks to do so. 36 | procCall self.as(ref enet.ClientNetworkSystem).process(message) # update remote->local entity mapping 37 | message.entity.delete() 38 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/impersonation.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import logging 4 | import strformat 5 | import tables 6 | 7 | import c4/sugar 8 | import c4/entities 9 | import c4/systems 10 | import c4/systems/network/enet 11 | import c4/systems/video/ogre 12 | import c4/lib/ogre/ogre as ogrelib 13 | 14 | import ../messages 15 | import ../systems/network 16 | import ../systems/video 17 | 18 | 19 | method process*(self: ref network.ClientNetworkSystem, message: ref ImpersonationMessage) = 20 | ## When server tells client to occupy some entity, send this message to video system 21 | procCall self.as(ref enet.ClientNetworkSystem).process(message) 22 | message.send("video") 23 | 24 | 25 | method process*(self: ref video.VideoSystem, message: ref ImpersonationMessage) = 26 | ## Store player's entity in `playerNode`; attach camera to impersonated entity 27 | self.playerNode = message.entity[ref Video].node 28 | 29 | self.playerNode.attachObject(self.camera) 30 | logging.debug &"Camera attached to player node" 31 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/init.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import logging 4 | import strformat 5 | 6 | import c4/entities 7 | import c4/threads 8 | import c4/lib/ode/ode 9 | 10 | import ../systems/physics 11 | import ../messages 12 | 13 | 14 | # method process*(self: PhysicsSystem, message: ref SystemReadyMessage) = 15 | # # We want to reset our scene when physics system is ready. 16 | # new(ResetSceneMessage).send(self) 17 | 18 | 19 | method process*(self: var PhysicsSystem, message: ref ResetSceneMessage) = 20 | logging.debug "Resetting scene" 21 | 22 | if not self.plane.isInitialized: 23 | self.plane = newEntity() 24 | (ref CreateEntityMessage)(entity: self.plane).send("network") 25 | let physics = new(PlanePhysics) 26 | self.init(physics) 27 | self.plane[ref Physics] = physics 28 | 29 | let position = physics.body.bodyGetPosition() 30 | (ref SyncPositionMessage)(entity: self.plane, x: position[0], y: position[1], z: position[2]).send("network") 31 | 32 | # first, delete all existing boxes 33 | for box in self.boxes: 34 | (ref DeleteEntityMessage)(entity: box).send("network") 35 | box.delete() 36 | 37 | self.boxes = @[] 38 | 39 | # define boxes locations 40 | let boxesCoords = @[ 41 | (0.0, 0.5, -10.0), 42 | (-2.0, 0.5, -10.0), 43 | (2.0, 0.5, -10.0), 44 | (-1.0, 2.0, -10.0), 45 | (1.0, 2.0, -10.0), 46 | (0.0, 3.5, -10.0), 47 | ] 48 | 49 | var box: Entity 50 | 51 | for coords in boxesCoords: 52 | # create box at each position and send its coordinates 53 | box = newEntity() 54 | (ref CreateEntityMessage)(entity: box).send("network") 55 | 56 | let physics = BoxPhysics.new() 57 | self.init(physics) 58 | box[ref Physics] = physics 59 | 60 | logging.debug &"Setting position: {coords[0]} {coords[1]} {coords[2]}" 61 | box[ref Physics].body.bodySetPosition(coords[0], coords[1], coords[2]) 62 | (ref SyncPositionMessage)(entity: box, x: coords[0], y: coords[1], z: coords[2]).send("network") 63 | # TODO: add RotateMessage 64 | # cube[ref Physics].body.bodySetQuaternion([1.0, 1.0, 1.0, 0]) 65 | # let quat = cube[ref Physics].body.bodyGetQuaternion() 66 | # (ref SyncRotationMessage)(entity: cube, quaternion: [quat[0], quat[1], quat[2], quat[3]]).send("network") 67 | 68 | self.boxes.add(box) 69 | 70 | logging.debug "Scene loaded" 71 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/player_actions.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import tables 4 | import math 5 | 6 | import c4/lib/ode/ode as odelib 7 | 8 | import c4/sugar 9 | import c4/messages as c4messages 10 | import c4/entities 11 | import c4/systems 12 | import c4/systems/physics/ode 13 | 14 | import ../systems/network 15 | import ../systems/physics 16 | import ../messages 17 | 18 | 19 | method store*(self: ref network.ServerNetworkSystem, message: ref PlayerRotateMessage) = 20 | ## Allow server to store ``PlayerRotateMessage`` 21 | assert not message.isLocal 22 | procCall self.as(ref System).store(message) 23 | 24 | 25 | method process*(self: ref network.ServerNetworkSystem, message: ref PlayerRotateMessage) = 26 | message.send("physics") 27 | 28 | 29 | proc eulFromR(r: dMatrix3): tuple[z, y, x: float] = 30 | # ZYXr case only 31 | let cy = sqrt(r[0] * r[0] + r[4] * r[4]) 32 | if cy > 16 * 0.000002: 33 | result.x = arctan2(r[9], r[10]) 34 | result.y = arctan2(-r[8], cy) 35 | result.z = arctan2(r[4], r[0]) 36 | else: 37 | result.x = arctan2(-r[6], r[5]) 38 | result.y = arctan2(-r[8], cy) 39 | result.z = 0 40 | 41 | 42 | proc eulFromQ(q: dQuaternion): tuple[z, y, x: float] = 43 | # ZYXr case only 44 | 45 | # get rotation matrix 46 | var m: dMatrix3 47 | m.rfromQ(q) 48 | 49 | eulFromR(m) 50 | 51 | 52 | proc getPitchYaw(q: dQuaternion): tuple[yaw: float, pitch: float] = 53 | ## Convert quaternion as only yaw and pitch rotations 54 | 55 | # rotation in Euler angles 56 | let eul = q.eulFromQ() 57 | 58 | # get current yaw & pitch (as if it was without roll) 59 | let flip: bool = not(abs(eul.z) <= 0.001) 60 | 61 | result.yaw = if not flip: eul.y else: PI - eul.y 62 | result.pitch = if not flip: eul.x else: eul.x + (if eul.x < 0: PI else: -PI) 63 | 64 | 65 | method process(self: ref physics.PhysicsSystem, message: ref PlayerRotateMessage) = 66 | let playerEntity = self.impersonationsMap[message.sender] 67 | 68 | # get current rotation quaternion 69 | let qCurrent = playerEntity[ref physics.Physics].body.bodyGetQuaternion()[] 70 | 71 | # get current yaw and pitch 72 | let current = qCurrent.getPitchYaw() 73 | 74 | # calculate combined pitch and yaw 75 | let 76 | yaw = current.yaw + message.yaw 77 | pitch = max(min(current.pitch + message.pitch, PI/2 * 0.99), -PI/2 * 0.99) 78 | 79 | # convert PlayerRotateMessage relative angles to rotation quaternions 80 | var qYaw, qPitch: dQuaternion 81 | qYaw.qFromAxisAndAngle(0, 1, 0, yaw) 82 | qPitch.qFromAxisAndAngle(1, 0, 0, pitch) 83 | 84 | # multiply rotation quaternions and set result as new entity rotation quaternion 85 | var qFinal: dQuaternion 86 | qFinal.qMultiply0(qYaw, qPitch) 87 | 88 | playerEntity[ref physics.Physics].body.bodySetQuaternion(qFinal) 89 | 90 | 91 | method store*(self: ref network.ServerNetworkSystem, message: ref PlayerMoveMessage) = 92 | ## Allow server to store ``PlayerMoveMessage`` 93 | assert not message.isLocal 94 | procCall self.as(ref System).store(message) 95 | 96 | 97 | method process(self: ref network.ServerNetworkSystem, message: ref PlayerMoveMessage) = 98 | message.send("physics") 99 | 100 | 101 | method process(self: ref physics.PhysicsSystem, message: ref PlayerMoveMessage) = 102 | let playerEntity = self.impersonationsMap[message.sender] 103 | 104 | # calculate selected direction as a result of yaw on (0, 0, -1) vector 105 | let direction: array[3, float] = [-sin(message.yaw) , 0.0, -cos(message.yaw)] 106 | 107 | # get current rotation matrix and apply it to selected direction 108 | let rotation = playerEntity[ref physics.Physics].body.bodyGetRotation()[] 109 | let finalDirection: array[3, float] = [ 110 | rotation[0] * direction[0] + rotation[1] * direction[1] + rotation[2] * direction[2], 111 | rotation[4] * direction[0] + rotation[5] * direction[1] + rotation[6] * direction[2], 112 | rotation[8] * direction[0] + rotation[9] * direction[1] + rotation[10] * direction[2], 113 | ] 114 | 115 | const walkSpeed = 5 * 1000 / 60 / 60 116 | playerEntity[ref physics.Physics].body.bodySetLinearVel( 117 | finalDirection[0] * walkSpeed, 118 | finalDirection[1] * walkSpeed, 119 | finalDirection[2] * walkSpeed, 120 | ) 121 | playerEntity[ref physics.Physics].startMovement() 122 | -------------------------------------------------------------------------------- /c4/templates/action/src/scenarios/position.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | 3 | import logging 4 | import strformat 5 | 6 | import c4/lib/ogre/ogre as ogrelib 7 | 8 | import c4/sugar 9 | import c4/entities 10 | import c4/systems 11 | import c4/systems/video/ogre 12 | import c4/systems/network/enet 13 | 14 | import ../systems/video 15 | import ../systems/network 16 | import ../messages 17 | 18 | 19 | method process*(self: ref network.ClientNetworkSystem, message: ref SetPositionMessage) = 20 | procCall self.as(ref enet.ClientNetworkSystem).process(message) 21 | message.send("video") 22 | 23 | 24 | method process(self: ref video.VideoSystem, message: ref SetPositionMessage) = 25 | if not message.entity.has(ref Video): 26 | logging.warn &"{$(self)} received {$(message)}, but has no Video component" 27 | # raise newException(LibraryError, "Shit im getting errors") 28 | # TODO: When client just connected to server, the server still may broadcast some messages 29 | # before syncing world state with client. When these messages reach client, it doesn't have 30 | # corresponding components yet, thus won't be able to process these messages and fail. 31 | return 32 | 33 | message.entity[ref Video].node.setPosition(message.x, message.y, message.z) 34 | 35 | 36 | method process*(self: ref network.ClientNetworkSystem, message: ref SetRotationMessage) = 37 | ## Forward the message to video system 38 | procCall self.as(ref enet.ClientNetworkSystem).process(message) 39 | message.send("video") 40 | 41 | 42 | method process*(self: ref video.VideoSystem, message: ref SetRotationMessage) = 43 | if not message.entity.has(ref Video): 44 | logging.warn &"{$(self)} received {$(message)}, but has no Video component" 45 | return 46 | 47 | message.entity[ref Video].node.setOrientation( 48 | message.quaternion[0], 49 | message.quaternion[1], 50 | message.quaternion[2], 51 | message.quaternion[3], 52 | ) 53 | -------------------------------------------------------------------------------- /c4/templates/action/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2/sdl as sdllib 2 | import math 3 | import net 4 | 5 | import c4/sugar 6 | import c4/threads 7 | import c4/systems/input/sdl 8 | import c4/systems/network/enet 9 | 10 | import ../messages 11 | 12 | 13 | type 14 | InputSystem* = object of SdlInputSystem 15 | 16 | 17 | proc handle*(self: ref InputSystem, event: Event) = 18 | case event.kind 19 | of WINDOWEVENT: 20 | case event.window.event 21 | of WINDOWEVENT_FOCUS_LOST: 22 | discard 23 | # self.state = State.inactive 24 | 25 | of WINDOWEVENT_TAKE_FOCUS: 26 | discard 27 | # self.state = State.active 28 | 29 | else: 30 | discard 31 | 32 | of MOUSEMOTION: 33 | var x, y: cint 34 | let radInPixel = PI / 180 / 4 # 0.25 degree in 1 pixel 35 | discard getRelativeMouseState(x.addr, y.addr) 36 | (ref PlayerRotateMessage)( 37 | yaw: -x.float * radInPixel, 38 | pitch: -y.float * radInPixel, 39 | ).send("network") 40 | 41 | of KEYDOWN: 42 | case event.key.keysym.sym 43 | of K_c: 44 | # When player presses "C" key, we want to establish connection to remote server. We create new ``ConnectMessage`` (which is already predefined in Enet networking system), set server address and send this message to network system. Default Enet networking system knows that it should connect to the server when receiving this kind of message. 45 | (ref ConnectMessage)(host: "localhost", port: Port(11477)).send("network") 46 | 47 | of K_q: 48 | # When player presses "Q" key, we want to disconnect from server. We create new ``DisconnectMessage`` (which is already predefined in Enet networking system), and sent this message to network system. Default Enet networking system knows that it should disconnect from the server when receiving this kind of message. 49 | new(DisconnectMessage).send("network") 50 | 51 | of K_r: 52 | # When player presses "R" key, we want server to reset the scene. We defined custom ``ResetSceneMessage`` and send it over the network. 53 | new(ResetSceneMessage).send("network") 54 | 55 | else: 56 | discard 57 | 58 | else: 59 | discard 60 | 61 | # fallback to default implementation 62 | self.as(ref SdlInputSystem).handle(event) 63 | 64 | 65 | proc update(self: ref InputSystem, dt: float) = 66 | # when some key is held down, there's usually a delay between first KEYDOWN event 67 | # and subsequent ones; so if you want to send some messages constantly when key is pressed 68 | # (for example, `PlayerMoveMessage`), you shouldn't rely on KEYDOWN event; instead, 69 | # you should check whether key is down in `update()` method 70 | 71 | self.as(ref SdlInputSystem).update(dt) 72 | 73 | # process long-pressing key by polling keyboard state 74 | let 75 | keyboard = getKeyboardState(nil) 76 | 77 | var 78 | forward = keyboard[SCANCODE_W].bool 79 | backward = keyboard[SCANCODE_S].bool 80 | left = keyboard[SCANCODE_A].bool 81 | right = keyboard[SCANCODE_D].bool 82 | 83 | # pressing opposite keys disables both of them 84 | if forward and backward: 85 | forward = false 86 | backward = false 87 | 88 | if left and right: 89 | left = false 90 | right = false 91 | 92 | if forward or backward or left or right: 93 | var yaw: float 94 | if right and not forward and not backward: 95 | yaw = -2 * PI/4 96 | elif right and forward: 97 | yaw = -1 * PI/4 98 | elif forward and not right and not left: 99 | yaw = 0 * PI/4 100 | elif forward and left: 101 | yaw = 1 * PI/4 102 | elif left and not forward and not backward: 103 | yaw = 2 * PI/4 104 | elif left and backward: 105 | yaw = 3 * PI/4 106 | elif backward and not left and not right: 107 | yaw = 4 * PI/4 108 | elif backward and right: 109 | yaw = 5 * PI/4 110 | 111 | (ref PlayerMoveMessage)(yaw: yaw).send("network") 112 | -------------------------------------------------------------------------------- /c4/templates/action/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | 3 | import c4/threads 4 | import c4/systems/network/enet 5 | import c4/utils/stringify 6 | 7 | import ../messages 8 | 9 | 10 | type 11 | ClientNetworkSystem* = object of EnetClientNetworkSystem 12 | ServerNetworkSystem* = object of EnetServerNetworkSystem 13 | 14 | 15 | method processRemote*(self: ref ServerNetworkSystem, message: ref ResetSceneMessage) = 16 | # When network receives ``ResetSceneMessage``, it forwards the message to physics system 17 | message.send("physics") 18 | -------------------------------------------------------------------------------- /c4/templates/action/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import logging 2 | import tables 3 | 4 | import c4/lib/ode/ode as odelib 5 | import c4/lib/enet/enet 6 | 7 | import c4/sugar 8 | import c4/entities 9 | import c4/threads 10 | import c4/messages as c4messages 11 | import c4/systems/physics/ode 12 | 13 | import ../messages 14 | 15 | 16 | type 17 | PhysicsSystem* = object of OdePhysicsSystem 18 | impersonationsMap*: Table[ptr Peer, Entity] ## Mapping from remote Peer to an Entity it's controlling 19 | 20 | boxes*: seq[Entity] 21 | plane*: Entity 22 | 23 | Physics* = object of OdePhysics 24 | # additionally store previous position & rotation; 25 | # position/rotation update messages are sent only when values really changes 26 | prevPosition: array[3, dReal] 27 | prevRotation: dQuaternion 28 | 29 | movementDurationElapsed: float # TODO: required only for players' nodes 30 | 31 | BoxPhysics* = object of Physics 32 | PlanePhysics* = object of Physics 33 | 34 | 35 | const 36 | G* = 0 # 9.81 37 | 38 | # when received any movement command, this defines how long the movement will continue; 39 | # even if there's no command from client, the entity will continue moving during this period (in seconds) 40 | movementDuration = 0.1 41 | 42 | 43 | # ---- Component ---- 44 | 45 | method init*(self: ref PhysicsSystem, physics: ref Physics) = 46 | procCall self.as(ref OdePhysicsSystem).init(physics) 47 | 48 | physics.prevPosition = physics.body.bodyGetPosition()[] 49 | physics.prevRotation = physics.body.bodyGetQuaternion()[] 50 | 51 | physics.movementDurationElapsed = 0 52 | 53 | 54 | proc startMovement*(self: ref Physics) = 55 | self.movementDurationElapsed = movementDuration 56 | 57 | 58 | method init*(self: ref PhysicsSystem, physics: ref BoxPhysics) = 59 | procCall self.init(physics as ref Physics) 60 | 61 | let geometry = createBox(self.space, 1, 1, 1) 62 | geometry.geomSetBody(physics.body) 63 | 64 | let mass = cast[ptr dMass](alloc(sizeof(dMass))) 65 | # TODO: var mass = ode.dMass() 66 | mass.massSetBoxTotal(0.5, 1.0, 1.0, 1.0) 67 | physics.body.bodySetMass(mass) 68 | 69 | # TODO: send geometry (AABB) to graphics system - AddGeometryMessage 70 | 71 | 72 | # ---- System ---- 73 | # proc nearCallback(data: pointer, o1: dGeomID, o2: dGeomID) = 74 | # logging.debug "COLLISION" 75 | # # static const int N = 4; // As for the upper limit of contact score without forgetting 4 static, attaching - chego blyat?! 76 | 77 | # # dContact contact [ N ]; 78 | 79 | # # int isGround = ((ground == o1) || (ground == o2)); 80 | # # int n = dCollide (o1, o2, N and &contact [ 0 ] geom, sizeof (dContact)); // As for n collision score 81 | # # if (isGround) { //the flag of the ground stands, collision detection function can be used 82 | # # for (int i = 0; I < n; I++) { 83 | # # contact [ i ] surface.mode = dContactBounce; // Setting the coefficient of rebound of the land 84 | # # contact [ i ] surface.bounce = 0.0; // (0.0 – 1.0) as for coefficient of rebound from 0 up to 1 85 | # # contact [ i ] surface.bounce_vel = 0.0; // (0.0 or more) the lowest speed which is necessary for rally 86 | 87 | # # / / Contact joint formation 88 | # # dJointID c = dJointCreateContact (world, contactgroup and &contact [ i ]); 89 | # # // Restraining two geometry which contact with the contact joint 90 | # # dJointAttach (c, dGeomGetBody (contact [ i ] geom.g1), 91 | # # dGeomGetBody (contact [ i ] geom.g2)); 92 | # # } 93 | # # } 94 | # # } 95 | 96 | 97 | proc init*(self: ref PhysicsSystem) = 98 | self.as(ref OdePhysicsSystem).init() 99 | self.world.worldSetGravity(0, -G, 0) 100 | # self.nearCallback = nearCallback 101 | 102 | proc update*(self: ref PhysicsSystem, dt: float) = 103 | self.as(ref OdePhysicsSystem).update(dt) 104 | 105 | for entity, physics in getComponents(ref Physics): 106 | ## This method compares previous position and rotation of entity, and (if there are any changes) sends ``MoveMessage`` or ``RotateMessage``. 107 | let position = physics.body.bodyGetPosition()[] 108 | for dimension in 0..2: 109 | if position[dimension] != physics.prevPosition[dimension]: 110 | physics.prevPosition = position 111 | (ref SetPositionMessage)( 112 | entity: entity, 113 | x: position[0], 114 | y: position[1], 115 | z: position[2], 116 | ).send("network") 117 | break 118 | 119 | let rotation = physics.body.bodyGetQuaternion()[] 120 | for dimension in 0..3: 121 | if rotation[dimension] != physics.prevRotation[dimension]: 122 | physics.prevRotation = rotation 123 | (ref SetRotationMessage)( 124 | entity: entity, 125 | quaternion: rotation, 126 | ).send("network") 127 | break 128 | 129 | if physics.movementDurationElapsed > 0: 130 | physics.movementDurationElapsed -= dt 131 | if physics.movementDurationElapsed <= 0: 132 | # it's time to stop movement 133 | physics.body.bodySetLinearVel(0, 0, 0) 134 | -------------------------------------------------------------------------------- /c4/templates/action/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import c4/sugar 5 | import c4/lib/ogre/ogre as ogrelib 6 | import c4/systems/video/ogre 7 | import c4/threads 8 | 9 | 10 | type 11 | VideoSystem* = object of OgreVideoSystem 12 | playerNode*: ptr SceneNode 13 | 14 | BoxVideo* = object of OgreVideo 15 | PlaneVideo* = object of OgreVideo 16 | 17 | 18 | method init*(self: ref VideoSystem, video: ref BoxVideo) = 19 | procCall self.as(ref OgreVideoSystem).init(video) 20 | video.node.attachObject(self.sceneManager.createEntity("box")) 21 | 22 | 23 | method init*(self: ref VideoSystem, video: ref PlaneVideo) = 24 | procCall self.as(ref OgreVideoSystem).init(video) 25 | video.node.attachObject(self.sceneManager.createEntity("plane")) 26 | 27 | 28 | proc init*(self: ref VideoSystem) = 29 | procCall self.as(ref OgreVideoSystem).init() 30 | logging.debug "Loading custom video resources" 31 | 32 | operateOn self.resourceManager: 33 | addResourceLocation(defaultMediaDir / "packs" / "SdkTrays.zip", "Zip", resGroup="Essential") 34 | addResourceLocation(defaultMediaDir, "FileSystem", resGroup="General") 35 | addResourceLocation(defaultMediaDir / "models", "FileSystem", resGroup="General") 36 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "Cg", "FileSystem", resGroup="General") 37 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "GLSL", "FileSystem", resGroup="General") 38 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "GLSL120", "FileSystem", resGroup="General") 39 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "GLSL150", "FileSystem", resGroup="General") 40 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "GLSL400", "FileSystem", resGroup="General") 41 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "HLSL", "FileSystem", resGroup="General") 42 | addResourceLocation(defaultMediaDir / "materials" / "programs" / "HLSL_Cg", "FileSystem", resGroup="General") 43 | addResourceLocation(defaultMediaDir / "materials" / "scripts", "FileSystem", resGroup="General") 44 | addResourceLocation(defaultMediaDir / "materials" / "textures", "FileSystem", resGroup="General") 45 | initialiseAllResourceGroups() 46 | 47 | self.camera.setNearClipDistance(0.01) 48 | self.sceneManager.setAmbientLight(initColourValue(0.5, 0.5, 0.5)) 49 | 50 | let light = self.sceneManager.createLight("MainLight") 51 | light.setPosition(20.0, 80.0, 50.0) 52 | 53 | # ---- draw axis ---- 54 | let axisObject = self.sceneManager.createManualObject() 55 | axisObject.begin("BaseWhiteNoLighting", OT_LINE_LIST) 56 | 57 | # X axis, red 58 | axisObject.position(0, 0, 0) 59 | axisObject.colour(1, 0, 0) 60 | axisObject.position(100, 0, 0) 61 | 62 | # Y axis, green 63 | axisObject.position(0, 0, 0) 64 | axisObject.colour(0, 1, 0) 65 | axisObject.position(0, 100, 0) 66 | 67 | # Z axis, blue 68 | axisObject.position(0, 0, 0) 69 | axisObject.colour(0, 0, 1) 70 | axisObject.position(0, 0, 100) 71 | 72 | discard axisObject.end() 73 | discard axisObject.convertToMesh("axis") 74 | 75 | let axis = self.sceneManager.createEntity("axis") 76 | self.sceneManager.getRootSceneNode().createChildSceneNode().attachObject(axis) 77 | 78 | # ---- create box mesh ---- 79 | let boxObject = self.sceneManager.createManualObject() 80 | 81 | operateOn boxObject: 82 | begin("BaseWhiteNoLighting", OT_TRIANGLE_LIST) 83 | 84 | # front 85 | position(-0.5, -0.5, 0.5) 86 | colour(0, 0, 0.75) 87 | position(0.5, -0.5, 0.5) 88 | position(0.5, 0.5, 0.5) 89 | position(-0.5, 0.5, 0.5) 90 | quad(0, 1, 2, 3) 91 | 92 | # back 93 | position(-0.5, 0.5, -0.5) 94 | position(0.5, 0.5, -0.5) 95 | position(0.5, -0.5, -0.5) 96 | position(-0.5, -0.5, -0.5) 97 | quad(4, 5, 6, 7) 98 | 99 | # right 100 | position(0.5, -0.5, 0.5) 101 | colour(0.75, 0, 0) 102 | position(0.5, -0.5, -0.5) 103 | position(0.5, 0.5, -0.5) 104 | position(0.5, 0.5, 0.5) 105 | quad(8, 9, 10, 11) 106 | 107 | # left 108 | position(-0.5, -0.5, -0.5) 109 | position(-0.5, -0.5, 0.5) 110 | position(-0.5, 0.5, 0.5) 111 | position(-0.5, 0.5, -0.5) 112 | quad(12, 13, 14, 15) 113 | 114 | # bottom 115 | position(-0.5, -0.5, -0.5) 116 | colour(0, 0.75, 0) 117 | position(0.5, -0.5, -0.5) 118 | position(0.5, -0.5, 0.5) 119 | position(-0.5, -0.5, 0.5) 120 | quad(16, 17, 18, 19) 121 | 122 | # up 123 | position(-0.5, 0.5, 0.5) 124 | position(0.5, 0.5, 0.5) 125 | position(0.5, 0.5, -0.5) 126 | position(-0.5, 0.5, -0.5) 127 | quad(20, 21, 22, 23) 128 | 129 | discard boxObject.end() 130 | discard boxObject.convertToMesh("box") 131 | 132 | # ---- plane ---- 133 | let planeObject = self.sceneManager.createManualObject() 134 | 135 | operateOn planeObject: 136 | begin("BaseWhiteNoLighting", OT_TRIANGLE_LIST) 137 | 138 | position(-50, 0.float, 50) 139 | colour(0.0, 1.0, 1.0) 140 | position(50, 0.float, 50) 141 | colour(1.0, 1.0, 1.0) 142 | position(50, 0.float, -50) 143 | colour(1.0, 0.0, 0.0) 144 | position(-50, 0.float, -50) 145 | colour(0.0, 0.0, 0.0) 146 | quad(0, 1, 2, 3) 147 | 148 | discard planeObject.end() 149 | discard planeObject.convertToMesh("plane") 150 | -------------------------------------------------------------------------------- /c4/templates/base/project.nim: -------------------------------------------------------------------------------- 1 | import net 2 | import logging 3 | 4 | import c4/processes 5 | import c4/threads 6 | import c4/utils/loglevel 7 | 8 | import c4/systems/network/enet 9 | import c4/systems/physics/ode 10 | import c4/systems/input/sdl 11 | import c4/systems/video/ogre 12 | 13 | import src/systems/network 14 | import src/systems/physics 15 | import src/systems/input 16 | import src/systems/video 17 | 18 | import src/scenarios/init 19 | 20 | 21 | when isMainModule: 22 | run("server"): 23 | spawn("network"): 24 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] server $levelname: ")) 25 | let network = new(ServerNetworkSystem) 26 | network.init(port=Port(9000)) 27 | network.run() 28 | network.dispose() 29 | 30 | spawn("physics"): 31 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] physics $levelname: ")) 32 | let physics = new(PhysicsSystem) 33 | physics.init() 34 | physics.run() 35 | physics.dispose() 36 | 37 | joinAll() 38 | 39 | run("client"): 40 | spawn("network"): 41 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] client $levelname: ")) 42 | let network = new(ClientNetworkSystem) 43 | network.init() 44 | network.connect(host="localhost", port=Port(9000)) 45 | network.run() 46 | network.dispose() 47 | 48 | spawn("input"): 49 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] input $levelname: ")) 50 | var input = new(InputSystem) 51 | input.init() 52 | input.run() 53 | input.dispose() 54 | 55 | spawn("video"): 56 | logging.addHandler(logging.newConsoleLogger(levelThreshold=getCmdLogLevel(), fmtStr="[$datetime] video $levelname: ")) 57 | let video = new(VideoSystem) 58 | video.init() 59 | video.run() 60 | video.dispose() 61 | 62 | joinAll() 63 | 64 | processes.dieTogether() 65 | -------------------------------------------------------------------------------- /c4/templates/base/project.nimble: -------------------------------------------------------------------------------- 1 | 2 | version = "0.1" 3 | author = "Anonymous" 4 | license = "MIT" 5 | 6 | skipDirs = @["build"] 7 | 8 | requires "nim >= 1.1.1" 9 | -------------------------------------------------------------------------------- /c4/templates/base/project.nims: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | include "c4/lib/ogre/ogre.nims" 4 | include "c4/lib/ogre/ogre_sdl.nims" 5 | 6 | const buildDir = thisDir() / "build" 7 | 8 | switch("threads", "on") 9 | switch("multimethods", "on") 10 | switch("nimcache", buildDir / "nimcache") 11 | switch("out", buildDir / projectName()) 12 | -------------------------------------------------------------------------------- /c4/templates/base/src/messages.nim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xE111/cat-400/df9f2fea2430ffca8461279ca93a7a00c7c9a763/c4/templates/base/src/messages.nim -------------------------------------------------------------------------------- /c4/templates/base/src/scenarios/init.nim: -------------------------------------------------------------------------------- 1 | when defined(nimHasUsed): 2 | {.used.} 3 | 4 | # put your first scenario here 5 | -------------------------------------------------------------------------------- /c4/templates/base/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2/sdl as sdllib 2 | 3 | import c4/systems/input/sdl 4 | 5 | import ../messages 6 | 7 | 8 | type InputSystem* = object of SdlInputSystem 9 | 10 | # redefine input system methods below 11 | 12 | # method handle*(self: ref InputSystem, event: Event) = 13 | # case event.kind 14 | # of QUIT: 15 | # new(SystemQuitMessage).send(@["video", "network"]) 16 | # of WINDOWEVENT: 17 | # case event.window.event 18 | # of WINDOWEVENT_SIZE_CHANGED: 19 | # (ref WindowResizeMessage)( 20 | # width: event.window.data1, 21 | # height: event.window.data2, 22 | # ).send("video") 23 | # else: 24 | # discard 25 | # else: 26 | # procCall self.as(ref SdlInputSystem).handle(event) 27 | -------------------------------------------------------------------------------- /c4/templates/base/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/network/enet 2 | 3 | import ../messages 4 | 5 | 6 | type ServerNetworkSystem* = object of EnetServerNetworkSystem 7 | type ClientNetworkSystem* = object of EnetClientNetworkSystem 8 | -------------------------------------------------------------------------------- /c4/templates/base/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/physics/ode 2 | 3 | import ../messages 4 | 5 | 6 | type PhysicsSystem* = object of OdePhysicsSystem 7 | 8 | # redefine physics system methods below 9 | -------------------------------------------------------------------------------- /c4/templates/base/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/video/ogre 2 | 3 | import ../messages 4 | 5 | 6 | type VideoSystem* = object of OgreVideoSystem 7 | 8 | # redefine video system methods below 9 | -------------------------------------------------------------------------------- /c4/threads.nim: -------------------------------------------------------------------------------- 1 | import times 2 | import os 3 | 4 | import ./messages 5 | import ./logging 6 | 7 | 8 | type 9 | ThreadID* = uint8 10 | ThreadUnavailable* = object of CatchableError 11 | 12 | var 13 | channels*: array[16, Channel[ref Message]] 14 | activeThreads*: set[ThreadID] 15 | threadID* {.threadvar.}: ThreadID 16 | threadName* {.threadvar.}: string 17 | 18 | threadID = 0 19 | threadName = "main" 20 | channels[threadID].open() 21 | 22 | publicLogScope: 23 | threadID 24 | threadName 25 | 26 | let 27 | defaultPollInterval*: Duration = initDuration(milliseconds=100) 28 | 29 | template spawnThread*(id: ThreadID, code: untyped) = 30 | activeThreads.incl(id) 31 | channels[id].open() 32 | 33 | var thread: Thread[ThreadID] 34 | thread.createThread(proc(thisThreadID: ThreadID) {.gcsafe.} = 35 | threadID = thisThreadID 36 | threadName = "thread" & $threadID 37 | withLog(DEBUG, "running thread"): 38 | code 39 | channels[thisThreadID].close() 40 | activeThreads.excl(thisThreadID) 41 | , id) 42 | 43 | 44 | proc probe*( 45 | id: ThreadID, 46 | timeout: Duration, 47 | pollInterval: Duration = defaultPollInterval, 48 | ) {.raises: [ThreadUnavailable].} = 49 | ## Probe whether specific thread becomes available in `timeout` seconds, checking every `interval` seconds. 50 | 51 | let timeoutTime = epochTime() + timeout.inNanoseconds.int / 1000000 52 | let sleepTime = (pollInterval.inNanoseconds.int / 1000000).int 53 | while epochTime() < timeoutTime: 54 | if id in activeThreads: 55 | return 56 | sleep(sleepTime) 57 | 58 | raise newException(ThreadUnavailable, "Thread " & $id & " is not available after " & $timeout & " seconds") 59 | 60 | 61 | proc joinActiveThreads*(pollInterval: Duration = defaultPollInterval) = 62 | let sleepTime = (pollInterval.inNanoseconds.int / 1000000).int 63 | while activeThreads.len > 0: 64 | trace "waiting for threads to finish", activeThreads, pollInterval 65 | sleep(sleepTime) 66 | trace "all active threads finished" 67 | 68 | 69 | proc send*(message: ref Message, id: ThreadID) = 70 | ## Sends a message to a thread. 71 | ## Usage example: 72 | ## new(HelloMessage).send(ThreadID(1)) 73 | ## (ref PayloadMessage)(x: 1).send(ThreadID(2)) 74 | 75 | trace "sending message", thread=id, message=message 76 | channels[id].send(message) 77 | 78 | 79 | when isMainModule: 80 | import unittest 81 | 82 | type HelloMessage* = object of Message 83 | 84 | suite "Mutithreading": 85 | 86 | test "Spawning thread template": 87 | const 88 | thread1 = ThreadID(1) 89 | thread2 = ThreadID(2) 90 | 91 | spawnThread thread1: 92 | echo "thread1 waiting for a message" 93 | discard channels[threadID].recv() 94 | echo "thread1 received a message" 95 | 96 | spawnThread thread2: 97 | echo "thread2 waiting for thread1 to appear" 98 | probe(thread1, timeout=initDuration(seconds=10)) 99 | echo "thread2 sending a message to thread1" 100 | new(HelloMessage).send(thread1) 101 | echo "thread2 sent a message to thread1" 102 | 103 | joinActiveThreads() 104 | -------------------------------------------------------------------------------- /c4/utils/floats.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | proc `==`*(a, b: float): bool {.inline.} = 4 | (a - b).abs <= 0.01 5 | 6 | proc `!=`*(a, b: float): bool {.inline.} = not (a == b) 7 | -------------------------------------------------------------------------------- /c4/utils/loading.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | import os 3 | import strutils 4 | import strformat 5 | 6 | # # Unused 7 | # macro importString*(module, alias: static[string]): untyped = 8 | # result = newNimNode(nnkImportStmt).add( 9 | # newNimNode(nnkInfix).add(newIdentNode("as")).add(newIdentNode(module)).add(newIdentNode(alias)) 10 | # ) 11 | 12 | macro importString*(module: static[string]): untyped = 13 | result = newNimNode(nnkImportStmt).add( 14 | newIdentNode(module) 15 | ) 16 | 17 | 18 | const 19 | frameworkDir = currentSourcePath.parentDir.parentDir 20 | projectDir {.strdefine.}: string = "" 21 | 22 | 23 | template load*(module: static[string]): untyped = 24 | const customModule = projectDir / module 25 | when fileExists(customModule & ".nim"): # try to import custom module from project root 26 | echo "> Using custom module " & customModule 27 | importString(customModule) 28 | else: # import default implementation 29 | importString(frameworkDir / module) 30 | 31 | 32 | # macro importDir*(dir: static[string]): untyped = 33 | # ## Imports all *.nim files from specific dir 34 | # # Does not work! 35 | # result = newNimNode(nnkStmtList) 36 | # echo &"Importing directory \"{dir}\":" 37 | # for kind, name in walkDir(dir, relative=true): 38 | # if kind == pcFile and name.endsWith(".nim"): 39 | # echo &" - {name}" 40 | # result.add(newNimNode(nnkImportStmt).add(newIdentNode(dir / name[0..^(".nim".len + 1)]))) 41 | -------------------------------------------------------------------------------- /c4/utils/loglevel.nim: -------------------------------------------------------------------------------- 1 | import logging 2 | import parseopt 3 | import strutils 4 | 5 | 6 | proc getCmdLogLevel*(): logging.Level = 7 | ## Scans command-line arguments for "--loglevel" or "-l" and returns specified log level. 8 | ## Returns lvlInfo by default. 9 | 10 | for kind, key, value in parseopt.getopt(): 11 | if (kind == cmdLongOption and key == "loglevel") or (kind == cmdShortOption and key == "l"): 12 | return parseEnum[logging.Level]("lvl" & value) 13 | 14 | return logging.Level.lvlInfo 15 | -------------------------------------------------------------------------------- /c4/utils/stringify.nim: -------------------------------------------------------------------------------- 1 | 2 | template strMethod*(T: typedesc, fields: bool = true) = 3 | ## Defines ``$`` method for selected type ``T``. Output contains type name and all fields' values. 4 | ## 5 | ## Args: 6 | ## fields - whether to output fields of T 7 | method `$`*(self: ref T): string = $(T) & (if fields: " " & $self[] else: "") 8 | -------------------------------------------------------------------------------- /docs/tutorials/01 - project setup/readme.md: -------------------------------------------------------------------------------- 1 | 2 | Tutorial 1 - project setup 3 | ========================== 4 | 5 | Introduction 6 | ------------ 7 | 8 | `Cat 400` is a game framework - a library that helps programmers to create games. Think of creating any game - you need timer, game loop, drawing something on screen, react to user input etc. `Cat 400` is the thing that will provide all these small blocks of code and (more important) glue them together, thus shifting focus from implementation details to game development itself. 9 | 10 | However, `Cat 400` is not an _engine_ - there is no visual editor, no blueprints or whatever. Everything is done in code, and it's up to you to program your game. Of course there is a possibility to build an engine on top of this library, but it's not even in plans. `Cat 400` is a low-level thing, and it will remain like this. 11 | 12 | Installing the framework 13 | ------------------------ 14 | 15 | This library doesn't require some special software. You can replace almost all its parts with your own implementations, so there are no external dependencies for new empty project. So, unlike other game frameworks, there are no strict requirements. 16 | 17 | However, `Cat 400` ships with some default systems which you may choose and use. For example, you may use `SdlInput` system for user input handling, which I believe is fine for almost all projects. If you do so, there appears a requirement to install `sdl2` shared library into your system, as well as nim-sdl bindings. Same for other systems, each of them has its own requirements. We'll cover this in depth later. 18 | 19 | Let's ensure that we can run just empty project. 20 | 21 | First, install latest `Cat 400` if you haven't done so. The following command will install latest version of libarary right from the repo: 22 | 23 | ``` 24 | nimble install https://github.com/c0ntribut0r/cat-400@#head 25 | ``` 26 | 27 | If you query for installed packages, you will see that `Cat 400` is named as `c4`, which is a shortand: 28 | 29 | ``` 30 | > nimble list -i | grep c4 31 | c4 [#head] 32 | ``` 33 | 34 | Test module import 35 | ------------------ 36 | 37 | Let's just ensure that the framework can be successfully imported: 38 | 39 | ```nim 40 | # test.nim 41 | import c4/processes 42 | 43 | 44 | when isMainModule: 45 | echo "C4 was successfully imported" 46 | ``` 47 | 48 | Compile the code with `nim c -r test` and ensure the message is successfully printed. 49 | 50 | Before going further, it's important to mention that you don't have to start each project from scratch - `cat 400` is shipped with some templates which allow you to immediately start coding your game without setting up a project. However, for learning purposes we'll cover creating brand new project from scratch. After this tutorial you should understand how the framework works and that there's no magic inside. 51 | 52 | > For every tutorial, you will find sources in [src](src/) folder. 53 | 54 | > Everything mentioned in the tutorials is a subject to change until v1.0 is released. Please open an issue if you find inconsistencies. 55 | 56 | Now you are ready for [Messages tutorial](../02%20-%20messages/readme.md) 57 | -------------------------------------------------------------------------------- /docs/tutorials/01 - project setup/src/main.nim: -------------------------------------------------------------------------------- 1 | # pingpong.nim 2 | import c4/logging 3 | 4 | 5 | when isMainModule: 6 | info "C4 was successfully imported" 7 | -------------------------------------------------------------------------------- /docs/tutorials/02 - messages/readme.md: -------------------------------------------------------------------------------- 1 | 2 | Messages 3 | ======== 4 | 5 | > Before reading this tutorial, it's highly recommended to read [the "Command" chapter](https://gameprogrammingpatterns.com/command.html) of Bob Nystrom's awesome "Game Programming Patterns" book. 6 | 7 | > `c4/messages` module does not depend on any other module and may be used separately in any project where messaging is required. 8 | 9 | --------------- 10 | 11 | Our ping pong game is quite straightforward: one moving ball and two paddles which should catch the ball. But under the hood there's a lot: 12 | * a physics system which is responsible for position, velocity and collision of box and paddles; 13 | * a video system which displays all game objects in a window; 14 | * an input system which scans key codes of your keyboard and makes a decision where to move the paddle; 15 | * AI which controls another paddle; 16 | * ... 17 | 18 | Most game engines combine these things and allow them live side-by-side. You may see such code: 19 | 20 | ```nim 21 | var 22 | # set up graphics stuff ... 23 | camera = Camera( 24 | position: vec3f(4, 2, 4), 25 | target: vec3f(0, 1.8, 0), 26 | up: vec3f(0, 1, 0), 27 | fovY: 60 28 | ) 29 | # ... and some user input ... 30 | mouseWheelMovement = 0 31 | mouseXRel = 0 32 | mouseYRel = 0 33 | evt: sdl2.Event 34 | # ... and a bit of game loop ... 35 | mainLoopRunning = true 36 | # ... and some video stuff again ... 37 | model: Model 38 | shader: Shader 39 | # ... and a timer 40 | startTime, endTime: float64 41 | fps: int 42 | ``` 43 | 44 | It's totally fine to mix everything when you're creating a small game, but it all becomes too tangled when you work on a big project. "Divide and rule" is one of core concepts of `Cat 400`, and different logical parts of application know nothing about each other and have no way to influence each other. Except by using `Message`s. 45 | 46 | `Message` is a primitive for sending a piece of information between two non-directly-related parts of application. For example, when position of ball changes, a message containing new position is sent to video system in order to redraw ball in another place. 47 | 48 | Under the hood, `Message` [is just an inheritable blank object](../../../c4/messages.nim). One should create custom messages by subclassing `Message` class or its predefined subclasses. 49 | 50 | > Since messages are heavily used for delivering information locally and over network, it's very important to make them as tiny as possible, thus saving bandwidth and memory. 51 | > 52 | > For example, you may represent object rotation using either rotation matrix (of size 4x3) or quaternion (of size 4x1), and of course the latter is better. 53 | 54 | Messages examples 55 | ----------------- 56 | 57 | Let's have a look at some messages you could create during development. 58 | 59 | For our ping-pong game, we want the paddle to move when player presses left or right arrow key. The message sent from `input` to `physics` system would look like 60 | 61 | ```nim 62 | import c4/messages 63 | 64 | 65 | type MovementDirection* = enum 66 | left, right 67 | 68 | type MoveMessage* = object of Message 69 | direction*: MovementDirection 70 | 71 | ``` 72 | 73 | It may be surprising that there's no field for saying _which_ entity we want to move. In fact physics system should already know which paddle belongs to player, so this info is redundant. Remember, try to keep messages as small as possible. 74 | 75 | Of course, if your game allows moving different entities with arrow keys, then you would include entity reference as well. It's up to you to think which messages you app needs and what they should store. 76 | 77 | Another example may be like this: 78 | 79 | ```nim 80 | type StartGameMessage* = object of Message 81 | ``` 82 | 83 | Yep, that's it. Sometimes the message _itself_ is enough to represent some information. In this case, receiving a `StartGameMessage` is enough to understand what should be done. 84 | 85 | Sending and processing messages 86 | ------------------------------- 87 | 88 | If using `c4/messages` module separately, it's up to you to decide how to use messages - creating references (`ref Message`) or plain objects (`Message`), storing them in some buffer/pipe or immediately processing after creation by calling some function. 89 | 90 | In `Cat 400`, all messages are created as references: 91 | 92 | ```nim 93 | var msg: ref Message 94 | 95 | msg = new(StartGameMessage) 96 | # or 97 | msg = (ref StartGameMessage)() 98 | 99 | msg = (ref MoveMessage)(direction: left) 100 | ``` 101 | 102 | There are several reasons to use reference types: 103 | 104 | 1. `ref Message` is a pointer, which is easy to send across systems on local machine. You don't have to copy entire message content, as you would do with just `Message` type. 105 | 106 | 2. Message may be sent to multiple destinations, and its lifetime is unknown - one system may not need the message anymore, while the other hasn't processed it yet. Using nim's garbage collection allows you to not care about message lifetime, which you wouldn't be able to achive with raw `ptr Message`. 107 | 108 | 3. Using `ref` type allows using dynamic dispatch (multimethods), so your methods may change their behaviour based on message type: 109 | 110 | ```nim 111 | method process*(msg: ref StartGameMessage) = 112 | echo "Starting game" 113 | 114 | method process*(msg: ref MoveMessage) = 115 | echo &"Moving {msg.direction}" 116 | ``` 117 | 118 | Serializing messages 119 | -------------------- 120 | 121 | When dealing with network, you need to serialize your message, send it, and then deserialize on the other side. `Cat 400` uses [msgpack4nim](https://github.com/jangko/msgpack4nim) library to serialize messages. 122 | 123 | By default, `msgpack4nim` does not include object type into serialized message, so if you pack `ref MoveMessage`, the unpacked type would be the base type - `ref Message`. 124 | 125 | `Cat 400` solves this problem by introducing `register` template. This template defines all required methods and procs for including message type into serialized string. Use `msgpack` and `msgunpack` procedures to serialize/deserialize messages: 126 | 127 | ```nim 128 | # src/messages.nim 129 | import c4/messages 130 | 131 | 132 | type RunMessage = object of Message 133 | data: string 134 | 135 | method name(msg: ref Message): string {.base.} = "Message" 136 | method name(msg: ref RunMessage): string = "RunMessage" 137 | 138 | var 139 | message: ref Message # here we will assign message subtype 140 | packed: string # here we will store packed message 141 | unpacked: ref Message # here we will unpack message 142 | 143 | register RunMessage # now we can serialize RunMessage 144 | 145 | message = (ref RunMessage)(data: "test data") # here message is of type `ref Message`, but in fact we assigned `ref RunMessage` to this variable 146 | assert message.name == "RunMessage" 147 | 148 | packed = message.msgpack() 149 | assert stringify(packed) == "1 [ \"test data\" ] " # `1` means that we packed message type (1 for RunMessage) together with message data 150 | 151 | # ... send `packed` over network / save it on disk / whatever ... 152 | 153 | # now it's time to restore the message 154 | unpacked = packed.msgunpack() # when unpacking, we use value `1` to understand that real runtime type of `unpacked` should be `ref RunMessage` 155 | assert unpacked of ref RunMessage # runtime type is preserved 156 | assert unpacked.name == "RunMessage" 157 | ``` 158 | 159 | > Don't forget to call `register CustomMessageType` on message types if you want to be able to resialize it. 160 | 161 | Now that you know how to work with messages, it's time to send them! Continue to [next tutorial](../03%20-%20processes%20and%20threads/readme.md). -------------------------------------------------------------------------------- /docs/tutorials/02 - messages/src/main.nim: -------------------------------------------------------------------------------- 1 | # src/messages.nim 2 | import c4/messages 3 | import c4/logging 4 | 5 | 6 | type RunMessage = object of Message 7 | data: string 8 | 9 | method name(msg: ref Message): string {.base.} = "Message" 10 | method name(msg: ref RunMessage): string = "RunMessage" 11 | 12 | var 13 | message: ref Message # here we will assign message subtype 14 | packed: string # here we will store packed message 15 | unpacked: ref Message # here we will unpack message 16 | 17 | register RunMessage # now we can serialize RunMessage 18 | 19 | message = (ref RunMessage)(data: "test data") # here message is of type `ref Message`, but in fact we assigned `ref RunMessage` to this variable 20 | assert message.name == "RunMessage" 21 | 22 | packed = message.msgpack() 23 | info "packed message", packed 24 | assert stringify(packed) == "1 [ \"test data\" ] " # `1` means that we packed message type (1 for RunMessage) together with message data 25 | 26 | # ... send `packed` over network / save it on disk / whatever ... 27 | 28 | # now it's time to restore the message 29 | unpacked = packed.msgunpack() # when unpacking, we use value `1` to understand that real runtime type of `unpacked` should be `ref RunMessage` 30 | info "unpacked message", unpacked 31 | assert unpacked of ref RunMessage # runtime type is preserved 32 | assert unpacked.name == "RunMessage" 33 | -------------------------------------------------------------------------------- /docs/tutorials/03 - processes and threads/src/processes_and_threads.nim: -------------------------------------------------------------------------------- 1 | # processes_and_threads.nim 2 | import c4/processes 3 | import c4/threads 4 | import c4/logging 5 | 6 | when isMainModule: 7 | const 8 | network = ThreadID(1) 9 | physics = ThreadID(2) 10 | video = ThreadID(3) 11 | 12 | info "running common code" 13 | 14 | spawnProcess "server": 15 | info "running server" 16 | 17 | spawnThread physics: 18 | info "running thread", threadID, threadName="physics" 19 | sleep 200 20 | 21 | spawnThread network: 22 | info "running thread", threadID, threadName="network" 23 | sleep 200 24 | 25 | joinActiveThreads() 26 | 27 | spawnProcess "client": 28 | info "running client" 29 | 30 | spawnThread video: 31 | info "running thread", threadID, threadName="physics" 32 | sleep 200 33 | 34 | spawnThread network: 35 | info "running thread", threadID, threadName="network" 36 | sleep 200 37 | 38 | joinActiveThreads() 39 | 40 | joinProcesses() 41 | info "all processes finished" 42 | -------------------------------------------------------------------------------- /docs/tutorials/03 - processes and threads/src/processes_creation.nim: -------------------------------------------------------------------------------- 1 | # processes_creation.nim 2 | import c4/processes 3 | import c4/logging 4 | 5 | info "running common piece of code", processName # for each process this will have its unique value 6 | 7 | # at this point we start new subprocess; 8 | # as mentioned earlier, every code before this line 9 | # will be executed in every subprocess 10 | spawnProcess "subprocess1": 11 | for i in 0..5: 12 | info "process payload", i 13 | sleep 100 14 | 15 | # everything before this line (except run("subprocess1") block) 16 | # will be executed in "subprocess2" process 17 | spawnProcess "subprocess2": 18 | for i in 0..5: 19 | info "process payload", i 20 | sleep 100 21 | 22 | info "only one process reaches this place", processName 23 | 24 | # wait for the processes to complete; 25 | # if one process is not running, others are force shut down 26 | joinProcesses() 27 | info "all processes completed" 28 | -------------------------------------------------------------------------------- /docs/tutorials/03 - processes and threads/src/threads_communication.nim: -------------------------------------------------------------------------------- 1 | # threads_communication.nim 2 | import strformat 3 | import times 4 | 5 | import c4/threads 6 | import c4/messages 7 | import c4/logging 8 | 9 | 10 | type 11 | DataMessage = object of Message 12 | data: int 13 | StopMessage = object of Message 14 | 15 | const 16 | thread1 = ThreadID(1) 17 | thread2 = ThreadID(2) 18 | 19 | # `recv()` and `tryRecv()` return `ref Message` type, 20 | # not `ref DataMessage` -> we need to use methods to 21 | # perform runtime type-specific actions, thus we define 22 | # `process()` and `value()` methods 23 | 24 | # these are methods for basic `Message` type 25 | method value(msg: ref Message): int {.base, gcsafe.} = 0 26 | method process(msg: ref Message) {.base, gcsafe.} = discard 27 | 28 | # and these are methods for `DataMessage` type 29 | method value(msg: ref DataMessage): int = msg.data 30 | method process(msg: ref DataMessage) = msg.data += 1 31 | 32 | 33 | when isMainModule: 34 | spawnThread thread1: 35 | # thread1 will wait for thread2 to appear by calling 36 | # `probe`; `probe` may accept `timeout` 37 | # arg (how many seconds to wait) and `interval` arg 38 | # (how often to check for thread) 39 | try: 40 | thread2.probe(timeout=initDuration(seconds=5)) 41 | except ThreadUnavailable: 42 | echo "Error: thread2 is not available" 43 | return 44 | 45 | # now we are sure that thread2 is running; 46 | # send message to thread2 47 | new(DataMessage).send(thread2) # by default, data == 0 48 | 49 | # wait for new message from thread2 50 | while true: 51 | # this will block execution until message received; 52 | # for non-blocking behaviour use `tryRecv()` 53 | let msg = channels[threadID].recv() 54 | 55 | info "received message", threadID, value=msg.value # print current thread name and message value 56 | 57 | msg.process() # increment message value 58 | if msg.value > 100: 59 | info "sending stop message", threadID 60 | new(StopMessage).send(thread2) 61 | info "stopping", threadID 62 | break # quit on condition 63 | 64 | msg.send(thread2) # send message to thread2 65 | 66 | spawnThread thread2: 67 | # this thread is spawned after thread1 68 | 69 | while true: 70 | let msg = channels[threadID].recv() # wait until message is received 71 | if msg of (ref StopMessage): 72 | info "received stop message", threadID 73 | info "stopping", threadID 74 | break 75 | 76 | info "received message", threadID, value=msg.value 77 | msg.process() # increment message value 78 | msg.send(thread1) 79 | 80 | joinActiveThreads() # wait for all threads 81 | -------------------------------------------------------------------------------- /docs/tutorials/03 - processes and threads/src/threads_creation.nim: -------------------------------------------------------------------------------- 1 | # threads_creation.nim 2 | import c4/threads 3 | import c4/logging 4 | 5 | const 6 | thread1 = ThreadID(1) 7 | thread2 = ThreadID(2) 8 | 9 | spawnThread thread1: 10 | for _ in 0..100: 11 | info "thread running", threadID # use `threadID` to get ID of currently running thread 12 | 13 | spawnThread thread2: 14 | for _ in 0..100: 15 | info "thread running", threadID 16 | 17 | joinActiveThreads() # call this to wait for all threads to complete 18 | -------------------------------------------------------------------------------- /docs/tutorials/04 - ecs/readme.md: -------------------------------------------------------------------------------- 1 | Entity-component system 2 | ======================= 3 | 4 | > Attention! Before reading this tutorial, it's highly recommended to read [the "Component" chapter](https://gameprogrammingpatterns.com/component.html) of Bob Nystrom's awesome "Game Programming Patterns" book. 5 | 6 | ECS is one of core parts of `C4`. It allows you to have entities and attach both predefined and custom components (objects) to it. 7 | 8 | Entity 9 | ------ 10 | 11 | ``Entity`` represents some game entity / object. It may be a tree, or wind, or player, or UI element - whatever you want to. 12 | 13 | Under the hood each entity is nothing but a number, which you may treat as entity ID. In `C4` it's just ``int16``, thus you may have up to ``65 535`` different entities with IDs from ``-32 768`` to ``32 767`` (except `0`). 14 | 15 | ``int16`` type was chosen because: 16 | * signed ints are checked for boundary errors, so if you try to create entity with ID ``32 767``, it won't be treated as ``-32 768`` - and you'll get an exception; 17 | * 16 bits is maximum for ``set`` type for efficiency reasons; could switch to `HashSet` type in case we ever need it 18 | 19 | Creating new entity 20 | ------------------- 21 | 22 | To create new entity, call ``newEntity()``. Entity ID will be the selected from unused IDs pool. If Entity limit is reached, an overflow exception will be thrown. 23 | 24 | ```nim 25 | # entities.nim 26 | import c4/entities 27 | 28 | let player = newEntity() 29 | echo "Player ID = " & $player # -32768 30 | while true: 31 | echo $newEntity() 32 | ``` 33 | 34 | ```sh 35 | Player ID = -32768 36 | ... 37 | 32764 38 | 32765 39 | 32766 40 | 32767 41 | /tmp/test.nim(4) test 42 | .../entities.nim(31) newEntity 43 | /usr/lib/nim/system/fatal.nim(39) sysFatal 44 | Error: unhandled exception: over- or underflow [OverflowError] 45 | ``` 46 | 47 | > The ID of newly created entity may be _any_ of unused IDs, not necessary _smallest_ unused ID. So don't rely on IDs order. 48 | 49 | Also, entity ID `0` is reserved for "non-initialized" entity, so that one may perform `isInitialized` check: 50 | 51 | ```nim 52 | var player: Entity # oops: forgot to initialize player, i.e. call `newEntity()` 53 | 54 | assert not player.isInitialized 55 | ``` 56 | 57 | Components 58 | ---------- 59 | 60 | Component is some value or object instance that you can attach to / retrieve from `Entity`. For example, our `player` entity may have `Health` and `Inventory` components, and we may also add a `chest` entity with only `Inventory` components (chests don't need health component, unless you wanna make chests breakable in your game). 61 | 62 | Here are rules for components: 63 | * Each entity may have as many types of components as you wish. For example, `player` may have `Health`, `Inventory`, `Spells`, `Diseases`, `Sprite`, `Sound`, `Animation`, `WalkState` and more, depending on your needs. 64 | * Each entity may have only one component of specific type. `player` can have no `Health` component, can have one `Health` component, but never two or more. 65 | 66 | Now we gonna create `Health` component for our game: 67 | 68 | ```nim 69 | # components.nim 70 | import strformat 71 | import c4/entities 72 | 73 | 74 | type Health = object 75 | value: uint8 76 | ``` 77 | 78 | It's recommended to define components as objects. Here health is just `uint8`, but when you game grows you'll definitely need to add something to your `Health` component, and it would be easier to do so if `Health` is already an object. 79 | 80 | Nothing restricts you from defining `Health` as `distinct int` type. It could also be `ref object`, which may be handy if you want to inherit `Health` and redefine some of its methods. 81 | 82 | > Tip: one can use plain `string` component type for storing entity name, i.e. `entity[string] = "player 1"`. 83 | 84 | Under the hood, a separate ``Table[Entity, ]`` is created for each component type, which can be accessed using `getComponents()`. 85 | 86 | Playing with entities 87 | --------------------- 88 | 89 | Now it's time to create a player. Since `Entity` is just int and usually should not be changed, it's a good practice to use `let` to show and force immutability. 90 | 91 | ```nim 92 | let player = newEntity() 93 | let chest = newEntity() 94 | ``` 95 | 96 | To check whether some entity has a specific component, use `has(entity, )` template: 97 | 98 | ```nim 99 | proc printHealth(self: Entity) = 100 | if self.has(Health): 101 | echo &"Entity {self} health: {self[Health].value}" 102 | 103 | else: 104 | echo &"Entity {self} has no component!" 105 | ``` 106 | 107 | To attach new component to entity, use `[]=` template: 108 | 109 | ```nim 110 | player[Health] = Health(value: 100) 111 | player.printHealth() # Entity -32768 health: 100 112 | chest.printHealth() # Entity -32767 has no component! 113 | ``` 114 | 115 | Define a proc on `Health` component and use it: 116 | 117 | ```nim 118 | proc poison(self: var Health) = 119 | self.value -= 10 120 | 121 | player[Health].poison() 122 | player.printHealth() # Entity -32768 health: 90 123 | ``` 124 | 125 | Or access component attribute directly (note that it works because `Health` is defined in the same module; if it wasn't you'd have to use `*` symbol in attribute definition, i.e. `value*: uint16`, or define a getter proc): 126 | ```nim 127 | player[Health].value = 100 128 | player.printHealth() # Entity -32768 health: 100 129 | ``` 130 | 131 | To get all entity-component pairs for specific component type: 132 | 133 | ```nim 134 | for entity, health in getComponents(Health): 135 | echo &"Entity {entity} has health {health.value}" 136 | ``` 137 | 138 | To delete a component, use `del(entity, )` template, or replace it with other component, or delete the entity completely - all these cases will remove previous component: 139 | 140 | ```nim 141 | player[Health] = Health() 142 | # or 143 | player.del(Health) 144 | # or 145 | player.delete() 146 | 147 | echo "Deleted player and all components" 148 | player.printHealth() # Entity -32768 has no component! 149 | ``` 150 | 151 | > Note that `del` and `delete` are not the same. `del` removes component from entity, while `delete` removes entity completely. As you may see from example above, `player` variable is still available after `delete()` (it's just integer after all), but now `newEntity()` may reuse former player's ID. Don't use an entity after you called `delete()` on it. 152 | 153 | Delete it the right way 154 | ----------------------- 155 | 156 | Deleting a component directly or via deleting entire entity does only one thing: removes the component from components table. If you are using `ref` or `ptr` type as a component, removing it from some components table **will not** destroy it immediately - it's up to garbage collector (in case of `ref`) or you (in case of `ptr`) to decide that the object is not needed and may be deleted. Garbage collection doesn't guarantee you *when* the object will be really deleted from memory. 157 | 158 | So, for example, if your `ref`/`ptr` component represents something in a scene graph, you better call custom `dispose()` proc before deleting it: 159 | 160 | ```nim 161 | tree = newEntity() 162 | tree[ref Video] = new(TreeVideo) 163 | 164 | # ... 2 hours later ... 165 | 166 | # time to delete the tree 167 | tree[ref Video].dispose() # this removes the tree from screen, deinitializes textures etc, i.e. does everything a good destructor would do 168 | 169 | # now we don't care how long the component will remain in memory, one day GC will take care of it 170 | tree.del(ref Video) 171 | ``` 172 | 173 | Next 174 | ---- 175 | 176 | Ready to learn [systems](../05%20-%20systems/readme.md)? 177 | -------------------------------------------------------------------------------- /docs/tutorials/04 - ecs/src/components.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import c4/entities 3 | 4 | 5 | type Health = object 6 | value: uint8 7 | 8 | let player = newEntity() 9 | let chest = newEntity() 10 | 11 | proc printHealth(self: Entity) = 12 | if self.has(Health): 13 | echo &"Entity {self} health: {self[Health].value}" 14 | else: 15 | echo &"Entity {self} has no component!" 16 | 17 | player[Health] = Health(value: 100) 18 | player.printHealth() # Entity -32768 health: 100 19 | chest.printHealth() # Entity -32767 has no component! 20 | 21 | proc poison(self: var Health) = 22 | self.value -= 10 23 | 24 | player[Health].poison() 25 | player.printHealth() # Entity -32768 health: 90 26 | 27 | player[Health].value = 100 28 | player.printHealth() 29 | 30 | for entity, health in getComponents(Health): 31 | echo &"Entity {entity} has health component with value {health.value}" 32 | 33 | # player[Health] = Health() 34 | # or: 35 | # player.del(Health) 36 | # or delete entire entity and all its components: 37 | player.delete() 38 | 39 | echo "Deleted player and all components" 40 | player.printHealth() # Entity -32768 has no component! -------------------------------------------------------------------------------- /docs/tutorials/04 - ecs/src/entities.nim: -------------------------------------------------------------------------------- 1 | # entities.nim 2 | import c4/entities 3 | 4 | let player = newEntity() 5 | echo "Player ID = " & $player # -32768 6 | while true: 7 | echo $newEntity() 8 | -------------------------------------------------------------------------------- /docs/tutorials/05 - systems/readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Systems 3 | 4 | ## What is a system? 5 | 6 | `System` is a large piece of code which is responsible for one global part of the game. Examples: 7 | 8 | * Input system - reads user input (keyboard / mouse) 9 | * Video system - draws image on screen 10 | * Audio system - plays sounds 11 | * Network system - connects players and server 12 | * Physics system - simulates game world 13 | 14 | In `Cat 400`, all systems are completely independent and know nothing about each other except names. 15 | 16 | ## How systems work 17 | 18 | There's no restriction on how your system should look like. However, there are some conventions that `c4` follows itself and you are encouraged to follow them too. 19 | 20 | ### Definition 21 | 22 | Each system is an object which encapsulates all information inside its private fields: 23 | 24 | ```nim 25 | import c4/entities 26 | 27 | 28 | type PhysicsSystem* = object 29 | boxes: set[Entity] 30 | player: Entity 31 | # whatever else 32 | ``` 33 | 34 | > As you may see, we don't store `boxes`, `player` and other resources in global scope. If we did it, these structures would be initialized at module import, which is unnecessary side effect: your program may import the module but never use its global variables. Also global variables won't allow you to create multiple instances of system, just in case you need it. 35 | 36 | ### Initialization 37 | 38 | The `init` method initializes all internal structures of the system. 39 | 40 | ```nim 41 | method init*(self: ref PhysicsSystem) = 42 | self.boxes = @[newEntity(), newEntity()] 43 | self.player = newEntity() 44 | # ... 45 | ``` 46 | 47 | ### Updating 48 | 49 | Use `update` method to update internal state of the system according to delta time (in seconds) since last update. 50 | 51 | ```nim 52 | method update*(self: ref PhysicsSystem, dt: float) = 53 | var playerPhysics = self.player[ref Physics] 54 | playerPhysics.position.x += playerPhysics.velocity.x * dt 55 | # ... 56 | ``` 57 | 58 | ### Disposal 59 | 60 | `dispose` method frees resources and terminates the system. 61 | 62 | ```nim 63 | method dispose*(self: ref PhysicsSystem) = 64 | getComponents[ref Physics].clear() 65 | # ... 66 | ``` 67 | 68 | ### Message processing 69 | 70 | System should be able to process incoming messages. By convention, systems have `process` methods for message handling: 71 | 72 | ```nim 73 | import c4/messages 74 | 75 | 76 | method process(self: ref PhysicsSystem, message: ref Message) {.base.}: 77 | # this is a general method which will capture all messages which don't have specific `process` method; it's a good practice to emit a warning here 78 | logging.warn(&"No rule for processing {message}") 79 | # nothing is done, i.e. message is ignored 80 | 81 | 82 | method process(self: ref PhysicsSystem, message: ref MoveMessage) = 83 | # this is an example of processing specific message 84 | var physics = message.entity[ref Physics] 85 | physics.velocity = message.direction * movementSpeed 86 | # ... 87 | ``` 88 | 89 | ### Running a system 90 | 91 | There's a `loop` template which runs your code with specific frequency and provides a `dt` variable (delta time in seconds): 92 | 93 | ```nim 94 | import strformat 95 | import c4/loop 96 | 97 | var i = 0 98 | loop(frequency=30): 99 | echo &"Current frequency: {1/dt}/s" 100 | i += 1 101 | if i > 100: 102 | break # use `break` to quit the loop 103 | ``` 104 | 105 | > Use `frequency=0` to run at max possible frequency. 106 | 107 | Systems have `run` proc which is usually quite straightforward - it updates the system and processes all pending messages. 108 | 109 | > Of course you're not restricted to use this logic, change it if you need different behavior. 110 | 111 | ```nim 112 | import c4/loop 113 | import c4/threads 114 | 115 | 116 | proc run*(self: ref PhysicsSystem) = 117 | loop(frequency=30) do: 118 | 119 | # update the system 120 | self.update(dt) 121 | 122 | # process all messages 123 | while true: 124 | let message = channel.tryRecv() 125 | if message.isNil: 126 | break 127 | self.process(message) 128 | ``` 129 | 130 | Creating a simple system 131 | ------------------------ 132 | 133 | Let's create a demo system which will do some useless thing - output fps (frames per second) based on delta time before previous and current game loop steps. 134 | 135 | `Cat 400` encourages you to use unified directories structure. Create a folder `systems` and create new system file `fps.nim` there: 136 | 137 | ```nim 138 | # systems/fps.nim 139 | import strformat 140 | 141 | import c4/loop 142 | 143 | # define new system 144 | type FpsSystem* = object # just some object, no inheritance needed 145 | # with custom field 146 | worstFps: int 147 | 148 | 149 | proc init*(self: var FpsSystem) = 150 | self.worstFps = 0 151 | 152 | proc run*(self: var FpsSystem) = 153 | var i = 0 154 | loop(frequency=60): 155 | # calculate fps 156 | let fps = (1 / dt).int 157 | 158 | # update custom field 159 | if fps > self.worstFps: 160 | self.worstFps = fps 161 | 162 | # use c4's logging system to output message 163 | echo &"FPS: {$fps}" 164 | inc i 165 | if i > 100: 166 | break 167 | ``` 168 | 169 | Here we create a `FpsSystem` which is just an object. All it does is display current fps and store worst result in internal field. 170 | 171 | Now let's run the framework. In order to do this, we'll just use `c4/threads` and `c4/processes` modules. In this tutorial we gonna run our `FpsSystem` on server process: 172 | 173 | ```nim 174 | # main.nim 175 | import threadpool 176 | import strformat 177 | import c4/threads 178 | import c4/processes 179 | 180 | # import our newly created system 181 | import systems/fps 182 | 183 | when isMainModule: 184 | run("server"): 185 | spawnThread("fps"): 186 | echo &" - Thread {threadName}" 187 | var system = FpsSystem() 188 | system.init() 189 | system.run() 190 | 191 | sync() 192 | ``` 193 | 194 | It's fine to call system `"fps"` for this example project, but in real projects you should use something more meaningful, like `"video"` or `"network"`. There's no restrictions on how much and which systems you have. 195 | 196 | Now compile and run the code: 197 | 198 | ``` 199 | > nim c -r main.nim 200 | ... 201 | FPS: 61 202 | FPS: 61 203 | FPS: 61 204 | FPS: 62 205 | FPS: 61 206 | FPS: 62 207 | FPS: 61 208 | FPS: 61 209 | FPS: 61 210 | FPS: 61 211 | FPS: 61 212 | FPS: 62 213 | FPS: 61 214 | FPS: 61 215 | FPS: 61 216 | FPS: 61 217 | FPS: 61 218 | FPS: 61 219 | FPS: 61 220 | ``` 221 | 222 | Our system is successfully running at 61 fps. Why not at 60? I have no clue. 223 | 224 | Congratulations! 225 | 226 | Let's go through built-in systems which will help you to build your first game. It's exciting to not only code but also see a result of your efforts, so let's start with at least showing some window: [input system](../06%20-%20video%20system/readme.md). -------------------------------------------------------------------------------- /docs/tutorials/05 - systems/src/main.nim: -------------------------------------------------------------------------------- 1 | # main.nim 2 | import c4/threads 3 | import c4/processes 4 | import c4/logging 5 | import c4/systems 6 | 7 | # import our newly created system 8 | import systems/fps 9 | 10 | when isMainModule: 11 | spawnProcess "server": 12 | spawnThread ThreadID(1): 13 | info "running thread" 14 | var system = new(FpsSystem) 15 | system.run(frequency=30) 16 | 17 | joinActiveThreads() 18 | 19 | joinProcesses() 20 | -------------------------------------------------------------------------------- /docs/tutorials/05 - systems/src/systems/fps.nim: -------------------------------------------------------------------------------- 1 | # systems/fps.nim 2 | import c4/loop 3 | import c4/systems 4 | import c4/logging 5 | 6 | # define new system 7 | type FpsSystem* = object of System 8 | # with custom field 9 | i: int 10 | bestFps: int 11 | 12 | method update*(self: ref FpsSystem, dt: float) = 13 | 14 | # calculate fps 15 | let fps = (1 / dt).int 16 | 17 | # update custom field 18 | if fps > self.bestFps: 19 | self.bestFps = fps 20 | 21 | # use c4's logging system to output message 22 | info "fps measured", value=fps, i=self.i 23 | 24 | inc self.i 25 | if self.i > 15: 26 | raise newException(BreakLoopException, "") 27 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/2d/main.nim: -------------------------------------------------------------------------------- 1 | # main.nim 2 | import sdl2 3 | 4 | import c4/threads as c4threads 5 | import c4/processes 6 | import c4/systems 7 | import c4/systems/video/sdl as c4sdl 8 | import c4/logging 9 | 10 | 11 | import systems/video 12 | 13 | 14 | when isMainModule: 15 | spawnProcess "server": 16 | spawnThread c4threads.ThreadID(1): 17 | var videoSystem = video.VideoSystem.new() 18 | 19 | videoSystem.process( 20 | (ref VideoInitMessage)( 21 | windowTitle: "My awesome game", 22 | windowWidth: 640, 23 | windowHeight: 480, 24 | windowX: SDL_WINDOWPOS_CENTERED, 25 | windowY: SDL_WINDOWPOS_CENTERED, 26 | flags: (SDL_WINDOW_SHOWN or SDL_WINDOW_RESIZABLE or SDL_WINDOW_OPENGL).uint32, 27 | ) 28 | ) 29 | 30 | videoSystem.run(frequency=60) 31 | 32 | joinActiveThreads() 33 | 34 | joinProcesses() 35 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/2d/systems/video.nim: -------------------------------------------------------------------------------- 1 | import std/times 2 | import math 3 | 4 | import sdl2 5 | 6 | import c4/logging 7 | import c4/systems/video/sdl 8 | 9 | 10 | type VideoSystem* = object of sdl.VideoSystem 11 | 12 | 13 | method update*(self: ref VideoSystem, dt: float) = 14 | if self.renderer.clear() != 0: handleError("failed to clear renderer") 15 | 16 | let windowSize = self.window.getSize() 17 | let rectSize = int(400 * abs(sin(epochTime()))) 18 | var rectangle = rect( 19 | x=(windowSize.x/2-rectSize/2).cint, 20 | y=(windowSize.y/2-rectSize/2).cint, 21 | w=rectSize.cint, 22 | h=rectSize.cint, 23 | ) 24 | if self.renderer.setDrawColor(255, 255, 255, 255) != SdlSuccess: handleError("failed to set renderer draw color") 25 | if self.renderer.drawRect(rectangle.addr) != SdlSuccess: handleError("failed to draw rectangle") 26 | 27 | if self.renderer.setDrawColor(0, 0, 0, 255) != SdlSuccess: handleError("failed to set renderer draw color") 28 | self.renderer.present() 29 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/consts.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | 3 | const videoThread* = ThreadID(1) 4 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/main.nim: -------------------------------------------------------------------------------- 1 | # main.nim 2 | import sdl2 3 | 4 | import c4/threads as c4threads 5 | import c4/processes 6 | import c4/systems 7 | import c4/systems/video/ogre 8 | import c4/logging 9 | 10 | import systems/video 11 | import messages 12 | import consts 13 | 14 | 15 | 16 | when isMainModule: 17 | spawnProcess "server": 18 | spawnThread videoThread: 19 | var videoSystem = video.VideoSystem.new() 20 | 21 | # this will be processed immediately 22 | videoSystem.process( 23 | (ref VideoInitMessage)( 24 | windowTitle: "My awesome game", 25 | windowWidth: 640, 26 | windowHeight: 480, 27 | windowX: SDL_WINDOWPOS_CENTERED, 28 | windowY: SDL_WINDOWPOS_CENTERED, 29 | flags: (SDL_WINDOW_SHOWN or SDL_WINDOW_RESIZABLE or SDL_WINDOW_OPENGL).uint32, 30 | ) 31 | ) 32 | 33 | # this will be processed on first loop iteration 34 | (ref CreateEntityMessage)(x: 0, y: 0, z: -130).send(videoThread) 35 | 36 | videoSystem.run(frequency=60) 37 | 38 | joinActiveThreads() 39 | 40 | joinProcesses() 41 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/messages 2 | 3 | 4 | type 5 | CreateEntityMessage* = object of Message 6 | x*: float 7 | y*: float 8 | z*: float 9 | 10 | RotateEntityMessage* = object of Message 11 | angle*: float 12 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/nim.cfg: -------------------------------------------------------------------------------- 1 | --backend:cpp 2 | -t:"-I/usr/include/OGRE -I/usr/include/OGRE/Bites" 3 | -l:"-lSDL -lOgreMain" 4 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/plugins.cfg: -------------------------------------------------------------------------------- 1 | # Defines plugins to load 2 | 3 | # Define plugin folder 4 | PluginFolder=/usr/lib/OGRE 5 | 6 | # Define plugins 7 | # Plugin=RenderSystem_Direct3D9 8 | # Plugin=RenderSystem_Direct3D11 9 | Plugin=RenderSystem_GL 10 | Plugin=RenderSystem_GL3Plus 11 | Plugin=RenderSystem_GLES2 12 | # Plugin=RenderSystem_Metal 13 | # Plugin=RenderSystem_Tiny 14 | # Plugin=RenderSystem_Vulkan 15 | Plugin=Plugin_ParticleFX 16 | Plugin=Plugin_BSPSceneManager 17 | # Plugin=Plugin_CgProgramManager 18 | # Plugin=Plugin_GLSLangProgramManager 19 | # Plugin=Codec_EXR 20 | Plugin=Codec_STBI 21 | # Plugin=Codec_RsImage 22 | # Plugin=Codec_FreeImage 23 | Plugin=Plugin_PCZSceneManager 24 | Plugin=Plugin_OctreeZone 25 | Plugin=Plugin_OctreeSceneManager 26 | Plugin=Plugin_DotScene 27 | # Plugin=Codec_Assimp 28 | -------------------------------------------------------------------------------- /docs/tutorials/06 - video system/src/3d/systems/video.nim: -------------------------------------------------------------------------------- 1 | import std/math 2 | import std/times 3 | 4 | import c4/logging 5 | import c4/systems/video/ogre as c4ogre 6 | import c4/lib/ogre/ogre 7 | import c4/messages 8 | import c4/entities 9 | import c4/sugar 10 | import c4/threads 11 | 12 | import ../messages 13 | import ../consts 14 | 15 | type 16 | VideoSystem* = object of c4ogre.VideoSystem 17 | entity*: entities.Entity 18 | Video* = object of c4ogre.Video 19 | 20 | 21 | method update*(self: ref VideoSystem, dt: float) = 22 | ((ref RotateEntityMessage)(angle: sin(epochTime()))).send(videoThread) 23 | procCall self.as(ref c4ogre.VideoSystem).update(dt) 24 | 25 | 26 | method process*(self: ref VideoSystem, message: ref VideoInitMessage) = 27 | procCall self.as(ref c4ogre.VideoSystem).process(message) 28 | 29 | var cameraNode = self.sceneManager.getRootSceneNode().createChildSceneNode() 30 | cameraNode.attachObject(self.camera) 31 | cameraNode.setPosition(0.0, 0.0, 0.0) 32 | cameraNode.lookAt(targetPoint=Vector3(x: 0.0, y: 0.0, z: -300.0), relativeTo=TS_LOCAL) 33 | 34 | self.sceneManager.setAmbientLight(initColourValue(0.5, 0.5, 0.5)) 35 | 36 | var light = self.sceneManager.createLight("MainLight"); 37 | var lightNode = self.sceneManager.getRootSceneNode().createChildSceneNode() 38 | lightNode.attachObject(light) 39 | lightNode.setPosition(20.0, 80.0, 50.0) 40 | 41 | withLog(DEBUG, "loading resources"): 42 | let mediaDir = "/usr/share/OGRE-14.0/Media" 43 | self.resourceGroupManager.addResourceLocation(cstring(mediaDir & "/packs/SdkTrays.zip"), "Zip", resGroup="Essential") 44 | self.resourceGroupManager.addResourceLocation(cstring(mediaDir & "/models"), "FileSystem", resGroup="General") 45 | self.resourceGroupManager.addResourceLocation(cstring(mediaDir & "/materials/textures"), "FileSystem", resGroup="General") 46 | self.resourceGroupManager.addResourceLocation(cstring(mediaDir & "/materials/scripts"), "FileSystem", resGroup="General") 47 | self.resourceGroupManager.initialiseAllResourceGroups() 48 | 49 | 50 | method process*(self: ref VideoSystem, message: ref CreateEntityMessage) = 51 | var node = self.sceneManager.getRootSceneNode().createChildSceneNode() 52 | node.setPosition(message.x.Real, message.y.Real, message.z.Real) 53 | 54 | var mesh = self.sceneManager.createEntity("ogrehead.mesh") 55 | if mesh.isNil: 56 | fatal "mesh loading failure" 57 | return 58 | 59 | node.attachObject(mesh) 60 | 61 | self.entity = newEntity() 62 | self.entity[Video] = Video(node: node) 63 | 64 | 65 | method process*(self: ref VideoSystem, message: ref RotateEntityMessage) = 66 | self.entity[Video].node.yaw(initRadian(message.angle.Real / 4)) 67 | -------------------------------------------------------------------------------- /docs/tutorials/07 - input system/readme.md: -------------------------------------------------------------------------------- 1 | # Input system 2 | 3 | `Cat-400` uses SDL as a library for displaying window and capturing user input. It is a cross-platform library, so it can be used on Windows, Linux and MacOS. 4 | 5 | -------------------------------------------------------------------------------- /docs/tutorials/07 - input system/src/main.nim: -------------------------------------------------------------------------------- 1 | # main.nim 2 | import sdl2 3 | 4 | import c4/threads as c4threads 5 | import c4/systems 6 | import c4/systems/video/sdl as sdlvideo 7 | import c4/systems/input/sdl as sdlinput 8 | import c4/logging 9 | 10 | import ./systems/input 11 | import ./systems/video 12 | 13 | 14 | when isMainModule: 15 | spawnThread c4threads.ThreadID(1): 16 | var videoSystem = new(video.VideoSystem) 17 | 18 | videoSystem.process( 19 | (ref VideoInitMessage)( 20 | windowTitle: "My awesome game", 21 | windowWidth: 640, 22 | windowHeight: 480, 23 | windowX: SDL_WINDOWPOS_CENTERED, 24 | windowY: SDL_WINDOWPOS_CENTERED, 25 | flags: (SDL_WINDOW_SHOWN or SDL_WINDOW_RESIZABLE or SDL_WINDOW_OPENGL).uint32, 26 | ) 27 | ) 28 | 29 | videoSystem.run(frequency=60) 30 | 31 | spawnThread c4threads.ThreadID(2): 32 | var inputSystem = new(input.InputSystem) 33 | inputSystem.process(new(InputInitMessage)) 34 | inputSystem.run(frequency=30) 35 | 36 | joinActiveThreads() 37 | -------------------------------------------------------------------------------- /docs/tutorials/07 - input system/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/messages 2 | 3 | type StopMessage* = object of Message -------------------------------------------------------------------------------- /docs/tutorials/07 - input system/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | 3 | import c4/systems/input/sdl 4 | import c4/logging 5 | import c4/sugar 6 | import c4/loop 7 | import c4/threads as c4threads 8 | 9 | import ../messages 10 | 11 | 12 | type 13 | InputSystem* = object of sdl.InputSystem 14 | 15 | 16 | method handleKeyboardState*( 17 | self: ref InputSystem, 18 | keyboard: ptr array[0 .. SDL_NUM_SCANCODES.int, uint8], 19 | ) = 20 | 21 | var keys = "" 22 | if keyboard[SDL_SCANCODE_W.int] > 0: keys.add "W" 23 | if keyboard[SDL_SCANCODE_S.int] > 0: keys.add "S" 24 | if keyboard[SDL_SCANCODE_A.int] > 0: keys.add "A" 25 | if keyboard[SDL_SCANCODE_D.int] > 0: keys.add "D" 26 | 27 | if keys.len > 0: 28 | info "keyboard input", keys 29 | 30 | if keyboard[SDL_SCANCODE_ESCAPE.int] > 0: 31 | new(StopMessage).send(c4threads.ThreadID(1)) 32 | info "quit" 33 | raise newException(BreakLoopException, "") 34 | 35 | method handleEvent*(self: ref InputSystem, event: Event) = 36 | procCall self.as(ref sdl.InputSystem).handleEvent(event) 37 | 38 | 39 | case event.kind 40 | of MOUSEMOTION: 41 | var x, y: cint 42 | discard getRelativeMouseState(x, y) 43 | if x != 0 or y != 0: 44 | info "mouse motion", x, y 45 | else: 46 | discard 47 | -------------------------------------------------------------------------------- /docs/tutorials/07 - input system/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/video/sdl 2 | import c4/loop 3 | 4 | import ../messages 5 | 6 | 7 | type VideoSystem* = object of sdl.VideoSystem 8 | 9 | 10 | method process*(self: ref VideoSystem, message: ref StopMessage) = 11 | raise newException(BreakLoopException, "") 12 | -------------------------------------------------------------------------------- /docs/tutorials/08 - physics system/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xE111/cat-400/df9f2fea2430ffca8461279ca93a7a00c7c9a763/docs/tutorials/08 - physics system/readme.md -------------------------------------------------------------------------------- /docs/tutorials/08 - physics system/src/main.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | import c4/systems 3 | import c4/systems/physics/ode 4 | import c4/logging 5 | 6 | import systems/physics 7 | 8 | 9 | when isMainModule: 10 | spawnThread ThreadID(1): 11 | let system = new(physics.PhysicsSystem) 12 | system.process(new(PhysicsInitMessage)) # process immediately 13 | new(CreateEntityMessage).send(threadID) # process in system loop 14 | system.run(frequency=30) 15 | 16 | joinActiveThreads() 17 | -------------------------------------------------------------------------------- /docs/tutorials/08 - physics system/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import c4/systems/physics/ode 2 | import c4/messages 3 | import c4/entities 4 | import c4/lib/ode/ode as libode 5 | import c4/logging 6 | import c4/sugar 7 | 8 | 9 | type 10 | PhysicsSystem* = object of ode.PhysicsSystem 11 | entity: Entity 12 | CreateEntityMessage* = object of Message 13 | 14 | method process*(self: ref PhysicsSystem, message: ref PhysicsInitMessage) = 15 | procCall self.as(ref ode.PhysicsSystem).process(message) 16 | self.world.worldSetGravity(0, -9.81, 0) 17 | 18 | method process*(self: ref PhysicsSystem, message: ref CreateEntityMessage) = 19 | withLog(DEBUG, "creating new entity"): 20 | self.entity = newEntity() 21 | 22 | let body = self.world.bodyCreate() 23 | body.bodySetPosition(0.0, 0.0, 0.0) 24 | 25 | self.entity[Physics] = Physics(body: body) 26 | 27 | method update*(self: ref PhysicsSystem, dt: float) = 28 | procCall self.as(ref ode.PhysicsSystem).update(dt) 29 | 30 | if self.entity.isInitialized(): 31 | let position = self.entity[Physics].body.bodyGetPosition()[] 32 | info "tracking body position", entity=self.entity, position 33 | -------------------------------------------------------------------------------- /docs/tutorials/09 - network system/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xE111/cat-400/df9f2fea2430ffca8461279ca93a7a00c7c9a763/docs/tutorials/09 - network system/readme.md -------------------------------------------------------------------------------- /docs/tutorials/09 - network system/src/main.nim: -------------------------------------------------------------------------------- 1 | import c4/systems 2 | import c4/systems/network/net 3 | import c4/systems/video/sdl as sdl_video 4 | import c4/systems/input/sdl as sdl_input 5 | import c4/processes 6 | import c4/logging 7 | import c4/threads 8 | 9 | import sdl2 10 | 11 | import ./systems/input 12 | import ./threads as thread_names 13 | 14 | when isMainModule: 15 | 16 | spawnProcess "server": 17 | 18 | spawnThread networkThread: 19 | threadName = "network" 20 | 21 | var network = new(ServerNetworkSystem) 22 | network.process((ref ServerInitMessage)(port: 6543)) 23 | network.run() 24 | 25 | joinActiveThreads() 26 | 27 | spawnProcess "client": 28 | 29 | spawnThread networkThread: 30 | threadName = "network" 31 | 32 | var network = new(ClientNetworkSystem) 33 | network.process(new(ClientInitMessage)) 34 | network.run() 35 | 36 | spawnThread videoThread: 37 | threadName = "video" 38 | 39 | var videoSystem = new(VideoSystem) 40 | videoSystem.process( 41 | (ref VideoInitMessage)( 42 | windowTitle: "My awesome game", 43 | windowWidth: 640, 44 | windowHeight: 480, 45 | windowX: SDL_WINDOWPOS_CENTERED, 46 | windowY: SDL_WINDOWPOS_CENTERED, 47 | flags: (SDL_WINDOW_SHOWN or SDL_WINDOW_RESIZABLE or SDL_WINDOW_OPENGL).uint32, 48 | ) 49 | ) 50 | videoSystem.run(frequency=60) 51 | 52 | spawnThread inputThread: 53 | threadName = "input" 54 | 55 | var inputSystem = new(input.InputSystem) 56 | inputSystem.process(new(InputInitMessage)) 57 | inputSystem.run(frequency=30) 58 | 59 | joinActiveThreads() 60 | 61 | joinProcesses() 62 | -------------------------------------------------------------------------------- /docs/tutorials/09 - network system/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | 3 | import c4/systems/input/sdl 4 | import c4/systems/network/net 5 | import c4/logging 6 | import c4/messages 7 | import c4/threads 8 | 9 | import ../threads as thread_names 10 | 11 | 12 | type 13 | InputSystem* = object of sdl.InputSystem 14 | connectionMessageSent: bool = false 15 | 16 | InputMessage* = object of NetworkMessage 17 | keys*: string 18 | 19 | 20 | InputMessage.register() 21 | 22 | 23 | method handleKeyboardState*( 24 | self: ref InputSystem, 25 | keyboard: ptr array[0 .. SDL_NUM_SCANCODES.int, uint8], 26 | ) = 27 | 28 | if keyboard[SDL_SCANCODE_C.int] > 0 and not self.connectionMessageSent: 29 | info "keyboard input - connection request" 30 | (ref ConnectMessage)(host: "127.0.0.1", port: 6543).send(networkThread) 31 | self.connectionMessageSent = true 32 | 33 | var keys = "" 34 | if keyboard[SDL_SCANCODE_W.int] > 0: keys.add "W" 35 | if keyboard[SDL_SCANCODE_S.int] > 0: keys.add "S" 36 | if keyboard[SDL_SCANCODE_A.int] > 0: keys.add "A" 37 | if keyboard[SDL_SCANCODE_D.int] > 0: keys.add "D" 38 | 39 | if keys.len > 0: 40 | info "keyboard input", keys 41 | (ref InputMessage)(keys: keys).send(networkThread) 42 | -------------------------------------------------------------------------------- /docs/tutorials/09 - network system/src/threads.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | 3 | const 4 | networkThread* = ThreadID(1) 5 | inputThread* = ThreadID(2) 6 | videoThread* = ThreadID(3) 7 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/readme.md: -------------------------------------------------------------------------------- 1 | 1) network waits for video & input to become available 2 | 2) network connects to server 3 | 3) client sends "PlayerReadyMessage" 4 | 4) physics sends initial setup 5 | 5) on any key press -> game start 6 | 6) on failure / success -> game freeze, result message, step 4 7 | 7) -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/main.nim: -------------------------------------------------------------------------------- 1 | import times 2 | 3 | import c4/processes 4 | import c4/threads 5 | import c4/logging 6 | 7 | import c4/systems 8 | import c4/systems/video/sdl as sdl_video 9 | import c4/systems/network/net 10 | import c4/systems/input/sdl as sdl_input 11 | import c4/systems/physics/simple 12 | 13 | import ./threads 14 | import ./systems/video 15 | import ./systems/network 16 | import ./systems/input 17 | import ./systems/physics 18 | 19 | import ./scenarios/master 20 | 21 | 22 | when isMainModule: 23 | 24 | spawnProcess "server": 25 | 26 | spawnThread physicsThread: 27 | threadName = "physics" 28 | 29 | var physics = new(physics.PhysicsSystem) 30 | physics.process(new(PhysicsInitMessage)) 31 | physics.run(frequency=60) 32 | 33 | spawnThread networkThread: 34 | threadName = "network" 35 | 36 | var network = new(network.ServerNetworkSystem) 37 | network.process((ref ServerInitMessage)()) 38 | network.run() 39 | 40 | joinActiveThreads() 41 | 42 | spawnProcess "client": 43 | 44 | spawnThread networkThread: 45 | threadName = "network" 46 | 47 | var network = new(network.ClientNetworkSystem) 48 | network.process((ref ClientInitMessage)()) 49 | network.run() 50 | 51 | spawnThread videoThread: 52 | threadName = "video" 53 | 54 | var video = new(video.VideoSystem) 55 | video.process((ref VideoInitMessage)()) 56 | video.run(frequency=60) 57 | 58 | spawnThread inputThread: 59 | threadName = "input" 60 | 61 | var input = new(input.InputSystem) 62 | input.process((ref InputInitMessage)()) 63 | input.run(frequency=60) 64 | 65 | for thread in @[networkThread, videoThread, inputThread]: 66 | probe(thread, timeout=initDuration(seconds=5)) 67 | 68 | # when all threads started, connect to the server 69 | new(ConnectMessage).send(networkThread) 70 | 71 | joinActiveThreads() 72 | 73 | joinProcesses() 74 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/entities 2 | import c4/messages 3 | import c4/systems/network/net 4 | 5 | 6 | type 7 | EntityMessage* = object of NetworkMessage 8 | entity*: Entity 9 | 10 | EntityCreateMessage* = object of EntityMessage 11 | width*, height*: float 12 | 13 | EntityMoveMessage* = object of EntityMessage 14 | x*, y*: float 15 | 16 | MoveMessage* = object of NetworkMessage 17 | up*: bool 18 | 19 | 20 | register EntityMessage 21 | register EntityCreateMessage 22 | register EntityMoveMessage 23 | register MoveMessage -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/scenarios/master.nim: -------------------------------------------------------------------------------- 1 | import std/random 2 | import std/times 3 | 4 | import c4/entities 5 | import c4/threads 6 | import c4/systems/network/net 7 | import c4/systems/physics/simple 8 | 9 | import ../systems/network 10 | import ../systems/physics 11 | import ../systems/video 12 | import ../messages 13 | import ../threads 14 | 15 | var rand = initRand() 16 | 17 | 18 | method receive*(self: ref network.ServerNetworkSystem, message: ref HelloMessage) = 19 | message.send(physicsThread) 20 | 21 | method process*(self: ref physics.PhysicsSystem, message: ref HelloMessage) = 22 | info "physics received hello message" 23 | 24 | # send entities messages to client 25 | for entity in iterEntities(): 26 | let physics = entity[ref Physics] 27 | 28 | (ref EntityCreateMessage)( 29 | connection: message.connection, 30 | entity: entity, 31 | width: physics.width, 32 | height: physics.height, 33 | ).send(networkThread) 34 | 35 | (ref EntityMoveMessage)( 36 | connection: message.connection, 37 | entity: entity, 38 | x: physics.position.x, 39 | y: physics.position.y, 40 | ).send(networkThread) 41 | 42 | method receive*(self: ref network.ClientNetworkSystem, message: ref EntityCreateMessage) = 43 | debug "creating entity" 44 | let entity = newEntity() 45 | self.entitiesMap[message.entity] = entity # remember mapping from server's entity to client's one 46 | message.send(videoThread) # forward message to video thread 47 | 48 | method receive*(self: ref network.ClientNetworkSystem, message: ref EntityMoveMessage) = 49 | debug "moving entity" 50 | try: 51 | message.entity = self.entitiesMap[message.entity] # convert server's entity to client's one 52 | except KeyError: 53 | return # move message was received before entity creation message -> do nothing 54 | message.send(videoThread) # forward message to video thread 55 | 56 | method process*(self: ref VideoSystem, message: ref EntityCreateMessage) = 57 | # create video component for new entity 58 | message.entity[ref Video] = (ref Video)(x: 0, y: 0, width: message.width, height: message.height) 59 | 60 | method process*(self: ref VideoSystem, message: ref EntityMoveMessage) = 61 | # update video component 62 | let video = message.entity[ref Video] 63 | video.x = message.x 64 | video.y = message.y 65 | 66 | method receive*(self: ref network.ServerNetworkSystem, message: ref MoveMessage) = 67 | message.send(physicsThread) 68 | 69 | method process*(self: ref physics.PhysicsSystem, message: ref MoveMessage) = 70 | if epochTime() < self.freezePlayerUntil: 71 | return # discard user input if frozen 72 | 73 | self.player[ref Physics].velocity = (x: 0, y: self.movementSpeed * (if message.up: -1 else: 1)) 74 | 75 | let ballPhysics = self.ball[ref Physics] 76 | if ballPhysics.velocity == (x: 0.0, y: 0.0): 77 | ballPhysics.velocity = ( 78 | x: self.movementSpeed * (1.0 + rand.rand(0.1)), 79 | y: self.movementSpeed * (1.0 + rand.rand(0.1)), 80 | ) 81 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | 3 | import c4/logging 4 | import c4/threads 5 | import c4/systems/input/sdl 6 | 7 | import ../messages 8 | import ../threads 9 | 10 | 11 | type 12 | InputSystem* = object of sdl.InputSystem 13 | 14 | 15 | method handleKeyboardState*( 16 | self: ref InputSystem, 17 | keyboard: ptr array[0 .. SDL_NUM_SCANCODES.int, uint8], 18 | ) = 19 | 20 | var direction = 0 21 | if keyboard[SDL_SCANCODE_UP.int] > 0: direction += 1 22 | if keyboard[SDL_SCANCODE_DOWN.int] > 0: direction -= 1 23 | 24 | case direction: 25 | of 1: 26 | (ref MoveMessage)(up: true).send(networkThread) 27 | of -1: 28 | (ref MoveMessage)(up: false).send(networkThread) 29 | else: 30 | discard 31 | 32 | # if keys.len > 0: 33 | # info "keyboard input", keys 34 | 35 | # if keyboard[SDL_SCANCODE_ESCAPE.int] > 0: 36 | # new(StopMessage).send(c4threads.ThreadID(1)) 37 | # info "quit" 38 | # raise newException(BreakLoopException, "") 39 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import std/tables 2 | 3 | import c4/entities 4 | import c4/systems/network/net 5 | 6 | 7 | type 8 | ServerNetworkSystem* = object of net.ServerNetworkSystem 9 | 10 | ClientNetworkSystem* = object of net.ClientNetworkSystem 11 | entitiesMap*: Table[Entity, Entity] # conversion from server entity to client entity 12 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/systems/physics.nim: -------------------------------------------------------------------------------- 1 | import std/times 2 | 3 | import c4/entities 4 | import c4/systems/physics/simple 5 | import c4/logging 6 | import c4/sugar 7 | import c4/threads 8 | 9 | import ../messages 10 | import ../threads 11 | 12 | type 13 | PhysicsSystem* = object of simple.PhysicsSystem 14 | borders*: array[4, Entity] 15 | player*: Entity 16 | computer*: Entity 17 | ball*: Entity 18 | 19 | movementSpeed*: float = 500.0 20 | freezePlayerUntil*: float = 0.0 21 | freezeDuration*: float = 0.3 22 | 23 | 24 | method process*(self: ref PhysicsSystem, message: ref PhysicsInitMessage) = 25 | self.player = newEntity() 26 | self.player[ref Physics] = (ref Physics)(width: 3, height: 100, position: (x: 20, y: 300)) 27 | 28 | self.computer = newEntity() 29 | self.computer[ref Physics] = (ref Physics)(width: 3, height: 100, position: (x: 780, y: 300)) 30 | 31 | # we make them fit precisely but not collide 32 | self.borders = [newEntity(), newEntity(), newEntity(), newEntity()] 33 | self.borders[0][ref Physics] = (ref Physics)(width: 3, height: 600-2, position: (x: -2.0, y: 300.0)) # left 34 | self.borders[1][ref Physics] = (ref Physics)(width: 3, height: 600-2, position: (x: 802.0, y: 300.0)) # right 35 | self.borders[2][ref Physics] = (ref Physics)(width: 800-2, height: 3, position: (x: 400.0, y: -2.0)) # up 36 | self.borders[3][ref Physics] = (ref Physics)(width: 800-2, height: 3, position: (x: 400.0, y: 602.0)) # down 37 | 38 | self.ball = newEntity() 39 | self.ball[ref Physics] = (ref Physics)(width: 6, height: 6, position: (x: 400.0, y: 300.0)) 40 | 41 | debug "physics initialization finished" 42 | 43 | proc reset*(self: ref PhysicsSystem) = 44 | let ballPhysics = self.ball[ref Physics] 45 | 46 | ballPhysics.position = (x: 400.0, y: 300.0) 47 | ballPhysics.velocity = (x: 0.0, y: 0.0) 48 | 49 | 50 | method update*(self: ref PhysicsSystem, dt: float) {.gcsafe.} = 51 | let 52 | computerPhysics = self.computer[ref Physics] 53 | ballPhysics = self.ball[ref Physics] 54 | if computerPhysics.position.y > ballPhysics.position.y + 5: 55 | computerPhysics.velocity = (x: 0, y: -self.movementSpeed * 0.95) 56 | elif computerPhysics.position.y < ballPhysics.position.y - 5: 57 | computerPhysics.velocity = (x: 0, y: self.movementSpeed * 0.95) 58 | 59 | # move stuff 60 | procCall self.as(ref simple.PhysicsSystem).update(dt) 61 | 62 | for entity, physics in getComponents(ref Physics): 63 | if physics.position != physics.previousPosition: 64 | (ref EntityMoveMessage)(entity: entity, x: physics.position.x, y: physics.position.y).send(networkThread) 65 | 66 | # stop moving anything but the ball at the end of update cycle 67 | for entity, physics in getComponents(ref Physics): 68 | if entity != self.ball: 69 | physics.velocity = (x: 0.0, y: 0.0) 70 | 71 | var winner: Entity 72 | if ballPhysics.position.x < self.player[ref Physics].position.x - 3: 73 | winner = self.computer 74 | elif ballPhysics.position.x > computerPhysics.position.x + 3: 75 | winner = self.player 76 | 77 | if winner.isInitialized(): 78 | info "end of game", winner=if winner == self.computer: "computer" else: "player" 79 | self.reset() 80 | self.freezePlayerUntil = epochTime() + self.freezeDuration 81 | -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | 3 | import c4/logging 4 | import c4/entities 5 | import c4/systems/video/sdl 6 | 7 | 8 | type 9 | VideoSystem* = object of sdl.VideoSystem 10 | Video* = object of RootObj 11 | x*, y*: float 12 | width*, height*: float 13 | 14 | proc topLeft(self: ref Video): tuple[x: float, y: float] = 15 | (self.x - self.width/2, self.y - self.height/2) 16 | 17 | method update*(self: ref VideoSystem, dt: float) = 18 | if self.renderer.setDrawColor(0, 0, 0, 255) != SdlSuccess: handleError("failed to set renderer draw color") 19 | if self.renderer.clear() != 0: handleError("failed to clear renderer") 20 | 21 | if self.renderer.setDrawColor(255, 255, 255, 255) != SdlSuccess: handleError("failed to set renderer draw color") 22 | 23 | for video in getComponents(ref Video).values(): 24 | 25 | let (x, y) = video.topLeft() 26 | var rectangle = rect( 27 | x=x.cint, 28 | y=y.cint, 29 | w=video.width.cint, 30 | h=video.height.cint, 31 | ) 32 | if self.renderer.drawRect(rectangle.addr) != SdlSuccess: handleError("failed to draw rectangle") 33 | 34 | self.renderer.present() -------------------------------------------------------------------------------- /docs/tutorials/10 - simple 2d game/src/threads.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | 3 | 4 | const 5 | networkThread* = ThreadID(1) 6 | physicsThread* = ThreadID(2) 7 | videoThread* = ThreadID(3) 8 | inputThread* = ThreadID(4) 9 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xE111/cat-400/df9f2fea2430ffca8461279ca93a7a00c7c9a763/docs/tutorials/11 - simple 3d game/readme.md -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/main.nim: -------------------------------------------------------------------------------- 1 | import times 2 | 3 | import sdl2 4 | 5 | import c4/processes 6 | import c4/threads 7 | import c4/logging 8 | 9 | import c4/systems 10 | import c4/systems/video/ogre as ogre_video 11 | import c4/systems/network/net 12 | import c4/systems/input/sdl as sdl_input 13 | import c4/systems/physics/ode 14 | 15 | import ./threads 16 | import ./systems/video 17 | import ./systems/network 18 | import ./systems/input 19 | import ./systems/physics 20 | 21 | # TODO: import all submodules automatically 22 | import ./scenarios/[ 23 | hello, 24 | entity_create, 25 | entity_move, 26 | player, 27 | ] 28 | 29 | 30 | when isMainModule: 31 | 32 | spawnProcess "server": 33 | 34 | spawnThread physicsThread: 35 | threadName = "physics" 36 | 37 | var physics = new(physics.PhysicsSystem) 38 | physics.process(new(PhysicsInitMessage)) 39 | physics.run() 40 | 41 | spawnThread networkThread: 42 | threadName = "network" 43 | 44 | var network = new(network.ServerNetworkSystem) 45 | network.process((ref ServerInitMessage)()) 46 | network.run() 47 | 48 | joinActiveThreads() 49 | 50 | spawnProcess "client": 51 | 52 | spawnThread networkThread: 53 | threadName = "network" 54 | 55 | var network = new(network.ClientNetworkSystem) 56 | network.process((ref ClientInitMessage)()) 57 | network.run() 58 | 59 | spawnThread videoThread: 60 | threadName = "video" 61 | 62 | var video = new(video.VideoSystem) 63 | video.process((ref ogre_video.VideoInitMessage)(windowWidth: 1200, windowHeight: 800)) 64 | video.run(frequency=60) 65 | 66 | spawnThread inputThread: 67 | threadName = "input" 68 | 69 | var input = new(input.InputSystem) 70 | input.process((ref InputInitMessage)()) 71 | input.run(frequency=30) 72 | 73 | for thread in @[networkThread, videoThread, inputThread]: 74 | probe(thread, timeout=initDuration(seconds=5)) 75 | 76 | # when all threads started, connect to the server 77 | new(ConnectMessage).send(networkThread) 78 | 79 | joinActiveThreads() 80 | 81 | joinProcesses() 82 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/messages.nim: -------------------------------------------------------------------------------- 1 | import c4/entities 2 | import c4/messages 3 | import c4/systems/network/net 4 | import c4/lib/ode/ode 5 | 6 | import ./utils 7 | 8 | 9 | type 10 | 11 | EntityMessage* = object of NetworkMessage 12 | entity*: Entity 13 | 14 | EntityCreateMessage* = object of EntityMessage 15 | shape*: tuple[ 16 | vertices: seq[dVector3], 17 | indexes: seq[array[3, int]], 18 | ] 19 | 20 | EntityMoveMessage* = object of EntityMessage 21 | x*, y*, z*: float 22 | 23 | EntityRotateMessage* = object of EntityMessage 24 | quaternion*: Quaternion 25 | 26 | PlayerRotateMessage* = object of NetworkMessage 27 | yaw*, pitch*: float 28 | 29 | PlayerMoveMessage* = object of NetworkMessage 30 | yaw*: float 31 | 32 | PlayerVerticalMoveMessage* = object of NetworkMessage 33 | up*: bool 34 | 35 | ImpersonateMessage* = object of EntityMessage 36 | 37 | 38 | register EntityMessage 39 | register EntityCreateMessage 40 | register EntityMoveMessage 41 | register PlayerRotateMessage 42 | register ImpersonateMessage 43 | register EntityRotateMessage 44 | register PlayerMoveMessage 45 | register PlayerVerticalMoveMessage -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/nim.cfg: -------------------------------------------------------------------------------- 1 | --backend:cpp 2 | -t:"-I/usr/include/OGRE -I/usr/include/OGRE/Bites" 3 | -l:"-lSDL -lOgreMain" 4 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/plugins.cfg: -------------------------------------------------------------------------------- 1 | # Defines plugins to load 2 | 3 | # Define plugin folder 4 | PluginFolder=/usr/lib/OGRE 5 | 6 | # Define plugins 7 | # Plugin=RenderSystem_Direct3D9 8 | # Plugin=RenderSystem_Direct3D11 9 | Plugin=RenderSystem_GL 10 | Plugin=RenderSystem_GL3Plus 11 | Plugin=RenderSystem_GLES2 12 | # Plugin=RenderSystem_Metal 13 | # Plugin=RenderSystem_Tiny 14 | # Plugin=RenderSystem_Vulkan 15 | Plugin=Plugin_ParticleFX 16 | Plugin=Plugin_BSPSceneManager 17 | # Plugin=Plugin_CgProgramManager 18 | # Plugin=Plugin_GLSLangProgramManager 19 | # Plugin=Codec_EXR 20 | Plugin=Codec_STBI 21 | # Plugin=Codec_RsImage 22 | # Plugin=Codec_FreeImage 23 | Plugin=Plugin_PCZSceneManager 24 | Plugin=Plugin_OctreeZone 25 | Plugin=Plugin_OctreeSceneManager 26 | Plugin=Plugin_DotScene 27 | # Plugin=Codec_Assimp 28 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/scenarios/entity_create.nim: -------------------------------------------------------------------------------- 1 | import c4/entities 2 | import c4/threads 3 | import c4/logging 4 | import c4/lib/ogre/ogre 5 | 6 | import ../systems/network 7 | import ../systems/video 8 | import ../messages 9 | import ../threads 10 | 11 | 12 | method receive*(self: ref network.ClientNetworkSystem, message: ref EntityCreateMessage) = 13 | debug "creating entity" 14 | let entity = newEntity() 15 | self.entitiesMap[message.entity] = entity # remember mapping from server's entity to client's one 16 | message.entity = entity 17 | message.send(videoThread) # forward message to video thread 18 | 19 | 20 | method process*(self: ref VideoSystem, message: ref EntityCreateMessage) = 21 | 22 | let node = self.sceneManager.getRootSceneNode().createChildSceneNode() 23 | if message.shape.vertices.len > 0: 24 | 25 | let manualObject = self.sceneManager.createManualObject() 26 | 27 | # -------- draw as triangles -------- 28 | # manualObject.begin("BaseWhiteNoLighting", OT_TRIANGLE_LIST) 29 | # # manualObject.colour(0.5, 0.5, 0.5) 30 | # for i in 0 ..< int(message.shape.len / 3): 31 | # manualObject.position(message.shape[i * 3], message.shape[i * 3 + 1], message.shape[i * 3 + 2]) 32 | # for i in 0 ..< int(message.shape.len / 9): 33 | # manualObject.triangle(i.uint32 * 3, i.uint32 * 3 + 1, i.uint32 * 3 + 2) 34 | 35 | # -------- draw as lines -------- 36 | manualObject.begin("BaseWhiteNoLighting", OT_LINE_LIST) 37 | for triangleIndexes in message.shape.indexes: 38 | let a = message.shape.vertices[triangleIndexes[0]] 39 | let b = message.shape.vertices[triangleIndexes[1]] 40 | let c = message.shape.vertices[triangleIndexes[2]] 41 | 42 | manualObject.position(a[0], a[1], a[2]) 43 | manualObject.position(b[0], b[1], b[2]) 44 | 45 | manualObject.position(b[0], b[1], b[2]) 46 | manualObject.position(c[0], c[1], c[2]) 47 | 48 | manualObject.position(c[0], c[1], c[2]) 49 | manualObject.position(a[0], a[1], a[2]) 50 | 51 | discard manualObject.end() 52 | discard manualObject.convertToMesh("manualObject") 53 | 54 | let videoEntity = self.sceneManager.createEntity("manualObject") 55 | node.attachObject(videoEntity) 56 | # assert false 57 | 58 | else: 59 | let box = self.sceneManager.createEntity("box") 60 | node.attachObject(box) 61 | 62 | message.entity[ref Video] = (ref Video)(node: node) 63 | debug "created new video", entity=message.entity 64 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/scenarios/entity_move.nim: -------------------------------------------------------------------------------- 1 | import c4/entities 2 | import c4/threads 3 | import c4/logging 4 | import c4/lib/ogre/ogre 5 | 6 | import ../systems/network 7 | import ../systems/video 8 | import ../messages 9 | import ../threads 10 | 11 | 12 | method receive*(self: ref network.ClientNetworkSystem, message: ref EntityMoveMessage) = 13 | try: 14 | message.entity = self.entitiesMap[message.entity] # convert server's entity to client's one 15 | except KeyError: # TODO 16 | warn "move message before entity creation", message=message 17 | return 18 | 19 | debug "moving entity" 20 | message.send(videoThread) # forward message to video thread 21 | 22 | method process*(self: ref VideoSystem, message: ref EntityMoveMessage) = 23 | let video = message.entity[ref Video] 24 | video.node.setPosition(x=message.x, y=message.y, z=message.z) 25 | debug "moved entity", entity=message.entity, x=message.x, y=message.y, z=message.z 26 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/scenarios/hello.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | import c4/logging 3 | import c4/systems/network/net 4 | import c4/entities 5 | import c4/systems/physics/ode 6 | import c4/lib/ode/ode 7 | import c4/lib/ogre/ogre 8 | 9 | import ../systems/network 10 | import ../systems/video 11 | import ../systems/physics 12 | import ../threads 13 | import ../messages 14 | 15 | 16 | method receive*(self: ref network.ServerNetworkSystem, message: ref HelloMessage) = 17 | message.send(physicsThread) 18 | 19 | method process*(self: ref physics.PhysicsSystem, message: ref HelloMessage) = 20 | info "physics received hello message" 21 | 22 | # when receiving HelloMessage from new client, send him whole world information 23 | for entity, physics in getComponents(ref physics.Physics): 24 | (ref EntityCreateMessage)( 25 | entity: entity, 26 | shape: ( 27 | vertices: physics.shape.vertices, 28 | indexes: physics.shape.indexes, 29 | ), 30 | ).send(networkThread) 31 | let position = physics.body.bodyGetPosition() 32 | (ref EntityMoveMessage)(entity: entity, x: position[0], y: position[1], z: position[2]).send(networkThread) 33 | 34 | (ref ImpersonateMessage)(entity: self.player).send(networkThread) 35 | 36 | method receive*(self: ref network.ClientNetworkSystem, message: ref ImpersonateMessage) = 37 | message.entity = self.entitiesMap[message.entity] # TODO: make it automatic 38 | message.send(videoThread) 39 | 40 | method process*(self: ref VideoSystem, message: ref ImpersonateMessage) = 41 | message.entity[ref Video].node.attachObject(self.camera) 42 | debug "camera attached to entity", entity=message.entity 43 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/scenarios/player.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import c4/entities 4 | import c4/threads 5 | import c4/logging 6 | import c4/lib/ogre/ogre 7 | import c4/lib/ode/ode 8 | import c4/systems/physics/ode 9 | 10 | import ../systems/network 11 | import ../systems/video 12 | import ../systems/physics 13 | import ../messages 14 | import ../threads 15 | import ../utils 16 | 17 | const walkSpeed = 5 * 1000 / 60 / 60 18 | 19 | method receive*(self: ref ServerNetworkSystem, message: ref PlayerRotateMessage) = 20 | message.send(physicsThread) 21 | 22 | method process*(self: ref physics.PhysicsSystem, message: ref PlayerRotateMessage) = 23 | 24 | # get current rotation quaternion 25 | let qCurrent = self.player[ref physics.Physics].body.bodyGetQuaternion()[] 26 | 27 | # get current yaw and pitch 28 | let current = qCurrent.getPitchYaw() 29 | 30 | # calculate combined pitch and yaw 31 | let 32 | yaw = current.yaw + message.yaw 33 | pitch = max(min(current.pitch + message.pitch, PI/2 * 0.99), -PI/2 * 0.99) 34 | 35 | # convert PlayerRotateMessage relative angles to rotation quaternions 36 | var qYaw, qPitch: dQuaternion 37 | qYaw.qFromAxisAndAngle(0, 1, 0, yaw) 38 | qPitch.qFromAxisAndAngle(1, 0, 0, pitch) 39 | 40 | # multiply rotation quaternions and set result as new entity rotation quaternion 41 | var qFinal: dQuaternion 42 | qFinal.qMultiply0(qYaw, qPitch) 43 | 44 | self.player[ref physics.Physics].body.bodySetQuaternion(qFinal) 45 | (ref EntityRotateMessage)(entity: self.player, quaternion: qFinal).send(networkThread) 46 | 47 | method receive*(self: ref ClientNetworkSystem, message: ref EntityRotateMessage) = 48 | message.entity = self.entitiesMap[message.entity] 49 | message.send(videoThread) 50 | 51 | method process*(self: ref VideoSystem, message: ref EntityRotateMessage) = 52 | message.entity[ref Video].node.setOrientation( 53 | message.quaternion[0], 54 | message.quaternion[1], 55 | message.quaternion[2], 56 | message.quaternion[3], 57 | ) 58 | 59 | method receive*(self: ref ServerNetworkSystem, message: ref PlayerMoveMessage) = 60 | message.send(physicsThread) 61 | 62 | method process*(self: ref physics.PhysicsSystem, message: ref PlayerMoveMessage) = 63 | 64 | let playerPhysics = self.player[ref physics.Physics] 65 | self.playerMovementElapsed = 1/60 66 | 67 | # calculate selected direction as a result of yaw on (0, 0, -1) vector 68 | let direction: array[3, float] = [-sin(message.yaw) , 0.0, -cos(message.yaw)] 69 | 70 | # get current rotation matrix and apply it to selected direction 71 | let rotation = playerPhysics.body.bodyGetRotation()[] 72 | let finalDirection: array[3, float] = [ 73 | rotation[0] * direction[0] + rotation[1] * direction[1] + rotation[2] * direction[2], 74 | rotation[4] * direction[0] + rotation[5] * direction[1] + rotation[6] * direction[2], 75 | rotation[8] * direction[0] + rotation[9] * direction[1] + rotation[10] * direction[2], 76 | ] 77 | 78 | self.player[ref physics.Physics].body.bodySetLinearVel( 79 | finalDirection[0] * walkSpeed, 80 | finalDirection[1] * walkSpeed, 81 | finalDirection[2] * walkSpeed, 82 | ) 83 | 84 | method receive*(self: ref ServerNetworkSystem, message: ref PlayerVerticalMoveMessage) = 85 | message.send(physicsThread) 86 | 87 | method process*(self: ref physics.PhysicsSystem, message: ref PlayerVerticalMoveMessage) = 88 | let body = self.player[ref physics.Physics].body 89 | let linearVelocity = body.bodyGetLinearVel() 90 | body.bodySetLinearVel( 91 | linearVelocity[0], 92 | walkSpeed * (if message.up: 1.0 else: -1.0), 93 | linearVelocity[2], 94 | ) 95 | self.playerMovementElapsed = 1/60 96 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/systems/input.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | import math 3 | 4 | import c4/loop 5 | import c4/logging 6 | import c4/threads 7 | import c4/systems/input/sdl 8 | import c4/sugar 9 | 10 | import ../messages 11 | import ../threads 12 | 13 | 14 | type 15 | InputSystem* = object of sdl.InputSystem 16 | 17 | 18 | method handleEvent*(self: ref InputSystem, event: Event) = 19 | procCall self.as(ref sdl.InputSystem).handleEvent(event) 20 | 21 | case event.kind 22 | of MOUSEMOTION: 23 | var x, y: cint 24 | let radInPixel = PI / 180 / 4 # 0.25 degree in 1 pixel 25 | discard getRelativeMouseState(x, y) 26 | trace "mouse moved", x, y 27 | (ref PlayerRotateMessage)( 28 | yaw: -x.float * radInPixel, 29 | pitch: -y.float * radInPixel, 30 | ).send(networkThread) 31 | else: 32 | discard 33 | 34 | 35 | method handleKeyboardState*( 36 | self: ref InputSystem, 37 | keyboard: ptr array[0 .. SDL_NUM_SCANCODES.int, uint8], 38 | ) = 39 | var 40 | forward = keyboard[SDL_SCANCODE_W.int] > 0 41 | backward = keyboard[SDL_SCANCODE_S.int] > 0 42 | left = keyboard[SDL_SCANCODE_A.int] > 0 43 | right = keyboard[SDL_SCANCODE_D.int] > 0 44 | 45 | # pressing opposite keys disables both of them 46 | if forward and backward: 47 | forward = false 48 | backward = false 49 | 50 | if left and right: 51 | left = false 52 | right = false 53 | 54 | if forward or backward or left or right: 55 | var yaw: float 56 | if right and not forward and not backward: 57 | yaw = -2 * PI/4 58 | elif right and forward: 59 | yaw = -1 * PI/4 60 | elif forward and not right and not left: 61 | yaw = 0 * PI/4 62 | elif forward and left: 63 | yaw = 1 * PI/4 64 | elif left and not forward and not backward: 65 | yaw = 2 * PI/4 66 | elif left and backward: 67 | yaw = 3 * PI/4 68 | elif backward and not left and not right: 69 | yaw = 4 * PI/4 70 | elif backward and right: 71 | yaw = 5 * PI/4 72 | 73 | (ref PlayerMoveMessage)(yaw: yaw).send(networkThread) 74 | 75 | # if keyboard[SDL_SCANCODE_ESCAPE.int] > 0: 76 | # raise newException(BreakLoopException, "") 77 | let 78 | up = keyboard[SDL_SCANCODE_SPACE.int] > 0 79 | down = keyboard[SDL_SCANCODE_LCTRL.int] > 0 80 | if up or down and not (up and down): 81 | (ref PlayerVerticalMoveMessage)(up: up).send(networkThread) 82 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/systems/network.nim: -------------------------------------------------------------------------------- 1 | import std/tables 2 | 3 | import c4/entities 4 | import c4/systems/network/net 5 | import c4/threads 6 | 7 | import ../threads 8 | import ../messages 9 | 10 | 11 | type 12 | ServerNetworkSystem* = object of net.ServerNetworkSystem 13 | 14 | ClientNetworkSystem* = object of net.ClientNetworkSystem 15 | entitiesMap*: Table[Entity, Entity] # conversion from server entity to client entity 16 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/systems/video.nim: -------------------------------------------------------------------------------- 1 | import sdl2 2 | 3 | import c4/lib/ogre/ogre as libogre 4 | import c4/systems/video/ogre 5 | import c4/sugar 6 | 7 | 8 | type 9 | VideoSystem* = object of ogre.VideoSystem 10 | 11 | Video* = object of RootObj 12 | node*: ptr SceneNode 13 | 14 | 15 | proc drawAxis*(self: ref VideoSystem) = 16 | let axisObject = self.sceneManager.createManualObject() 17 | axisObject.begin("BaseWhiteNoLighting", OT_LINE_LIST) 18 | 19 | # X axis, red 20 | for z in -10..10: 21 | axisObject.position(-10.0, 0.0, z.float) 22 | axisObject.colour(if z == 0: 1.0 else: 0.5, 0, 0) 23 | axisObject.position(10.0, 0.0, z.float) 24 | 25 | # Y axis, green 26 | axisObject.position(0, 0, 0) 27 | axisObject.colour(0, 1, 0) 28 | axisObject.position(0, 100, 0) 29 | 30 | # Z axis, blue 31 | for x in -10..10: 32 | axisObject.position(x.float, 0.0, -10.0) 33 | axisObject.colour(0, 0, if x == 0: 1.0 else: 0.5) 34 | axisObject.position(x.float, 0.0, 10.0) 35 | 36 | discard axisObject.end() 37 | discard axisObject.convertToMesh("axis") 38 | 39 | let axis = self.sceneManager.createEntity("axis") 40 | self.sceneManager.getRootSceneNode().createChildSceneNode().attachObject(axis) 41 | 42 | 43 | proc createBoxMesh(self: ref VideoSystem) = 44 | 45 | let boxObject = self.sceneManager.createManualObject() 46 | let size = 0.1 47 | 48 | operateOn boxObject: 49 | begin("BaseWhiteNoLighting", OT_TRIANGLE_LIST) 50 | 51 | # front 52 | position(-size/2, -size/2, size/2) 53 | colour(0, 0, 0.75) 54 | position(size/2, -size/2, size/2) 55 | position(size/2, size/2, size/2) 56 | position(-size/2, size/2, size/2) 57 | quad(0, 1, 2, 3) 58 | 59 | # back 60 | position(-size/2, size/2, -size/2) 61 | position(size/2, size/2, -size/2) 62 | position(size/2, -size/2, -size/2) 63 | position(-size/2, -size/2, -size/2) 64 | quad(4, 5, 6, 7) 65 | 66 | # right 67 | position(size/2, -size/2, size/2) 68 | colour(0.75, 0, 0) 69 | position(size/2, -size/2, -size/2) 70 | position(size/2, size/2, -size/2) 71 | position(size/2, size/2, size/2) 72 | quad(8, 9, 10, 11) 73 | 74 | # left 75 | position(-size/2, -size/2, -size/2) 76 | position(-size/2, -size/2, size/2) 77 | position(-size/2, size/2, size/2) 78 | position(-size/2, size/2, -size/2) 79 | quad(12, 13, 14, 15) 80 | 81 | # bottom 82 | position(-size/2, -size/2, -size/2) 83 | colour(0, 0.75, 0) 84 | position(size/2, -size/2, -size/2) 85 | position(size/2, -size/2, size/2) 86 | position(-size/2, -size/2, size/2) 87 | quad(16, 17, 18, 19) 88 | 89 | # up 90 | position(-size/2, size/2, size/2) 91 | position(size/2, size/2, size/2) 92 | position(size/2, size/2, -size/2) 93 | position(-size/2, size/2, -size/2) 94 | quad(20, 21, 22, 23) 95 | 96 | discard boxObject.end() 97 | discard boxObject.convertToMesh("box") 98 | 99 | 100 | method process*(self: ref VideoSystem, message: ref ogre.VideoInitMessage) = 101 | procCall self.as(ref ogre.VideoSystem).process(message) 102 | 103 | discard setRelativeMouseMode(True32) 104 | 105 | self.camera.setNearClipDistance(0.01) 106 | self.sceneManager.setAmbientLight(initColourValue(0.5, 0.5, 0.5)) 107 | 108 | self.drawAxis() 109 | self.createBoxMesh() 110 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/threads.nim: -------------------------------------------------------------------------------- 1 | import c4/threads 2 | 3 | 4 | const 5 | networkThread* = ThreadID(1) 6 | physicsThread* = ThreadID(2) 7 | videoThread* = ThreadID(3) 8 | inputThread* = ThreadID(4) 9 | -------------------------------------------------------------------------------- /docs/tutorials/11 - simple 3d game/src/utils.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import c4/lib/ode/ode 4 | 5 | 6 | type Quaternion* = array[4, float] # w, x, y, z 7 | 8 | 9 | proc eulFromR*(r: dMatrix3): tuple[z, y, x: float] = 10 | # ZYXr case only 11 | let cy = sqrt(r[0] * r[0] + r[4] * r[4]) 12 | if cy > 16 * 0.000002: 13 | result.x = arctan2(r[9], r[10]) 14 | result.y = arctan2(-r[8], cy) 15 | result.z = arctan2(r[4], r[0]) 16 | else: 17 | result.x = arctan2(-r[6], r[5]) 18 | result.y = arctan2(-r[8], cy) 19 | result.z = 0 20 | 21 | 22 | proc eulFromQ*(q: dQuaternion): tuple[z, y, x: float] = 23 | # ZYXr case only 24 | 25 | # get rotation matrix 26 | var m: dMatrix3 27 | m.rfromQ(q) 28 | 29 | eulFromR(m) 30 | 31 | 32 | proc getPitchYaw*(q: dQuaternion): tuple[yaw: float, pitch: float] = 33 | ## Convert quaternion as only yaw and pitch rotations 34 | 35 | # rotation in Euler angles 36 | let eul = q.eulFromQ() 37 | 38 | # get current yaw & pitch (as if it was without roll) 39 | let flip: bool = not(abs(eul.z) <= 0.001) 40 | 41 | result.yaw = if not flip: eul.y else: PI - eul.y 42 | result.pitch = if not flip: eul.x else: eul.x + (if eul.x < 0: PI else: -PI) 43 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | --multimethods:on 3 | --gc:arc 4 | -d:chronicles_log_level=DEBUG 5 | -d:chronicles_sinks=textlines 6 | -d:chronicles_disable_thread_id:1 7 | -d:chroniclesLineNumbers:1 8 | -o:"/tmp/out" 9 | -p:"~/.nimble/pkgs2" 10 | --stackTrace:on --------------------------------------------------------------------------------