3071 - 8bit-wars comp entry

Started by iWasAdam, January 29, 2019, 08:45:01

Previous topic - Next topic

Imerion

Wow, this was really impressive! Fantastic art, really nice flow to the gameplay and very fun. I also appreciated being able to switch between classic Spectrum/Spectrum Next versions for both graphics and music - a cool detail!

Derron

Think the gameplay was a bit too fast to enjoy or even grasp the individual screens of a level. You "rush" through them (at least this is how I played it). Rushed from Pinata to Pinata in the hope to find the runes.
What I found particulary cool were the scream voices ... as this is a "humanoid voice" you can simulate with the hardware available in those computers of that time. Great idea.

Only "bug" I found was: if you hit "ESC" in the main screen the music restarts (might be a "onScreenEnter ...PlayMusic()" thing).

Game looked pretty "authentic" - have to confess I preferred the "next" to the uhmmm "barebone" alternatives ;-)


bye
Ron

iWasAdam

​VivaMortis Source code now released.
https://adamstrange.itch.io/viva-mortis/devlog/76268/vivamortis-source-code-released

This is the mx2 source code. MX2 is a fork of the monkey2 programming language.

There are a number of support file not included (sound and graphics mainly), so IT WONT COMPILE. But it will allow you to see the entire source of the game, how things were done, how the game code is arranged etc.

Any thoughts, suggestions, desires. I'll do my best to help :)

Derron

Thanks for sharing the code - had a quick glance at it of course :-)

- you seem to favor procedural code (many "case"-lines instead of having objects handling their stuff automatically)
- you like numbers instead of constants ;-)

Code (Monkey) Select

_fontMap._tileMap[ 5, _roomX, _roomY ] = 167 'erase pinata from map

[...]
Select _roomKind
Case 164 '-
base = 1
Case 165 '|
base = 2
Case 166 '+
base = 3
Case 167 'room
base = 27
Case 168 'start
base = 0

Case 180 'tV
base = 4
Case 181 't>
base = 7
Case 182 '<t
[...]


I know I would try to do it with const-values. Also you define some "enum values" as "Field" (eg. the "kind" in the codebox). Doesn't your fork allow for constants or globals?



How was such code created:
Code (Monkey) Select

Case 97
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  1 )
Case 98
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  2 )
Case 99
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  3 )
Case 100
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  4 )
Case 101
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  5 )
Case 102
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  6 )
Case 103
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  10 )
Case 104
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  11 )
Case 105
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  12 )
Case 106
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  18 )
Case 107
AddSprite( x*16, y*16,  KIND_PLANT, SPR_BLOCK,  19 )

Couldn't you simply map the case numbers to the spriteTypes? just to save a lot of LOC ? I am sure I am overcomplicating already by suggesting a map to bring down 50 lines to a handful.


Another thing I noted is: you do not split updates and renders - so update rate is the same as the render rate (which might create issues for collision or so). also some animations are not "time based" but "frame based" (350 frames rendered? do something). While this works for a 60fps setup - what happens if you now decide to play at 120fps?

Is this a general way of doing it that way for you - or just for this compo entry? Did you try to play it with eg. 120fps?


Code (Monkey) Select

RequestRender()
ProcessKeyboard()
ProcessLoop()

Any reason to first render and then change physics/logic? I prefer to do vice-versa but maybe your's does have a valid reason too?



Aside of that I have to say that the code looks structured and well layout. Might be of help for some interested readers.


Sorry for almost just "blaming" now but youngest is awake so napping-time is over.


bye
Ron

iWasAdam

Good points Derron  ;D
First the const variables vs number
The data comes from outside and is always in flux initially. So const would need to be updated frequently
Plus... the numbers directly relate to the data and the chr codes so 176 references character 176, which in turn is sprite 176 from the font map

So.. if I update the sprites, the reference chr and codes all need updating too

Next up is the code and the case
Yep, it could be caught and remapped say case1,2,3,4,5 etc
But... there is a train of thought here;
In this case the sprite numbers could be directly mapped to the case numbers. But this is not always that way. They can be completely different
Also, lots of code is basically the same, but there can be changes, multiple sprites draw, etc. Or i may have removed a lot of redundant code already and titled things up.

Having everything separate is a much quicker way of getting to the data/info I want without having to decode things and make new code

Next up frame rates and timers
I lock everything to 60fps. This is timer based, so refresh/screen draw will stretch to accommodate.
The general thought is to aim for a stable 60fps.

If the FPS is 'fixed' at a certain rate, then I can generally use the frame counter for timing etc and it will alway remain almost the same across platforms and machines specs  :P
In some respects this makes working across platforms much more like an 8bit machine. You know how it will update and don't have to think about it

Lastly I suppose there are different approaches to render/logic or logic/render. Either is valid, but things get odd once you start to deal with 3D

The question about procedural vs object is interesting
Oop is really just a variant of procedural programming - just broken  into small lumps. You still have to have the whole oop paradigm in your head.
If you have a system with a map, and players, which all use and want access to the same data, you need to work out how to organise that data.
At a top level you will have a main program that watches marshals and genially keeps order. If you know in advance what you need, oop is great. If you have no idea what you want - oop can get in the way.

I'm certainly not saying oop is bad - quite the reverse. You need to be able to work out which approach gives the best results in the time you have.

Oop is great for reusing stuff....


Derron

@ reusing stuff
Yes - or to allow stuff separate from the "main file". The main project file does not need what a "Page" does - it just sets a current page and calls update() and render() of that.
Whatever happens in there is unimportant for the "main project file".

Same for units, entities on screen ...  all this stuff can work separate from all other thingies.


Quote
First the const variables vs number
The data comes from outside and is always in flux initially. So const would need to be updated frequently
Plus... the numbers directly relate to the data and the chr codes so 176 references character 176, which in turn is sprite 176 from the font map
Isn't that exactly the reason to USE constants instead of having that exact numbers everythere in the code?
Using "const" (or like you did: "Field KIND_***") allows to update it in one line of code.

Extending with new types is also only a matter of having another type then (if we talk about more than a simple "sprite defined by a key/index in a sprite map"). If a tile "number" is tried to get instanced without having a registered type for it: throw an error and the developer knows that he missed to handle something.

If we talk about simple "wall, grass, ..." so maybe only decorative stuff then your "select...case" thing of course is the most efficient thing (imho). Yet these things are "basic tiles" which most often are known right at begin of development (of course a "wall" can be of bricks or of thousands of non-crossable-plants).



@ timers
So what happens to devices having 30fps or "59Hz"? Hmm should try on my HP Elitebook when the kids are in bed and the friends gone. Think it just renders "twice per refresh" ?
Years ago I decoupled physics/logic from rendering to allow faster updates (logic) even with low render rates (some might come along with 30fps to save on cpu/gpu stress - and battery).

bye
Ron

iWasAdam

QuoteIsn't that exactly the reason to USE constants instead of having that exact numbers everythere in the code?
Using "const" (or like you did: "Field KIND_***") allows to update it in one line of code.

yes and no.
If the numbers reference a character location. the number is exactly what it looks like.
For a const you would need to have the text for that 'thing and also need to have both the code and const open at the same time to know what it referenced

Derron

If you have eg. "case 167" and you also need to know "DrawSprite(...., 17)" then this I think is more confusing than to define all of them in one spot

TILE_KIND_GRASS:int = 167
[...]
SPRITE_GRASS:int = 17

And then
case TILE_KIND_GRASS: DrawSprite(...., SPRITE_GRASS)

Else you end up using "dynamic/changing/non-fixed" values in your handle-area instead of definition-area.


Also I understand that you did not give it a name as the content is not fixed and might change - so "167" might mean whatever ...
Ok, but if that was - how to know how to tackle "167" - is it blocking? Is it animated?

This is why I would prefer some kind of semi-automatism: tile = CreateTile(167, 17, [... additional config]). This can of course get then configured directly in your tile editor and lands in a .json or .xml file.


Nonetheless I understand this approach of yours for prototyping ("as long as it works"). Also might be an old habbit of yours (and a habbit of mine to do it differently).


bye
Ron

Steve Elliott

This is in itself a topic for debate.  OOP-style brings benefits no doubt, but needs a structure that doesn't vary too much.  And producing a game is where game designs/ideas change constantly, a more fluid experience.  A balanced approach is required I guess.
Win11 64Gb 12th Gen Intel i9 12900K 3.2Ghz Nvidia RTX 3070Ti 8Gb
Win11 16Gb 12th Gen Intel i5 12450H 2Ghz Nvidia RTX 2050 8Gb
Win11  Pro 8Gb Celeron Intel UHD Graphics 600
Win10/Linux Mint 16Gb 4th Gen Intel i5 4570 3.2GHz, Nvidia GeForce GTX 1050 2Gb
macOS 32Gb Apple M2Max
pi5 8Gb
Spectrum Next 2Mb

iWasAdam

Both have strengths and weakness. But Steve is correct about fluidity (where a design is in a state of flux), and this can hold back (quick) development work with oop.

I think that particles are a good example here.
A particle is a set of combined data. this can be as arrarys, or as types, or as oop. but...
The essence is the same: set a particle, update a particle, check a particle

I usually use oop for this with a particle class and then a manager class that corals all the particles. The app deals with the manager, the manager deals with the particles.

In VivaMortis I went with something quick and dirty. in fact there are 2 separate particle systems: rain and and more general particle setup. both are simple arrays. Here is the entire rain code:

------------
'data
field _pRainX:float[] = New float[64]
field _pRainY:float[] = New float[64]
------------
'setup
For x = 0 To 63
_pRainX[x] = Rnd(400)
_pRainY[x] = Rnd(-100, 150)
Next
------------
'tick the code
local p:int
For p = 0 To 63
_font.DrawChar( monoCanvas, 223,   _pRainX[p], _pRainY[p], 8 )
_pRainY[p] += Rnd(1.3, 3)
_pRainX[p] -= Rnd(0.2, 0.7)
If _pRainY[p] > 140 Then
_pRainX[p] = Rnd(400)
_pRainY[p] = Rnd(-100, 0)
End if
Next
End if


the particle code is much more involved and deals with different types of particles.
method   SetupParticles( count:int, life:int, kind:int = 0, subkind:int = 0 )
Gives an example of how to set up the particles to do different things. Originally there was only one type but this was extended to multiple behaviours hence the case giving more options
This was written just for Vivamortis and was a bit dirty because I wanted something fast.

I could take the particle code, wrap it in a manager with oop and be able to reuse this in another project. But the code would need to extended as I added features that are currently not present (when I need them). it would still operate in vivamortis, but would be overkill as it would do loads of things that vivamortis did not require.
Similarly when used in a new project there will be lots of the vivamortis stuff that was not needed or used.

oop (at its worse) can tend towards abstraction creep: in that you can keep abstracting and using polymorphism, etc until you have lots of lovely oop code that is elegant and ticks all the boxes - but is very hard to read and maintain. oop at it's worse can be used to prevent access into the code, for no reason other than to be clever.

If we look at procedural code and particularly If and case and nested code. They are seen (by education) as bad. because they produce long pages of replicated code (spaghetti). - "This must be broken into manageable chunks" is a good mantra.
My answer here would be yes and no. If you are a learner - absolutely yes.
The moment you get into c/c++ and os systems. we're talking low level. you find all those nice educating lessons are dropped. You have loong pages of case/if statements, etc.

You mentioned json/xml files and I know that (derron) loves those. My (very) personal thoughts are 'they are the worse programming crap i've come across'. The reason I will not use or support them in any way is to do with forced abstraction and data hiding.
As an example:
I worked on a large commercial project (still operational) that used xml as the core setup. It loaded the xml parsed it and filled in the init data. Until it went wrong - which it did. that was when I saw the contents of the xml file. everything was a hash code. so reading the xml file gave you no help as to what the data was. You needed to have the hash codes translated or written down for you to know what was the data meant.
so (for example) to edit the xml to change the default x position on the window you need to find:
#dj84029fjfnvkeng928273kgmklengjenwelw9485667392o237y5, -3465
not
#dj84029fjfnvkeng928273kg4klengjenwelw9485667392o237y5, -3465
and never
#dj84029fjfnvkeng9Z8273kgmklengjenwelw9485667392o237y5, -3465

xml is just text. if a human need to be able to read it then use HUMAN READABLE CODE

Why not use
[display properties]
window_x = 32

This sort of brings me back to Derrons numbers vs Const
TILE_KIND_GRASS:int = 167
[...]
SPRITE_GRASS:int = 17


Great SPRITE_GRASS=17 is obvious. although i do have to check in the constants when I need to know the actual value of SPRITE_GRASS. and the number (17) will change over it's life, so I cant really use ENUM and need to use CONST.
But wait...
I've now got 3 kinds of grass
SPRITE_GRASS = 17
SPRITE_GRASS1 = 15
SPRITE_GRASS2 = 19

and I've added a new kind:
KIND_LEAVES and a new sprite
SPRITE_LEAF = 17

but isn't SPRITE_GRASS = 17? Yep, but the grass graphics are in _fontBig and the leaf graphics are in _fontSmall

When I run the program the leaf graphics show a skull?
What was the number of the leaf?
load the const file and find the definition leaf =17
what was the skull graphic code?
check the number of the leaf and the skull in another program

Of course I could have just had a simple number which was the direct reference. leaf = 18 change number to 18 in code


Derron

Regarding numbers, consts, fields ...

I think I did not properly understand what benefits you see to have eg. "167" written all across your code (which might be multiple files) instead of having a TMapConstants-type which holds all these information for you ("number/string"-holders are nice as they do not have complex dependencies and you can import them in all files requiring access to it).

Now assume we have a TMapConstants with globals/fiels like "KIND_GRASS1" (or 2 or 3). Everywhere in your code you replace the 167 with "TMapConstants.KIND_GRASS1". If you somewhen change the number it automatically changes it everyhwere.

If you decide somewhere to not use grass 1 but grass 2 - you replace that stuff there with KIND_GRASS2 which seems to be not more work than replacing the 167 there with the new number (which you have to lookup then of course too).


So how to do "random grass" then? TMapConstants could have some helper functions - and or contain arrays which reference the numbers: TMapConstants.KIND_ALL_GRASS:int[] = [167, 168, ...]. Then just pick a random one and you are good to go. Or you add a function "GetRandomGras:int()" which does a similar thing by defining there what belongs to grass and what to return from them.


...

I do not see a reason to use hardcoded numbers/strings in the code especially if you use it more than just 2 times.



@ Particle Systems
To avoid udderly complex basic types you could play a bit with the component approach (adding behaviours etc). That way the TParticle only contains eg a position and some abstract update() and render() methods which loop over attached components. Or - if you dislike components because of performance issues you just extend TParticle into your own to skip the whole components iteration which means you eg. just have a single byte pointer to "components:TComponent[]" (an empty array) which could be neglectible.

components are then special movement behaviour, child particles, attached emitters or whatever you can imagine.

bye
Ron

iWasAdam

I think you're over thinking thing a bit Derron
Const are actually very useful/helpful - especially when the actual (internal) code for the const is irrelevant.

In the cases I have been using (and which were commented about). The values themselves are extremely important: Can (and will) be non linear. Also will change as development progresses.

Using the sprite example
Lets assume that DrawSprite( SPRITE_GRASS, x, y ) is replicated to use grass1&2, and that the values for the sprites has changed <- a very real situation

It is simple to copy DrawSprite( 17, x, y ) to become DrawSprite( 34, x, y ), DrawSprite( 14, x, y ) and the values are very simple to check

DrawSprite( SPRITE_GRASS, x, y ) on the other hand may become DrawSprite( SPRITE_WALL, x, y ), or DrawSprite( SPRITE_WALL3, x, y ). which I have to go and make sure I have SPRITE_WALL3, what it's correct value is etc.

I'm not saying that Const values are wrong - just use them where they fit best.

;D

iWasAdam

Ohh, one thing to add Re the Sprite vs Const.

Vivamortis uses 8 fontsprites, so that is a potential 8x256 = 2048 const needed for SPRITE_...

My personal thought is that a number is better that wading through 2048 const definitions

And if you look throught the vivamortis code you will see I do use consts, and fields to store permanent variables to make thing simple.
And some a variables and not consts, as they are allocated at runtime (their number being irrelevant just unique)

Derron

irrelevant numbers: objects have an .id and something referencing it eg stores "tileID = object.id".


@ DrawSprite( 17, x, y )
Replacing "SPRITE_WALL" with "SPRITE_GRASS123" is a bit less error prone than replacing "17" with "64" (as it affects 177 or 717 too - so you need to make clever "(17," replacements etc.).

I understand that there might be way too many constants but this is only because you need 2048 sprites - which could be way less with grouping (sprites_player[0...9]) or parenting (spriteGroup.Add([sprite1, sprite30, sprite39]).
Just think that wordy expressions help more than having to constantly lookup 2000 numbers.

This is one of the reasons why some stuff could be written in ini, xml, json, whatever format to store it once and automatically use it in the engine without having to take care of what exact type is placed there and how it was numbered.
Somewhere you have the description if something is blocking or not. If this item is used in the map the engine automatically sets some properties (blocking...) and you never touch the tile ID in code again.

Next to static/basic tiles you could have dynamic tiles which need special handling via code. Of course "simple" dynamic tiles could even be defined via external files by providing some basic effects for tiles: "pickup", "pickupObject=objectID" or "pickupRandomObject=object1ID,object2ID" ...
This of course becomes more useful if working: in groups (level designer does not need to be able to modify code) or if you want stuff to be moddable / level editor.


Of course not everything must be overcomplicated and not everywhere OOP is needed.

bye
Ron