np-eazy / Pentaquad

3 stars 1 forks source link

[DESIGN] Graphics Infrastructure & Ground Rules for Impl. #31

Closed np-eazy closed 1 year ago

np-eazy commented 1 year ago

As of now, this is backlogged until all game rules and controls are set in place. Fix ALL non-DevOps issues before working on these in case we can further refactor in anticipation of the features listed in this issue.

Setting some ground rules and infrastructure ideas for dealing with graphics from the ground up with just HTML Canvas and no game/render engine. The GameState is lightweight enough to handle purely in JS, so we don't have to deal with any Rust listeners; in fact, render algorithms tend to be as straightforward as it gets, so Rust/WASM would be perfect for dealing with the many particles.

  1. The GameState is the single source of truth, and the GameCanvas is the single point of entry.

  2. GlobalProps: Every rendered object and GameState can have control of GlobalProps which include:

    • shakeParam: a number that tells how much to oscillate a global offset
    • flashParam: a Color that tells is globally added to every Color used
    • gravityMagnitude: the magnitude of particle velocity acceleration
    • gravityAngle: the direction of the above as well
  3. RenderProps: Any rendered object must follow a universal RenderProps schema

    • parent: a pointer to a parent RenderProps
    • neighborList: a list of pointers to neighbor RenderProps
    • baseColor: a Color() object
    • tStroke: a transient value between 0 and 1. This is reserved for stroke graphics
    • strokeFunc(canvas, tStroke, x0, y0, xSize, ySize): a 6-arg function that tells us how to draw the stroke of this object
    • glowAddProbability: a value that controls how frequently we add to the glowBuffer
    • glowDecayRate: a value that controls how quickly each element in the glowBuffer decays decays
    • glowBuffer: a list of Colors gathered from neighbors at random; items in the list have coefficients that decay, and when a threshold is hit they are removed from the list. This linear combination is added to the BaseColor to create a glow effect
    • xOffset: to facilitate "falling" animations
    • yOffset: to facilitate "falling" animations
    • scale: to facilitate growing/shrinking animations
    • controlParams: another Object to hold extra variables
    • createMode: boolean
    • updateMode: boolean
    • deleteMode: boolean
    • ... (we would also need to add in things like direction and speed to facilitate falling/accelerating animations here)
  4. Renderables: The only objects we are dealing with are Cells. Corresponding to each Cell's lifecycle in the core state, we should have the following available methods in GameState:

    • createRenderProps(cell) based on its types and props, to be called upon the creation of a new Cell
    • getRenderProps(cell): get the right RenderProps from the cell by accessing the PID
    • createLoop(cell): When a cell is created, it is given a timeToLive flag to count frames in an animation associated with introducing the cell.
    • updateLoop(cell): an unconditional update that only takes in a cell and nothing else; it shouldn't need any extra arguments
    • deleteLoop(cell): When a cell is destroyed, its RenderProps live on in GameState but will have a deleteLoop flag up and a set timeToLive which keeps incrementing down
    • By default, createLoop and deleteLoop can do nothing and the thing should still work.
    • The idea is that we want to keep GameState as a mediator between CoreState and rendering, but we want to keep CoreState as decoupled as possible. If CoreState itself wants to initiate certain animations, like things falling or particles spawning, that should be delegated to createLoop and deleteLoop, and rely on controlParams to make seamless interactions across the three different loops for each renderable.
  5. Particle: An object that once spawned is completely independent of CoreState objects. These can only be generated in createLoops and deleteLoops.

    • baseColor: a Color() object
    • x
    • y
    • dx
    • dy
    • size
    • timeToLive
    • vibrationAmplitude: an animation parameter
    • vibrationPhase: an animation parameter
  6. Calling Convention

    • CoreState: gives us a list of which Cells have been created or deleted, and maintain a list of these for GameState to access after calling update() on CoreState.
    • GameState: initiates createLoops and deleteLoops based on that list, and idly updates all other Cells and Particles.
    • GameGraphics: take in GameState and loop through all renderable objects with RenderProps
  7. Render Domains: Rendered objects are grouped into RenderDomains, which have global x-y positions and contain their own localized coordinate systems. We have the following domains:

    • Main board
    • Piece queue
    • Next piece
    • Scoreboard

Some more references for later: https://dev.to/fallenstedt/making-a-canvas-based-game-with-rust-and-webassembly-2l46.

np-eazy commented 1 year ago

I am coding this up in the "graphics" branch, which as of now has basic piece drop markers, base colors, and individual functions for each cell type. The game look much more playable already and doesn't seem to suffer from much performance issues, but I've found that my description above is too difficult to follow.

There really isn't much point separating the cell props from another set of RenderProps, because the former is already basically the latter and has no use for CoreState. We can just kinda think of cell.props, render(cell), and update(cell) as managing props as a single state in themselves, and IMO applying the render-update loop once again to granular elements is more intuitive and scalable.

However, there would still have to be careful organization of the arrow functions we are using to introduce new cells, and we have to gradually refactor graphics-related dependencies; entry points are all over the place and that isn't very sustainable.

np-eazy commented 1 year ago

Another thing worth noting is that helper functions under drawCell() will act as entry points for Rust functions; we want to have the JS drawCell() function call a corresponding Rust routine container-style, and also multithread these drawCell() calls. Once this is in place, we have room to implement a lot more complex graphics without much processing cost.

np-eazy commented 1 year ago

After I work out PR #48 I will take down this issue and reincarnate it as a wiki page, my PR really deviated from this issue description, but they share the same backbone