Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411478 Posts in 69369 Topics- by 58424 Members - Latest Member: FlyingFreeStudios

April 23, 2024, 06:23:06 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsCANTATA - (1.0 OUT AUGUST 15th) Sci-Fi Advance Wars + Logistics + Base-building
Pages: 1 2 [3] 4 5 ... 9
Print
Author Topic: CANTATA - (1.0 OUT AUGUST 15th) Sci-Fi Advance Wars + Logistics + Base-building  (Read 29493 times)
bacon
Level 1
*


View Profile
« Reply #40 on: February 09, 2017, 07:19:22 PM »

So besides working on the actual game, in the background I've been working on refactoring the way the tile rendering was going on. As you may have seen from a screenshot posted earlier, the way this works right now is by just instantiating a GameObject for every tile. This works great if you want a map that's only 5x5, but this is a strategy game! The maps have to have the ability to be vast! The system chugged pretty hard with even 30x30 tile grids, so I knew that sooner than later I was going to have to change the rendering system to be somewhat bespoke.

I found my initial guidance in the tutorial series Quill18 did on his gamedev channel all about doing tilemaps "the right way". While this was a great start, there's a pretty big gap at the very end of episode 9 where he talks about doing individual verts for every tile, but then never makes a follow up video on actually doing it!

I used what I had learned up to that point, and finally got it mostly implemented!



I'll post the code later, as I feel like this is pretty useful for other people in a similar position to myself. I know Unity is also working on their new 2D features that have some sort of tilemap functionality, but so far they haven't provided any sort of API for interacting with it, and it looks mainly to be used for Gamemaker style level editing. I'm still trying to get the UVs right, but here's a preview of the tiles being drawn:



The biggest takeaway I learned here was to, instead of trying to draw verts in rows like you would if you only had a single set of verts on every row, to set up the code so that you draw the verts and tris for a square, and then just offset down the line, doing a square at a time. I was being somewhat thick trying to do the verts down a line, which was... difficult. I had a small brainblast tonight and swiftly implemented the working code, and am glad to say it seems to be working Smiley

For those wondering too why I didn't want any of the tris to share verts: a single vert could only ever have a single UV coordinate. This meant that, like in Quill18's tutorial, you have to procedurally build the proper texture to fit your map. With unique verts on the tris, I can now just reference the tilemap texture directly!
« Last Edit: May 06, 2017, 08:18:41 AM by bacon » Logged

CANTATA
s_harriton
Level 0
***



View Profile
« Reply #41 on: February 09, 2017, 08:43:56 PM »

The art is very classy! Good work!
Logged

bacon
Level 1
*


View Profile
« Reply #42 on: February 26, 2017, 08:42:53 AM »

Hey Everyone!

So I'm really excited to say that I finally released the tilemap rendering library I was working on for the game! You can download it on Github here, with the Asset Store release currently pending.



Here's the blurb:
"SimpleTilemap is a totally free library for generating tilemaps in Unity at runtime. It provides users with a single script that, when used with a tilemap sprite, will generate a custom textured mesh with tilemap-indexed tiles, using a single GameObject. Its intended use is for 2D pixelart games with orthographic cameras, and has not been tested or verified with 3D assets."

I'm now working on getting it implemented into the actual game, which already requires some modification of the base library to support the ability to render the single mesh with multiple materials. I didn't bake this sort of stuff into the library itself yet because again, I wanted it to be simple vs. fully-featured. However, it's great, as implementing it is also guiding other refactors to how map generation is working, and deleting old/redundant code is always a good feeling Smiley

Because this is the devblog, I wanted to go a bit more into how it works. As I think I said before, it started with Quill18's youtube tutorial on doing tilemaps the "right way", but his tutorial stopped short of doing it really the right way, so I started from about where he left off, ripped out all the superfluous stuff, and then made it my own.

The most major difference is that now, if you followed his tutorial, I no longer generate a texture in memory that maps 1-to-1 to the generated mesh. He did this because he wasn't giving every tile unique UVs, so had to basically generate a texture that would fit perfectly on the generated mesh. I ripped out all that, and now just directly reference the texture material stored in Unity's MeshRenderer. In code it basically looks like this:

Code:
//create a new material from the "prefab" material so you don't destroy the prefab
Material mat = new Material(tilemapMaterial);
mat.mainTexture = tilemap.texture;
mr.sharedMaterial = mat;

The "trick" then is basically assigning each tile both its own verts as well as the proper UV coordinates that coorespond to the supplied sprite Tilemap. That happens here:

Code:
public void AssignSpriteToTile(Tile targetTile, int tilemapIndex, bool shouldUpdateMesh)
{
        //get starting vert for the tile
int rowVerts = mapSizeX * 6;
int vertIndex = (targetTile.y * rowVerts) + (targetTile.x * 6);

//convert tilemapIndex to x,y of tile, with 0,0 being the bottom left tile to make finding UV cords easier
Tile mapIndex = new Tile(tilemapIndex%tileColumns,(tileRows-1) - (tilemapIndex/tileRows));

//assign new UV coords based on tile index
MapInfo.uv [vertIndex + 0] = new Vector2 (tileUVWidth                * mapIndex.x, tileUVHeight + tileUVHeight * mapIndex.y);
MapInfo.uv [vertIndex + 1] = new Vector2 (tileUVWidth  + tileUVWidth * mapIndex.x, tileUVHeight + tileUVHeight * mapIndex.y);
MapInfo.uv [vertIndex + 2] = new Vector2 (tileUVWidth                * mapIndex.x, tileUVHeight                * mapIndex.y);
MapInfo.uv [vertIndex + 3] = new Vector2 (tileUVWidth  + tileUVWidth * mapIndex.x, tileUVHeight + tileUVHeight * mapIndex.y);
MapInfo.uv [vertIndex + 4] = new Vector2 (tileUVWidth  + tileUVWidth * mapIndex.x, tileUVHeight                * mapIndex.y);
MapInfo.uv [vertIndex + 5] = new Vector2 (tileUVWidth                * mapIndex.x, tileUVHeight                * mapIndex.y);
if (shouldUpdateMesh) {UpdateMesh(false, false, false, true);}
}

For the library I'm also not using Unity's default "multiple" sprite import setting, as it doesn't seem to really allow you any added benefit of easily referencing the split sprites, and seems to still treat it as a single texture, but with hidden UVs or something. If anyone has any guidance here I'd love to here it! For now, you basically enter your tilemap settings in the SimpleTilemap component, which allows you to virtually split the map into different tiles that can then be referenced by a given index.



I'd love to hear if anyone finds this library useful or has any other suggestions! Someone on Twitter already suggest just using four verts for the tiles instead of six, so I'll definitely be incorporating that option in the future.
« Last Edit: May 06, 2017, 08:20:59 AM by bacon » Logged

CANTATA
foofter
Level 4
****


MAKE THAT GARDEN GROW


View Profile WWW
« Reply #43 on: February 26, 2017, 12:11:45 PM »

I don't understand the technical stuff, but I still can't wait to play the game! I just love the colors and style, and isometric strategy games are my literally favorite genre! GO FOR IT!!!  Kiss Kiss
Logged


@_monstergarden (game) and @williamzwood (me)
See Monster Garden progress here: https://forums.tigsource.com/index.php?topic=56012.0
bacon
Level 1
*


View Profile
« Reply #44 on: February 26, 2017, 02:26:20 PM »

I don't understand the technical stuff, but I still can't wait to play the game! I just love the colors and style, and isometric strategy games are my literally favorite genre! GO FOR IT!!!  Kiss Kiss

Ah this is so kind! Thanks for the kind words. I'm really excited to make it and glad people are interested! Also, thanks for the twitter follow Smiley
Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #45 on: May 11, 2017, 08:49:34 AM »

 Adventures In Modular UI Programming

So one of the core aspects of this game that I’ve talked about before is the fact it’s highly moddable, which is a great idea in theory but then can be a pain in the ass in terms of actual implementation. You have to be programming with at least some sort of abstraction in mind, defining how you program through the edge cases vs. discrete states. This is to say, assume nothing.

This has recently been coming to a head in development in how I’ve been programming the UI. UI is usually a pretty static thing, but because I can assume almost nothing about the interactables that you control during the game, I can assume almost nothing about the UI. Compounding frustration with that on top of trying to figure out how Unity’s UI works, have made the past bit of development pretty frustrating. However, what I’ve noticed in programming the interaction UI is that a lot of the core parts of how the game works are implemented! It’s pretty rare right now for me to go back and redefine how something works on a core level, which has made the actual function linking for UI be a breeze on that end. Granted a lot of implementation is still in a first pass state, but this is all to say that that the game is becoming close to playable (with a ton of caveats)!

Going back to the UI specifically, this is what it looks like right now:



For a bit of explanation, the left panel lists what are essentially “inputs”, and the right panel lists “output”, which can be anything from a supply line to a building to a unit. These two panels encompass the “Supply Management UI”. The orange bar at the bottom lists the functionality of the current interactable, the “Interactable UI”. The bottom grey bar is the actual match UI, which is persistent. Note this is all also a first pass, which has also led to frustration knowing that a lot of work will probably be scrapped when I find an actually good UI paradigm. The interactable UI and supply management UI are persistent, and just display the info of what is currently selected. This means that every interactable has to internally store its state in some way, which is then called upon when the interactable is selected. To do this, I’ve been using simple structs stored inside the elements the UI, the “view”, actually represents. So for a SupplyProject, it internally stores what its UI representation should be. Then when it is activated, it serves that data and allows it to be written to through interaction with the UI itself. In a way then, I’m basically serializing UI state on a per interactable basis.

I think this is why games that have modular UI components (thinking RTSs/MOBAs), have very static “template” UI’s that are just populated with the right buttons for what is selected. The ugly part about that is that you get the “empty box” problem when you may have a unit that can do only three things, but still has six empty boxes displayed. The most recent example of this is probably Dawn of War 3 (look in the bottom left):



This system uses a grid of buttons, which probably behaves where they enable a generic button, skin it properly, and then link it to a function call. Me, an indie game developer, am instantiating new, arbitrary buttons when the intractable is selected, each linked to the proper function. However, when a unit is selected, the UI looks more “unique” to that interactable, vs. showing a lot of empty space where functionality for other, unused buttons would go. What this also allows me to do is potentially different layouts for buttons, as functionality is bound to the specific instanced button, not a prefab. For a bit of code, this is also the secret runtime function binding code for a Unity button:

Code:
yourButton.onClick.AddListener( () => {yourFunc();}); 

This makes it possible to bind to arbitrary events as well, which is a godsend.

Lastly, I wanted to show the UI “design pattern” I’ve kind of landed on to support this. To start, here’s a simple snippet of code:

Code:
public class SupplyProjectProviderUI : EventedMonoBehaviour
    {
        //The provider governing this UI
        public ProjectProvider ProjectProvider { get; private set; }
        [SerializeField]
        private Text providerName;
        public Text ProviderName { get {return providerName;} }
        [SerializeField]
        private Text providingAmount;
        public Text ProvidingAmount { get {return providingAmount;} }
        [SerializeField]
        private Button increaseProviderAmount;
        public Button IncreaseProviderAmount { get {return increaseProviderAmount;} }
        [SerializeField]
        private Button decreaseProviderAmount;
        public Button DecreaseProviderAmount { get {return decreaseProviderAmount;} }
        public void Init(ProjectProvider p)
        {
            ProjectProvider = p;
            providerName.text = p.Provider.Interactable.name;
            providingAmountNum = p.Pack.Amount;
            UpdateTextAmounts();
        }

        public void UpdateProvidingAmountByNum(int num)
        {            
            ProjectProvider.Pack.UpdatePackAmount(providingAmountNum);
            UpdateTextAmounts();
        }

        public void UpdateTextAmounts()
        {
            providingAmount.text = ProjectProvider.Pack.Amount.ToString();
        }
    }

The main things to notice are the way the instance variables are declared, and the fact the UI component stores a direct reference to the object it is representing. This is probably something like MVC for you computer scientists out there. For instance variables, each one has a private representation that is marked [Serializable], and a public property that allows you to access it. The reason for this is so that the actual button can be set from the inspector, making binding to an actual button easy. This is done instead of setting it public in order to ensure that var is only ever acted upon through the UI itself, hence the property. To update the values, you check against the actual object to see if you can update, then push those changes to the UI.

So, when the UI is activated, it just calls the Init() function with the proper parameter, binds the proper function to the proper button (through the property), the values are updated based on the cached values of that parameter, and then the UI element chugs on as if nothing happened. Changing interactables just illicits another call of Init()/binding.

I think that’s all for now, but it’s also worth saying that I’ve been working with an artist to do the CO art, and we’ve got three of them already and they look fantastic. I’ll probably hold off to roll these out properly, but until then, thanks for reading! Also, follow on twitter @isotacticsgame!

Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #46 on: May 18, 2017, 05:56:21 AM »

Been really hammering away at the UI stuff this past week or so. I've finally got it where it's pretty functional, and allows you to route incoming supply to different supply projects. The hard part about doing a lot of this is that you have to 1) add functionality to add something, and then 2) add functionality to remove something, then 3) realize the systems are kind of incompatible and have to refactor a new "golden" solution that works both ways.

The way I'm doing it right now I've got the actual logic objects talking to each other to modify their state, and then the UI kind of tags along and just gets updated based on what it knows. This way I don't run into any issues with the UI modifying state unexpectedly. Here's what it currently looks like:


Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #47 on: June 06, 2017, 05:11:03 PM »

Just a small update but I just started a newsletter for the game! Sign up here!!
Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #48 on: July 29, 2017, 04:34:37 PM »

Hey everyone! Small update, but I've been working on helping out this Steamworks library to make it so that it has the ability to use Steamworks' lobby system. Steamworks.NET is kind of archaic in the way that you go about using Steam, and I saw an opportunity to help out here to hopefully help this library become more widely used/adopted! To make a lobby in Steam, you basically just have to do a single call of SteamClient.CreateLobby()!

Also in working on this I found that there are almost _no_ good high level tutorials on how the Steamworks API actually works, so after I finish up this little sprint I'm going to write up a high level tutorial of the library to kind of demystify how steam works.

Until next time!
Logged

CANTATA
kinnas
Level 5
*****



View Profile WWW
« Reply #49 on: July 30, 2017, 10:52:13 PM »

Not to get too suprematist or anything but isometrics is totally the best (lack of) perspective.  SmileyHand Thumbs Up Right

Game looks dope, gonna keep an eye on this.
Logged

foofter
Level 4
****


MAKE THAT GARDEN GROW


View Profile WWW
« Reply #50 on: July 31, 2017, 07:01:59 AM »

Just signed up for the newsletter. There are only a handful of games that seriously turn me on graphics-wise, but this is one of them, and I love isometric SRPGs to boot, so PLZ get this done so I can play it!!  Beer! Coffee Gentleman Hand Fork Left Gomez Hand Knife Right
Logged


@_monstergarden (game) and @williamzwood (me)
See Monster Garden progress here: https://forums.tigsource.com/index.php?topic=56012.0
Sokuaisushi
Level 0
**


Sole Developer of The Void (Title WIP)


View Profile WWW
« Reply #51 on: July 31, 2017, 07:38:07 AM »

I love the art style and the direction! Advanced War was a great turn based strategy game, and I'm sure this will be great as well! I'll definitely be following the project to see where it goes, and I hope your project is going well Smiley
Logged


A fast, yet still rogue-like RPG.

Facebook - Twitter - Trailer - Video Devlog
bacon
Level 1
*


View Profile
« Reply #52 on: August 11, 2017, 05:44:32 PM »

I love the art style and the direction! Advanced War was a great turn based strategy game, and I'm sure this will be great as well! I'll definitely be following the project to see where it goes, and I hope your project is going well Smiley

Ah thank you so much!! Smiley

The project moves along, though somewhat slowly as noticed by the pace of (indie strategy games are hard). I do have a small update though! I'm super happy to say that my pull request to the Facepunch.Steamworks library was accepted, so the Lobby functionality I added is now an official part of the library! I'm writing up a post that walks through the library in general so other indie devs can know where to start/how to start using Steamworks.

What this also means is that you can now make a lobby in the game, join the lobby through steam, and then get punted into the main scene. Next up is working on P2P connection stuff to actually send data back and forth, but needless to say I'm super excited with how everything is coming along. I'm hoping to do some sort of limited access release by the end of the summer that gives people access to a basic set of modding tools and the map generator, and from there start fleshing things out.

It's also hard doing a lot of this stuff because it doesn't "show well". I'd love to post more in #screenshotsaturday and places like that (and more here!), but right now it all looks very similar. If anyone has any questions about anything that I'm doing though (or what I'm actually doing at all) I'd love to know and be happy to answer! Regardless, look forward to a decently long thing here soon about using Steamworks!

Here's a screenshot of me and someone else in a lobby together over steam Smiley
« Last Edit: August 11, 2017, 05:57:08 PM by bacon » Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #53 on: August 17, 2017, 01:07:23 PM »

Okay it's finally here! I ended up writing a lot more to kind of flesh out some points, but here's my post on getting started with Steamworks inside of Unity. I focus on the Facepunch.Steamworks library here, as it's a much more modern and easy to implement library for Steamworks than the incumbent, Steamworks.NET. I'm also happy because I posted it to the Gamasutra blogs section last night and today it is featured on the site! Let's get into it!

A Gentle Introduction to Steamworks
Unity, C#, Steamworks.NET, and Facepunch.Steamworks
Introduction

Like most game developers, I'm very interested in getting my game on Steam.

Like most indie game developers, I lack any sort of access to the resources/knowledge that would easily clarify for me what "being on Steam" actually means.

It's also hard to bridge this gap in knowledge because, though a lot of Steam's functionality is well documented, knowing where to start in that massive store of documentation, as well as what ties all the disparate pieces together, can be difficult. On top of that, general discussion about the Steam platform happens on a private forum that is only open to developers that have been approved for the platform already through either Steam Direct or a Valve referral. So for someone starting out, looking for answers to basic questions can be difficult.

Because of this, I wanted to write a high-level overview for people who are just setting out, trying to get Steam working with their games. Specifically, I wanted to dive into the Steamworks SDK, the Valve-provided software library that gives you access to things like Workshop, Leaderboards, Achievements, and so on.

Steamworks _is_ already well documented by Valve, but it's written from the perspective of someone who is using the native C++ library and likely already knows how all its functionality intersects. If this isn't you, even better! What follows is for that second person, someone who is making a game in a higher-level language and just wants a simple Steamworks integration (it exists, I promise!). More specifically, this post is targeted at people using C# in some for or another, but ideally using Unity as their game engine.

Steamworks

Steamworks is two things. First, it's the developer portal for managing everything about your game's presence on Steam, from changing your game's banner pictures to managing sales to listing controller support, etc. Secondly, it's an SDK Valve provides that lets you interoperate with everything _else_ Steam provides like Workshop, Leaderboards, Servers, Achievements, etc. Keep that order in mind! The SDK "is only required to upload your content to Steam", [source]. This means you can totally forego tackling nearly all of the aforementioned stuff of the SDK and just focus on how to use it to get your game on Steam. However, the SDK does offer a lot of other great, useful functionality. So let's get it up and running!

If you're a C++ developer you can go ahead and add the library to your game via the instructions here.
 
But, if you're a C#/Unity developer, we've got a little work to do. Because native C++ headers/source aren't compatible with Unity, we need to use a wrapper library that allows us to integrate the SDK's functionality. Such a wrapper library would allow us to use high level C# functions to call low level C++ functions. In the past, the go-to library for this was Steamworks.NET, which is exactly as it sounds: Steamworks implemented with .NET. However, it is exactly as it sounds, to a fault.

Steamworks.NET provides one-to-one mapping of Steamworks functions to C# functions, but that means you need to fully understand Steamworks as a library to get anything done. For someone who is just getting started and wants to do simple tasks, this can be a lot of work. If you want to do something more complex, Steamworks.NET requires you to essentially write your own utility wrapper on top of their wrapper, which seems to defeat the purpose of a wrapper in the first place.

Facepunch.Steamworks

Due to these limitations and other reasons, Facepunch Studios (of Rust and Garry's Mod fame) set out to build a better Steamworks library for C#/Unity with the explicit purpose of making everything easier.

It gets rid of the need for tons of code scaffolding to do basic (and complex) tasks in Steamworks, which lets you focus on just "working" with Steam. It's also currently being used in Rust, which means it is production tested on one of the highest user-volume games on Steam. Complex tasks are abstracted away into simple function calls, and the library itself is only three files so it doesn't cause any extra bloat. I can't emphasize enough how much more of a godsend it is for someone starting out; it's truly wonderful. The creator of Steamworks.NET has even said that Facepunch.Steamworks is "basically what I wanted the next step of Steamworks.NET to be" and that it "should probably be the default choice for most people". Steamworks.NET is still there for people who want to implement their own version of what Facepunch.Steamworks essentially is, but in my opinion, if it's good enough for Rust it's good enough for me. So how does it work and what's so special about it? Let's get started.

Getting Started

First off, It's easy to think that you need to be an approved Steamworks developer to start working with Steamworks, but the truth is that you can start using the SDK right now without needing to have gone through the process of registering. This is because Valve provides developers with a test "AppID" of 480 that you can program against.

The AppID
An AppID is how your game is uniquely identified on Steam (and Steamworks), and is one of the primary things you get when you get your game properly registered. It "stakes out" your section of Steam/Steamworks and allows you to wholly own anything associated with that AppID. AppID 480 corresponds to "SpaceWar", a demonstration game Valve made and open-sourced that shows of some of the capabilities of Steamworks (you should check it out!).

While a unique AppID is nice and obviously required for your game at some point, the test AppID (480) allows you to work with Steam's services _as if_ your game were live. Your real AppID should be substituted in once you get one, but in the meantime, 480 will serve you just fine. That said, probably don't create a server with the name "My 'Trademark Pending Game Name' Server".

Downloading and Importing Facepunch.Steamworks
So, let's download the Facepunch.Steamworks library (FP lib from now on), knowing we can properly test it with AppID 480. Head to the releases section of its Github page (the library is fully open source, MIT licensed) and download the latest release. Once you extract the .zip file you should see a few folders. The READMEs go into more detail, but you essentially need to copy a small set of these files over to your Unity project, depending on your platform. The Facepunch.Steamworks files are the lib itself, and the steam_api files are platform specific files that contain the actual Steamworks SDK. Your Unity directory should look something like this after your files are imported (assuming Windows x86/x64):

Code:
Unity Project Folder
|— Assets
    |— Plugins
        |— Facepunch.Steamworks
            |— Facepunch.Steamworks.dll
            |— Facepunch.Steamworks.pdb
            |— Facepunch.Steamworks.xml           
|— steam_api.dll (Windows x86)
|— steam_api64.dll (Windows x64)
|— steam_appid.txt

The steam_appid.txt is literally a text file with only your AppID in it, so for our cases it should just be a text file with 480 in it, no quotes. The .dll, .pdb, and .xml files come from the Release folder of the downloaded .zip file, using whichever .NET version you're targeting. For Unity, 3.5 is fine. If you're starting out with a clean project, Facepunch has also graciously provided a small test project that does a lot of this for you that you can you to start off your project.

Unity

Once you've copied all the lib's files to their proper directory locations, you're basically setup! All we need to do now is to write some code to get everything integrated. I'm going to copy over the test file from the test project and just abbreviate it here for clarity's sake.

Code:
using System;
using UnityEngine;
using System.Collections;
using System.Linq;
using Facepunch.Steamworks;

public class SteamTest : MonoBehaviour
{

    void Start ()
{
        // Don't destroy this when loading new scenes
        DontDestroyOnLoad( gameObject );

        // Configure for Unity
        // This is VERY important - call this before doing anything
    Facepunch.Steamworks.Config.ForUnity( Application.platform.ToString() );

        // Create the steam client using the test AppID (or your own AppID eventually)
        new Facepunch.Steamworks.Client( 480 );

        // Make sure we started up okay
        if ( Client.Instance == null )
        {
            Debug.LogError( "Error starting Steam!" );
            return;
        }

        // Print out some basic information
        Debug.Log("My Steam ID: " + Client.Instance.SteamId);
        Debug.Log("My Steam Username: " + Client.Instance.Username );
        Debug.Log("My Friend Count: " + Client.Instance.Friends.AllFriends.Count() );
}

    private void OnDestroy()
    {
        if ( Client.Instance != null )
        {
            // Properly get rid of the client if this object is destroyed
            Client.Instance.Dispose();
        }
    }

    void Update()
    {
        if ( Client.Instance != null )
        {
            // This needs to be called in Update for the library to properly function
            Client.Instance.Update();
        }
    }

}
And... that's it! If you attach that script to a GameObject in your scene and enter play mode, you should see yourself in Steam as playing "Spacewar", and see the console print out some basic Steam information about you (if not, make sure you are actually logged onto Steam).


Life with Facepunch.Steamworks
Capabilities
Once you're set up, accessing deeper Steam functionality is pretty easy since the FP lib covers and wraps almost all parts of the standard Steamworks SDK. The question does still stand though as to "what" that stuff is, so here's a small list and some descriptions of what you can work with (in the FP lib):

1. Servers - Create servers using a player's client or run a headless server elsewhere. This is used for ping-sensitive games that have steep networking requirements (think Dota 2, Overwatch, etc.)
2. Lobbies - These are essentially "staging rooms" for players that act as a meeting point to exchange SteamIDs or other user info.
3. Friends - You and your player's friends on Steam!
4. Workshop - Upload/Download content from Steam Workshop!
5. Leaderboards - Create and maintain global leaderboards for your game.
6. Achievements - Create or award achievements.
7. Networking - Send P2P data to clients.
8. Steam Cloud - Save data to Steam Cloud! This is very useful for Saves.
9. Voice - Interact with the Steam voice API for in-game chat.
10. Stats - Set Steam-side stats for a given player.

The best way to see how to use a given function is to see if there is a working example of it in the Facepunch.Steamworks test project (NOTE: this is _not_ the Unity test project), and model the implementation in your game.

Most of these features are documented further in the the FP lib's wiki, but only a few classes really properly filled out. If you don't see any example, check out the library's code and see if the function is implemented at all. If not, see how far you can get implementing it on your end, or just submit an issue to the library. The Facepunch team is generally very responsive, so they can let you know if something is being worked on or not and even  guide you on how to give back to the community, if you go the route of implementing it yourself.

Subscribers and Callbacks
When working with the FP lib (or even the native API), you'll notice it's not always as easy to work with as simply calling something like Client.Instance.SteamId. This is because the Steamworks SDK (and FP lib by extension) makes heavy use of asynchronous functions so that your game doesn't hang every time you want to do some non-trivial interaction with Steam. Without async calls, you would have to wait for the main Steam server to respond before any more of your code could execute, which is obviously error-prone and frustrating, as far as play experience goes. So, to use the library, you will need to get familiar with the idea of delegates and callbacks. It may be a lot for someone just getting started, but it's easy to understand once you grasp the core idea. I'll give an example.

If you want to get a list of all the lobbies in your game using the FP lib, you would do this:
Code:
client.LobbyList.Refresh();
Notice how there is no return or assignment happening there. If that's the case, how do we actually get what we just requested? In Steamworks, once this function is called, the Steam backend gets everything ready for you, and then sends you some data via a "callback". Almost literally, Steam "calls you back" to say "Hey, your data is ready! Here!".

To receive the call, we need to pick up the phone, or as it's actually known, "subscribe", to the callback. This is done by defining a function that takes in the data the callback provides. Sometimes, no data is provided, and the callback is used primarily as a "hook", or a way to know that it's OK to resume. Once you're inside your callback handler, you can safely continue operation. Here's an example:

Code:
void Start()
{
    // Subscribe to the proper event by defining the function you want to be called when OnLobbyJoined is called by Steam.
    client.Lobby.OnLobbyJoined += OnLobbyJoinedCallback;
    // Call the function
    Native.Lobby.Join(LobbyID);
}

void OnLobbyJoinedCallback(bool success)
{
    // If we're inside here, the callback has properly fired!
    // We can check the status of this callback by seeing if the bool "success" that Steam passed in is true or false
    if(success)
    {
        // If there were no errors, we can safely call functions in here that require a specific circumstance
        // For example, we can now print out the id of the Lobby's Owner
        Debug.Log(client.Lobby.Owner);
    }
}

Understanding this pattern will be immensely helpful as you use the library, as well as for understanding what Steamworks is actually doing when you inevitably peek at the actual documentation. If you want a more in-depth discussion of how this works, I definitely recommend Valve's documentation on the topic, as well as some bits on the Steamworks.NET website.

Moving Forward

From here on, you're free to implement whatever you want! Valve has no requirements here, but if you are on the platform it is obviously in your best interest to engage with it and its community in the ways Valve has made available. Once you get registered with Steam Direct you can simply swap in your AppID and carry over any Steam functionality you implemented with the test AppID.

I really hope this is helpful to anyone looking to get started with Steam, and I obviously strongly recommend people check out Facepunch.Steamworks on Github. If you feel so inclined, try contributing in small ways like filling out the documentation, or if you want something meatier to chew on submit an issue and ask if anything needs doing. If you have any questions about this article, feel free to reach out to me @kkukshtel. I'd also love it if you followed my game on Twitter @isotacticsgame or sign up for the newsletter.
Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #54 on: August 21, 2017, 02:58:44 PM »

Changing up Events
A quick little update.

I've been meaning to switch over my event library for a while because right now there is almost none of it that is "strongly typed". This means that for my events I can basically send in whatever type/number of parameters I want with a given call, and then cast them as the proper type at the receiving end. This is almost fine for small scale things, but for anything of even modest size it introduces massive issues if not handled carefully. This is because you have no way to enforce that the parameters you are sending are the proper type and in the right order. For example, before I had to do this:

Code:
// fire an event
EventManager.FireEvent(Events.MyEventType.MyEvent, "some param", anInt, randomObject)

// functions that subbed had to be like this
void myHandler(params object[] args)
{
    string stringParam = (string)args[0];
    int intParam = (int)args[1];
    RandomObjectType objectPargam = (RandomObjectType)args[2];
}

You can see how there are two points of failure — both the function caller and the handler need to know about specific parameters and their order without having any sort of strong typing to verify either. It was easy to get started with this as you don't have to think too hard about events, but moving to something stricter was always in the plan.

So, I've been wanting to switch to something better for a while, and with the recent addition of networked multiplayer I got a really good view of how two people will play against each other but also realized my event system finally need this predicted change. This was because that, though my game logic and input were properly decoupled, my events had no parameter that specified who was sending an event. This meant that when I received a P2P message and needed to fire an event, I had no way to fire an event only for a specific player, and instead would fire the event for all players. A "simple" fix for this would be to just change all the event calls to send a player as a first parameter, but I couldn't shake the fact that I was just adding more cruft onto a system that was now begging to be refactored.

So I've changed it! When looking at templates for a "better" way to do events, I looked first at the way SteamVR does events, and then looked into MessageKit by prime31. The former I was made aware of because I was tracking their changes and noted that they too used to use non-strongly type events, but specifically switched over to something strongly typed to avoid the runtime allocations you incur but having functions with "param object[] args" in a method signature. MessageKit I found by just exploring some of prime31's repos (good stuff there!), and saw that it was very similar to my current event system, but was strongly typed in a way that enforced proper parameters. What I also liked about MessageKit was that it leveraged use of events defined as const ints vs. SteamVR's use of static actions. Events serialized a bit easier because of this, and were less bulky to define.

After fiddling with SteamVR's events for a bit, I decided to go with implementing MessageKit as it more easily meshes with my current event system while also being strongly typed. So now I'm spending some time basically replacing a lot of code, but in the end hoping to have cleaned up the core way my game logic works in general.

Lastly, I've started trying to pick up streaming again! I implemented most of the above on stream today, and plan to contiue trying to stream weekday mornings. I also got a webcam so I'm no longer an anonymous voice! If you're interested, you can sub to the channel here.

Logged

CANTATA
bacon
Level 1
*


View Profile
« Reply #55 on: August 29, 2017, 03:19:24 PM »

P2P Networking from Top to Bottom

I feel like all I ever talk about is events! So in continuing that I’m going to talk some events + networking. Networking feels like a massive task to take on as an indie and the truthfulness of it is that it becomes harder and harder the later you start trying to implement it. This is to say that, if you plan on having your game be networked, you ideally start implementing network functionality from day one.

The reason for this is that, when you’re working with networked code in a P2P context, you’ll want to be able to use the same code that executes local game logic to be the same code that you call when you need to execute an event that you receive from the network. Otherwise, you’ll end up having two functions for every call, one that works locally and one that the network calls that then calls the local function. The second method is totally viable but the former allows for much cleaner code. When you receive a call from the network, you just call the same function that you would normally call to move something locally.

To elaborate a bit more, here’s how I’m doing things.

To start, I write down what the action is that I went sent over the network. For your game, these are usually “big actions” that affect moment to moment play. So things like moving, shooting, spawning, etc. Once you know the action, you basically need to come up with your own network packet type that your game understands. “Network Packet” sounds both scary and archaic, but it loosely translates to “data structure that your game understands and can be sent across a network”.

The goal here is to send the minimum amount of information required to still successfully produce the required action on the side of the person you’re send to. Thinking about what this is can be hard depending on the action, but, if you’re reusing client code of network code it’s easy — it’s just the function parameters! So, let’s assume I’m trying to move something. Here’s my move function:

Code:
public void moveToTile(List<Tile> tilesToMoveTo, bool sendNetworkAction)
{
    (lots of code here I took out for clarity sake)
    Events.CanMove.InteractableMoved(Interactable, tilesToMoveTo, sendNetworkAction);
}

So when I move something, I do the moving (in the omitted code), then send out an event that states that something has moved. This event is subscribed to by my class P2PHandler, and when it receives this event calls the requisite function that bundles up the network packet to be sent:

Code:
public P2PHandler()
{
    //subscribe to the move event
    Events.CanMove.InteractableMoved += sendP2PMove;
}

void sendP2PMove(Interactable interactable, List<Tile> tilesMovedTo, bool sendNetworkAction)
{
    if(!sendNetworkAction){return;} //this is here so the call isn’t infinitely bounced back and forth.
    List<Vector2> tilePositions = new List<Vector2>();
    foreach (Tile tile in tilesMovedTo)
    {
        tilePositions.Add(new Vector2(tile.GridX, tile.GridY)); //NOTE A
    }
    int interactableID = interactable.OwningPlayer.InteractablesManager.GetIDForInteractable(interactable); //NOTE B
    P2PMove moveBody = new P2PMove(interactableID, tilePositions); //build the move packet
    P2PMessage message = new P2PMessage(interactable.OwningPlayer.PlayerName, P2PMessageKey.MOVE, moveBody.Serialize()); //build the full packed
    GameManager.NetworkManager.Client.SendP2PMessage(message); //send across the network
}

A few notes on this before moving on. At points NOTE A/B, you can see that I’m kind of “casting” my function parameters to different types. I’m converting a list of Tiles to a List of Vector2’s and converting the moved interactable to just be an int. What I’m doing here is “serializing” my parameters into a format that can be serialized by Unity. Serialization is a whole topic in and of itself, but the core idea is that Unity cannot remember every detail about any type you declare, and instead only “knows” about a specific set of types outlined here. It is possible to make it so that something like Tile could just be easily sent across the wire as is, but often time optimizing for serialization can lead to a lot of excess code and headaches as you may have to jump through extra hoops to get at simple parameters. What I prefer doing for bigger types like this is to just send a integer reference that points to the object on the client side, and make sure that those tables mirror each other on initialization.

To actually send this data, I use P2PMessage which is defined like so:

Code:
[System.Serializable] //require per Unity’s rule that it will serialize “custom structs with the Serializable attribute”
public struct P2PMessage
{
    public string Player;
    public P2PMessageKey Key; //an enum
    public string Body;
    public P2PMessage(string player, P2PMessageKey key, string body)
    {
        Player = player;
        Key = key;
        Body = body;
    }
}

public enum P2PMessageKey
{
    MOVE,
    SPAWN
}

public interface P2PAction
{
    string Serialize();
}

[System.Serializable]
public struct P2PMove : P2PAction
{
    public int InteractableID;
    public List<Vector2> Locations;
    public P2PMove(int interactableID, List<Vector2> locations)
    {
        InteractableID = interactableID;
        Locations = locations;
    }
    public string Serialize()
    {
        return JsonUtility.ToJson(this); //serialize this struct to JSON using Unity’s built in serializer
    }
}

A P2PMessage is basically an enum key and a P2PAction that is serialized in the P2PMessage constructor. P2PMove in this case is the container for the data that is necessary to properly interpret the move action on the client side. So here’s the last bit of code from above that does the packing and sending:

Code:
P2PMove moveBody = new P2PMove(interactableID, tilePositions); //build the move packet
P2PMessage message = new P2PMessage(interactable.OwningPlayer.PlayerName, P2PMessageKey.MOVE, moveBody.Serialize()); //build the full packed
GameManager.NetworkManager.Client.SendP2PMessage(message); //send across the network

The parameters are packaged up and send off in the message. I’m using Facepunch.Steamworks to do this, which makes sending P2P data really easy. Here’s the code for that:

Code:
public void SendP2PMessage(P2PMessage message)
{
    string serializedMessage = JsonUtility.ToJson(message); //serialize the whole message
    var data = Encoding.UTF8.GetBytes( serializedMessage ); //convert to bytes

    //loop through all members of the current lobby and send the data
    foreach (ulong id in GetLobbyMemberIDs()) //in my game, data is sent to people who are in a persistent lobby together. kind of a poor man’s server
    {
        if(id == PlayerID){continue;} // don't send the P2P message to the player who sent the message
        Native.Networking.SendP2PPacket( id, data, data.Length ); //this is a call to the DLL
    }
}

Once you send the packet, how do you get it? In the same class that sends the P2PMessage, I’m also subscribing to events that I get from the P2P functionality of the Steamworks library:

Code:
void SubscribeToP2PEvents()
{
    Native.Networking.SetListenChannel(0 , true);
    Native.Networking.OnP2PData += OnP2PData;
    Native.Networking.OnIncomingConnection += OnIncomingConnection;
    Native.Networking.OnConnectionFailed += OnConnectionFailed;
}

When I get P2P data, OnP2PData is called:

Code:
void OnP2PData(ulong sender, byte[] bytes, int length, int channel)
{
    var str = Encoding.UTF8.GetString( bytes, 0, length );
    P2PMessage serializedMessage = JsonUtility.FromJson<P2PMessage>(str); //de-serialize the message
    GameManager.NetworkManager.HandleP2PMessage(sender, serializedMessage);
}

And in this class I grab the message, and deserialize it into it’s proper form, a P2PMessage. I then tell the NetworkManager that I got a new P2PMessage. The NetworkManager function looks like this:

Code:
public void HandleP2PMessage(ulong sender, P2PMessage msg)
{
    P2PHandler.ParseP2PMessage(sender, msg);
}

Which brings us back to P2PHandler, the same class that sent the message originally, but on the other side of the wire! For now, P2PHandler both sends and parses events, though it may change if things become to bulky. When it receives a message, it is parsed like this:

Code:
public void ParseP2PMessage(ulong senderID, P2PMessage msg)
{
    //grab the proper player who the action was done for
    Player player = MatchManager.GetPlayer(msg.Player);

    //call public function/event on the player object that sent in the event
    switch (msg.Key)
    {
        case P2PMessageKey.MOVE:
            P2PMove moveBody = JsonUtility.FromJson<P2PMove>(msg.Body); //deserialize the message body
            //get tiles to move to
            List<Tile> tilesToMoveTo = new List<Tile>();
            foreach (Vector2 pos in moveBody.Locations)
            {
                tilesToMoveTo.Add(MatchManager.CurrentMap.GetTileAtCoords(pos.x, pos.y)); //convert tile coords to tiles
            }
            //grab the interactable
            CanMove interactable = player.InteractablesManager.ActiveInteractables[moveBody.InteractableID].GetAspect<CanMove>();
            interactable.moveToTile(tilesToMoveTo, false); //call the move function. also note the false flag, which instructs the program to NOT also try sending the event again.
            break;
////other case
    }
}

This code calls the move function for the proper intractable, and now both clients are in sync! I didn’t realize this would be as comprehensive as this, but I haven’t seen any great info on actually implementing P2P code inside of Unity and thought it would be useful to see it stepped through bit by bit. I will say that this tuts+ post was a godsend, and most of the ideas here are an outgrowth of that. I highly recommend people who are interested in this read over that, because it provides a great high level overview of P2P networking.






Logged

CANTATA
Suimisu
Level 0
*


View Profile
« Reply #56 on: August 31, 2017, 01:23:05 AM »

Really looking forward how the combination with civilization pans out. And thanks a lot for going so much into the technical details and sharing your knowledge!
Logged
bacon
Level 1
*


View Profile
« Reply #57 on: September 02, 2017, 05:39:52 PM »

Really looking forward how the combination with civilization pans out. And thanks a lot for going so much into the technical details and sharing your knowledge!

Ah you're welcome! I think that indies find their way off of other peoples knowledge because we don't have institutional knowledge to defer to. So I'm happy to share how I'm doing things as to better help others looking at the same problems. As for the civ connection, I'm still trying to figure out if that's a great analogy or not. The game is very much tactical to the level of Advance Wars, but it contains a strategic layer in the actual gameplay that isn't present in a lot of the games I'm otherwise drawing influence from. There it's more akin to traditional wargames. So as I continue to make it, I hope that I can better find a way to describe it...
Logged

CANTATA
Suimisu
Level 0
*


View Profile
« Reply #58 on: September 04, 2017, 11:22:40 PM »

Ah you're welcome! I think that indies find their way off of other peoples knowledge because we don't have institutional knowledge to defer to. So I'm happy to share how I'm doing things as to better help others looking at the same problems. As for the civ connection, I'm still trying to figure out if that's a great analogy or not. The game is very much tactical to the level of Advance Wars, but it contains a strategic layer in the actual gameplay that isn't present in a lot of the games I'm otherwise drawing influence from. There it's more akin to traditional wargames. So as I continue to make it, I hope that I can better find a way to describe it...

Yeah, I love your mindset (and the game)! Keep it up Hand Thumbs Up Right Subscribed to your newsletter so I'll be up to date how it's going forward and which direction it takes Smiley
Logged
bacon
Level 1
*


View Profile
« Reply #59 on: September 20, 2017, 08:56:43 AM »

I've implemented a first pass at bitwise tilemapping:



I’ve read a few posts on bitwise tilemapping and I think they kind of over complicate the idea so I wanted to take a whack at explaining it.

The basic idea is that, instead of needing to meticulously place every tile for a given map (slow, manual), you can mathematically calculate what tile should go where (fast, automatic!) depending on its surrounding tiles. This means you can basically say “land here” on a map and it will place the proper land tile by accounting for the surrounding tiles. This is how most level editors with “brushes” work. The “brush” is just calculating the proper tile to use for a given connection. It may seem like magic but this is pretty easy to do on your own!

Imagine you have a single tile and imagine a data structure that says whether a tile exists in a given direction from that tile. So if I’m checking the North, South, East, and West directions, my data structure would be “NSEW”. For each of those directions, a tile can either exist at that position or not, meaning each direction can be either True (tile exists) or False (no tile).



So a tile with only a tile to the north of it would be “TFFF”. To only the south and the west would be “FTFT”. Look familiar? This is just binary! So TFFF = 1000 = 8 and FTFT = 0101 = 5. Hence, every unique combination directions yields a unique number from 1 to 15. You can also rearrange your “data structure” to make the values different — so instead of NSEW you could use SENW or something like that. Note that your bitwise numbers would change, but as long as you are consistent with how you “encode” the directions you’ll be fine.

With a direct mapping of unique directions to unique number, you can build a simple look up table to tell your game what sprite should go where. So for the following tile map:



A calculated bitwise index then maps to a sprite. Note that you can also easily check corners by just expanding your data structure to include those checks, something like N|S|E|W|NE|NW|SE|SW

Why “Bitwise”?
Although what I presented suggests that you just convert from binary to integer, the term “bitwise” comes from the fact that older ways in which you did this likely used actual bitwise operators for determining the correct tile (to conserve memory). So “North” was likely hardcoded as 1000, South as 0100, East as 0010, and West as 0001. If a tile existed in those directions, you would “logical or” combine them, so NORTH | SOUTH = 1100 = 12, which then maps to a sprite value. You could also still do it this way!

Why not just count?
In doing the operation “bitwise”, you’re assigning unique powers of 2 to every possible neighbor tile, and hence ensuring that (bitwise) combinations of the neighbors are unique and non-repeatable. If you just assigned random numbers, you run into two issues:

1) Tile direction combinations aren’t guaranteed to be unique. If you just used the numbers 1-4 for directions North(1), South(2), East(3), West(4), different directional combinations would yield the same result in the created lookup table. For this example, a tile with a neighbor to the North(1) and South(2) would yield a lookup value of 3, the same value as a tile with a neighbor only to the East(3).

2) If you were smarter about your assignment and did something like North(12), South(13), East(18), West(45), you would generate unique table lookup values, but they wouldn’t necessarily have any meaningful relationship to each other. Assigning directional values in a bitwise manner allows them to maintain a small memory footprint and be deterministic and open for operations — if you want to check if a  tile exists to the North of a tile with a bitwise value (and you’re storing the bitwise value), you can just use if(value & NORTH > 0).

Hope this helps other people looking to do something like this in their games!
Logged

CANTATA
Pages: 1 2 [3] 4 5 ... 9
Print
Jump to:  

Theme orange-lt created by panic