bbodi / rustarok Goto Github PK
View Code? Open in Web Editor NEWMultiplayer, fast-paced Moba style game
Multiplayer, fast-paced Moba style game
This is the first weekly blog post about the project.
This was a very productive week, I spent around 30 hours on the project.
The biggest achievement for this week is definitely the Status system. You can find the design document for it here, and I plan to write a follow-up post about the difficulties and differences from the original design.
Some gifs demonstrating the capabilities of the new Status system:
A README file was created for the project on github
Design documentation about Statuses and the new Graphic system
Camera follows the character (can be turned on/off by pressing 'L')
"Remaining time for status" bar above the character (the orange decreasing bar in firebomb.gif)
'absorb' and 'block' texts are rendered if the attack was absorbed or blocked.
Statuses can modify incoming damages on characters (decrease their values, block/absorb them etc)
Font loading and basic text rendering
Faster walking speed makes walking animation faster
Skill casting animation has changed: Only the first frame is rendered during casting, and when it is finished, the whole animation is played. Now this post animation can be interrupted by other actions (e.g. casting other skill or walk away), but in the future I might introduce a mandatory delay after casting.
Skill bar
Rendering skill name during target selection
Browser clients and streaming works again (ultra basic and inefficient yet) (low resolution is because of github's 10MB file size limit)
Embedded web server for browser clients
Self casting (ALT + skill key)
I'm finishing up a version in a container for linux, and the last bit that I need to know is where exactly the game expects to have write. I know that cargo/debug/.cargo-lock needs write, but the error message doesn't tell me where else it would be needed:
Singularity rustarok_latest.sif:/code> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target/debug/rustarok`
[00:00:15.836] (7f8d990edf40) INFO GRF loading: 15836ms
[00:00:16.444] (7f8d990edf40) INFO rsw loaded: 144ms
[src/asset/gat.rs:137] non_walkable_cells.iter().filter(|&&it| it).count() = 60788
[src/asset/gat.rs:184] rectangles.len() = 1317
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 30, kind: Other, message: "Read-only file system" }', src/libcore/result.rs:1084:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
As soon as I know this, I think it should work!
First, by status, I mean supportive or destructive effects on an entity, like buffs, stun, frozen, poison etc.
They should be able to
Multiple statuses can have different effects on rendering the entity, e.g. one Buff can increase the size of the entity, and a Poison can make it green. In that case we have to render an increased, green sprite. If that entity then gets stunned, then we have to render the stun effect above the increased, green sprite (so the y offset of the stun effect will be different than for a normal sprite).
Player attributes (atk, armor etc) are modified by statuses. The naive approach would be to have for example an atk
field on entity, then when a Buff status is applied on that entity, it would modify that atk
field, like adding +10% to it.
However it would lead to unmaintainable and complicated code.
atk
(which is increased by 10%), and then the second one removes 10% from the reduced atk
.atk
has a specific value. If there is a bug, and some status forgets to clean up his work, it is more difficult to find that bug.atk
of a character, so you can't just modify it.So rather I choose immediate calculation, where entity attributes are calculated on demand, in every frame if necessary. It would have a base attribute data structure, which then would be copied and modified by all the active Statuses on the entity. That copied attribute structure will be used for damage calculations.
My assumption is that there won't be much Status on an entity at the same time, maximum ~10. In a team fight, they change rapidly, then have a bigger time span when they don't change at all (e.g. go into a team fight will add/remove many statuses on almost all the players. After and before the fight it almost does not change at all).
Many skills and effects will be implemented by statuses, which first does not seem as a Status. E.g. a wizard put a fire bomb on a character. Before expiration, it renders some fire effect on the character, then on expiration it damages the character and its area. This is a perfect example for a Status, however it is rather strongly related to the "fire bomb" skill, and I would put the code of this Status in the same file as the skill which created the status.
Since some game logic has to figure out if a specific Status is applied on an entity (e.g. some skills damages more on Stunned entities), some States have to have an identifier, a "type". But I would not give an own enum for every "not so important" status, since they won't be queried for and other parts of the code does not have to know about them.
So to avoid polluting the code with those Skill-related statuses, I distinguish "important" and "not so important" Statuses.
Important Statuses are the ones who are known globally, does not relate to one concrete skill, and used heavily by other codes, statuses, calculations etc. For example:
The enum will look something like this:
pub enum StatusType {
Stun(ElapsedTime),
Frozen(ElapsedTime),
Root(ElapsedTime),
Sleep(ElapsedTime),
// "not so important" statuses
OtherHarmfulStatus(Box<impl Status>),
OtherSupportiveStatus(Box<impl Status>)
}
With this enum, it is possible to implement skills like "remove all negative status from the target".
Status
will be a trait with functions like
fn can_target_move -> bool
fn can_target_cast -> bool
(e.g. being rooted disallows moving but allows casting skills)fn get_render_effect(&self) -> ???
(It might be necessary to render more than one effect on the target, so the return type is in question)fn get_render_color -> [f32; 4]
fn get_render_size -> f32
fn calc_attribs(&self, &mut CharAttribs)
fn update(&mut self, now: ElapsedTime)
fn get_duration_percent_for_rendering() -> Option<f32>
: This is for rendering a decreasing status bar above the character which tells when the next status will expireEntities will have a Vec<Status>
field.
Then:
Vec
in insertion orderVec
and call those functions to determine attributes and rendering properties of the target entityTo avoid elements to be moved when Statuses are added to the Vec
, I might use an [Option<Status>; 32]
instead.
Here, the term entity is used as in Entity Component Systems.
Currently, utilizing the Entity Component System, specific entities are responsible for rendering Sprites or Effects.
E.g. a skill, which wish to render a lightning bolt on an area, has to create an entity with the details of the animation (when it started, what is the effect id, when it ends etc), then a system will pick up that entity and render it.
impl PushBackWallSkill {
pub fn new(...) {
let effect_comp = StrEffectComponent {
effect: "StrEffect::FireWall".to_owned(),
pos: effect_coords,
start_time: system_time,
die_at: system_time.add_seconds(3.0)
};
let effect_entity = entities.create();
updater.insert(effect_entity, effect_comp);
}
Later, the skill is responsible for removing the effect entity:
impl SkillManifestation for PushBackWallSkill {
..
fn update(&mut self,
self_entity_id: Entity,
updater: &mut specs::Write<LazyUpdate>,
...) {
if self.die_at.has_passed(system_vars.time) {
updater.remove::<SkillManifestationComponent>(self_entity_id);
for effect_id in &self.effect_ids {
updater.remove::<StrEffectComponent>(*effect_id);
}
The other option would be to initiate rendering directly in the update function of the skill.
e.g.
impl SkillManifestation for PushBackWallSkill {
fn update(..) {
...
render_sys.render_effect(
"StrEffect::FireWall",
effect_coords,
self.effect_started,
self.effect_ends_at
);
}
}
One advantage of the first approach is cache utilization. StrEffectComponent
can be stored in a Vec
, so the system who will render those components can iterate through them easily, achieving great cache utilization.
An other advantage is "fire and forget" rendering: It is enough to add an effect to the system once, and the effect will appear and be animated without any further effort.
The disadvantage of it is that the skill- and its visualization logic are separated.
The advantage is not that obvious anymore if we consider real world scenarios, like
EffectComponent
and modify its position.Command
structures, then the rendering system can optimize and render those Commands
, so cache utilization advantage would be moved to the the render system level anyway with either approach.Personally I like the second approach more. It feels that effects/sprites in this situation belongs to the skill, and it is the skill's responsibility to manage them. However I would keep StrEffectComponent
, but for simpler cases, like when I want to put an effect on the map, which does not have any "owner", just stands there alone (e.g. for debugging purposes, to check how an effect look like on the map).
As an explanation on why I chose to put rendering data into the Skill, but I don't do it in case of Characters for example, is that Skills are stored as a Box
object anyway.
Almost every Skill has a different data structure and update logic, and their living instance numbers will be very few, so it is impossible to store them in a Vec
. Which means I don't get any benefit separating the rendering logic from the Skills.
But in the case of Characters, both the Characters and their rendering data (SpriteRenderDescriptorComponent
) have a homogene structure and stored in a Vec
as components. And there are systems in the code which uses solely either one or the other, so they are not coupled strongly.
Right now there isn't a separate graphic module in the code, different systems access OpenGL to render whatever they want.
So the idea is to have one rendering layer which collects high level rendering commands (e.g. "draw a sprite here" or "draw a circle there").
It then processes, prepares and renders those commands.
pub struct RenderCommandCollector
This will be a component, and every ControllerComponent
(a controller represents a human player) will own an instance from it.
It will be responsible for collecting rendering commands from the systems. Currently I don't plan to put any preprocessing logic into this struct. The sorting will happen in the RenderSystem
, before the rendering, not by this struct.
It will contain several Vec
s to store specific commands, one for rendering models, one for sprites, one for circles etc.
pub struct RenderSystem
This is the system which is responsible for processing and rendering the commands obtained from RenderCommandCollector
s.
It will have many cached VertexBuffers for dynamic objects.
Achievable goals:
Is this game supported for running on linux? If the Rustarok files can be distributed, we could make a container to serve it. Do you have any direct (programmatic) links for download? I'd like to try / play, but I use Linux.
Use SDL_TTF and create an atlas texture from the letters.
Should work both in 2d and 3d.
I'm trying to understand the code for sprite loading, and I've been struggling to understand the different implementations for loading indexed images with and without RLE encoding.
Both
rustarok/client/src/grf/spr.rs
Line 44 in 7ea7abf
rustarok/client/src/grf/spr.rs
Line 46 in 7ea7abf
fn read_indexed_frames(buf: &mut BinaryReader, indexed_frame_count: usize) -> Vec<SprFrame> {
(0..indexed_frame_count)
.map(|_i| {
let width = buf.next_u16();
let height = buf.next_u16();
let frame = SprFrame {
typ: SpriteType::PAL,
width: width as usize,
height: height as usize,
data_index: buf.tell(),
};
buf.skip(width as u32 * height as u32);
// buf.next(width as u32 * height as u32).to_vec();
frame
})
.collect()
}
fn read_indexed_frames_rle(
reader: &mut BinaryReader,
indexed_frame_count: usize,
) -> Vec<SprFrame> {
(0..indexed_frame_count)
.map(|_i| {
let width = reader.next_u16();
let height = reader.next_u16();
let data_index = reader.tell();
let size = reader.next_u16();
reader.skip(size as u32);
SprFrame {
typ: SpriteType::PAL,
width: width as usize,
height: height as usize,
data_index,
}
})
.collect()
}
Am I not seeing something, or maybe is this how they are actually supposed to be?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.