• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
Tyl3r684

Best language to be "versatile" in game making.

68 posts in this topic

I think perhaps someone on this thread who is very stuck on the idea that C# is easier should try to convert this "Test" class to C#.
What I would like them to do is pay extra attention to making sure the outcome is exception safe (since "Danger" might throw an exception).
I can guarentee that the exception safe C# implementation will involve quite a bit more code.. and be more error prone... and not as elegant... and slower... ;)

[code]
struct Test
{
// Something contains a thread or socket descriptor so must be cleaned up.
std::auto_ptr<Something> something;

Test()
{
something.reset(new Something());
Danger danger(something.get()); // throws exception
}
};
[/code] Edited by Karsten_
-2

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347354822' post='4978834']
I think perhaps someone on this thread who is very stuck on the idea that C# is easier should try to convert this "Test" class to C#.
What I would like them to do is pay extra attention to making sure the outcome is exception safe (since "Danger" might throw an exception).
I can guarentee that the exception safe C# implementation will involve quite a bit more code.. and be more error prone... and not as elegant... and slower... ;)

[code]
struct Test
{
// Something contains a thread or socket descriptor so must be cleaned up.
std::auto_ptr<Something> something;

Test()
{
something.reset(new Something());
Danger danger(something.get()); // throws exception
}
};
[/code]
[/quote]

Well considering we have no idea what you are trying to achieve, what you are trying to do or any information other than some random code, one could easily and validly do anything, maybe the design choices made sense in C++ and thats why 'you' feel they wont work in C#, maybe theres a better way to do things that are so much simpler over all and you have just complicated the situation and since we dont know what Something / Danger is how are we meant to mimic the example.

Sometimes more code is written in C#, that doesnt make it a bad thing, depending on what it is, you cant simply add some code and accuse C# of being error prone or not elegant, I was tempted test your theory but honestly I have no idea what I am even looking at never mind why an exception is required and I could easily question why you used a struct, why theres no destructor, why you chose auto_ptr, randomly (and pointlessly) questioning your example.

I am sure if you posted the whole example a C# programmer would happily test your theory, I was going to post an equally pointless async method and ask you to do it just as elegant and safe in C++ but I dont think theres much point
0

Share this post


Link to post
Share on other sites
Heh, I still suggest you try it.. You will be suprised how awkward these (effectively 3 lines) are in C#

And this is barely as complex a technical requirement as an async example (although we have some pretty good C++ libraries for that without requiring language changes. For Compiler specific async functionality, OpenMP is much better than C#'s offering, and Codeplay's offload can even do it in parallel on the SPU. Much more powerful than anything C# can do!). This just demonstrates that C# fails at even the most simplistic design requirements.

edit: FYI

[code]
public class Test : IDisposable
{
// Something contains a thread or socket descriptor so must be cleaned up.
Something something;

public Test()
{
try
{
something = new Something();

using(Danger danger = new Danger(something)) // throws exception
{

}
}
catch(Exception)
{
Cleanup();
throw;
}
}

private void Cleanup()
{
if(something != null){something.Dispose();}
}

public void Dispose()
{
Cleanup();
}
}
[/code] Edited by Karsten_
-2

Share this post


Link to post
Share on other sites
[quote name='swiftcoder' timestamp='1347216051' post='4978362']On the other hand, most of the people making C++ out to be a nightmare (myself included), have experienced C++ [b]in a production environment[/b]. My impression (possibly incorrect) is that most of the posters avidly defending C++ do not share this same experience.[/quote]
Over the last years I have spend quite some time debugging, fixing and refactoring parts of an old C++ engine that contains large pieces of badly written code, parts of it written by long-gone interns. Yeah, it contains some nasty bugs, and evil C++ constructions and even had some dependencies on compiler-specific bugs. Still, the majority of the bugs are still logic errors, only a very few number of the bugs are actually those so called evil C++ constructions that invoke undefined behaviour. Your post kind of gives the idea that these bugs are a significant part of the average C++ code base, but in general that is not the case. Logic errors still remain the majority of the bugs, and for the fatal errors most of it were either assertions or null pointer crashes, which will not be different on so called safer languages (yeah, null pointers may trigger an exception rather then crash, but if you didn't expect a null pointer you probably didn't put try/catch there either so it will terminate with unhandled exception after all).
0

Share this post


Link to post
Share on other sites
[quote name='MichaBen' timestamp='1347387753' post='4978986']
yeah, null pointers may trigger an exception rather then crash, but if you didn't expect a null pointer you probably didn't put try/catch there either so it will terminate with unhandled exception after all[/quote]
But it will also give you a lovely stack trace, which tells you exactly where to start investigation.

By comparison, a SEGFAULT doesn't give you any trace, unless you are experienced enough to a) be running your program in GDB, or b) have core dumps enabled, both of which require you to know how to compile with debug symbols enabled and optimisation disabled.

And that's assuming you even received a SEGFAULT in the first place - it's entirely possible that your invalid pointer landed somewhere you actually have permission to write, in which case the error will be silently ignored (until some indeterminate time later, when you find an invalid value elsewhere in your program.
1

Share this post


Link to post
Share on other sites
Valgrind has always given me a much better output than .NET's generic stacktrace stuff and gdb for this kinda thing and as such is trivial to fix.
Valgrind can also detect memory errors without requiring a SEGFAULT too.
Using standard smart pointers (like you should be using) helps avoid these issues anyway.
In C and C++ it is very easy to disable optimizations and add debugging symbols.
C# also needs to be put in debug mode or it won't give out line numbers. Edited by Karsten_
-2

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347389425' post='4978997']
Valgrind has always given me a much better output than .NET's generic stacktrace stuff and gdb.[/quote]
Sure, but now we aren't talking about languages, we are talking about complex and platform-specific development tools (that beginners won't know to use, or how to use effectively).

[quote]In C and C++ it is very easy to disable optimizations and add debugging symbols. C# needs to be put in debug mode or it woun't give out line numbers.[/quote]
Can you tell me, off the top of your head, exactly which optimisations are disabled at -O1 versus -O2, and what optimisations still need to be disabled to reach Python-level debug-ability (i.e. constant folding)? Can a beginner do the same?

We aren't talking about experienced C++ developers using their language of choice. We are talking about fresh-out-of-highschool, wrote-a-few-pages-in-html beginners. Your protestations to the contrary, I just don't see where C++ is in fact as easy as Python for this use-case.

(and please keep in mind that when I assert this, it is on the basis of having spent several semesters teaching programming to non-CS students).
1

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347378414' post='4978931']
Heh, I still suggest you try it.. You will be suprised how awkward these (effectively 3 lines) are in C#

And this is barely as complex a technical requirement as an async example (although we have some pretty good C++ libraries for that without requiring language changes). This just demonstrates that C# fails at even the most simplistic design requirements.

edit: FYI

[code]
public class Test : IDisposable
{
// Something contains a thread or socket descriptor so must be cleaned up.
Something something;

public Test()
{
try
{
something = new Something();

using(Danger danger = new Danger(something)) // throws exception
{

}
}
catch(Exception)
{
Cleanup();
throw;
}
}

private void Cleanup()
{
if(something != null){something.Dispose();}
}

public void Dispose()
{
Cleanup();
}
}
[/code]
[/quote]

I wouldnt know what I was trying to achieve, I understand that you want to mimic something but what if 'Danger' or 'Something' were poorly implemented, that caused the verbosity in the first place.

You will need to post the full example if you want me or anyone else to understand and then a comparison can be done, I dont have a link but an oldish thread a while back attempted something like this and when the guy posted the full C++ example it was a disaster and didnt even work in either C# or C++ and I think he even compared WPF to Win32 :/.

The problem with these comparisons are they are limited to certain things and often important parts are missing, for example, in C++ you got away with Something something; where as in C# I would have had to write Something something = new Something(); based on this I could easily say C# is more verbose, but ultimately its the end product that is the focus. I recently had a situation where I had to change my design because I no longer could use PhsyX, changing the C# side of things was fine and straight forward, but C++ holy shit I lasted almost 2 weeks fixing stuff and still found problems after, now even though it was my fault for redesigning mid way development I could easily have said "C# is better".

Sometimes its the developer that needs a review not the language ;)

Anyway im a fan of C# and C++ and use them both for different things but typically together, I personally am more in favour of C# for many reasons but sometimes I prefer to use C++, I could list all the great things about C# and even compare it to C++ but it makes no difference what others prefer and I gain nothing by convincing people to pick a language and you know what the best thing is about using both C# and C++ together, it means I get to laugh at comparisons
0

Share this post


Link to post
Share on other sites
Dynamo_Maestro,

This code example really couldn't be simpler. We arn't interested in how Disaster or Something are implemented, all we need to know is that we must call Dispose on them.
This is mimicking something. It is mimicking C#'s piss poor attempt at useful resource management and it fails. It is not suitable for games.

As for your suggestions on design change or the developer needing review.. Hey, I agree this stuff does happen, but changing effectively 3 lines compared to around 8 plus 2 extra functions ain't a good start to the refactor... or the poor git having to sit down with the coder under review to check the src! hah.

[quote name='swiftcoder' timestamp='1347389929' post='4979002']Can you tell me, off the top of your head, exactly which optimisations are disabled at -O1 versus -O2, and what optimisations still need to be disabled to reach Python-level debug-ability
[/quote]

I see what your saying but frankly just because C++ has the extra features, you certainly don't need to teach it all to beginners. Likewise with python you probably teach them using the garbage collector rather than true RAII (with CPython).

Kinda like we don't need to teach C# using the /unsafe flag and pseudo pointers to beginners. There is more to learn in C++ to become a master... but that is not a bad thing... it just shows how limited C# really is.

Afterall, they will probably always be ending up using Visual Studio and select the Debug profile in exactly the same way they would in C# and Visual Studio. Teaching them using the command line and Vim never goes down well.

I also teach a few courses at my University (while I am doing my PhD) and I have taught using two very different platforms. XNA/C# in the first year (until it was effectively dropped by Microsoft) and now C++ and OpenGL. So far there have been no extra issues due to the upgrade in language and platform. Plus the students will be in a far better position at the end of it knowing a true industry standard language rather than being tied down to hobby or games making tools like XNA. Edited by Karsten_
-1

Share this post


Link to post
Share on other sites
Karsten_, you do know that C# allows you to override the finalizer, don't you? It still involves more code than the C++ version, but you can move that try/catch block out to somewhere more appropriate. I just wrote up a simple test program in C# to try this out for myself and see if the finalizer would get called if an exception was thrown in a constructor:

[source lang="csharp"]
class Something : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed something");
}
}
class Danger : IDisposable
{
public Danger(Something something)
{
throw new InvalidOperationException();
}
~Danger()
{
Dispose();
}

public void Dispose()
{
Console.WriteLine("Disposed danger");
}
}
// Karsten_'s example class
class Test : IDisposable
{
Something something;
public Test()
{
something = new Something();
using (Danger danger = new Danger(something)) { }
}
// finalizer overload
~Test()
{
Dispose();
}
public void Dispose()
{
if (something != null)
{
something.Dispose();
something = null;
Console.WriteLine("Disposed test");
Console.ReadLine();
}
}
}
class Program
{
static void Main(string[] args)
{
Test test = null;
try
{
test = new Test();
}
catch (Exception e)
{
}
}
}

[/source]

The output I get is this:
[code]
Disposed danger
Disposed something
Disposed test
[/code]

It's still a bit verbose compared to the C++ version, but arguably less so than the one with the try/catch inside Test's constructor. Granted that now the disposing will happen when the GC runs, which may not always be desirable, but you never specified that the disposing must happen immediately after the exception is thrown. Edited by Oberon_Command
2

Share this post


Link to post
Share on other sites
The finalizer does not get called if there is an exception in the constructor. The only reason why it appears to be working is that the finalizer is being run for the other classes because the program is terminating. In a game, this is unlikely to be the case unless you run GC.Collect() every loop (though even this wont help finalize some things like out of scope threads).

For example, move or copy your Console.ReadLine() to the end of Main() after letting the Test reference go out of scope and you will see exactly what I mean. Better still, throw an exception just before the end of your program (just after the new ReadLine() position) and (in mono on x86 linux) the other dispose statements will never even be called. (This is an example of not exception safe).

[code]
static void Main(string[] args)
{
Test test = null;
try
{
test = new Test();
}
catch (Exception e)
{
}

throw new Exception();
}
[/code]

New output
[code]
Disposed danger
*App terminated due to thrown exception*
[/code]

The only reason why danger is disposed is because it is in a "using" but unfortunately using only works within a block so can't help ensure "something" is cleaned up.
The critical issue is here though.

[code]
something = new Something();
using (Danger danger = new Danger(something)) { }
[/code]

Now that "something" is constructed and we know that the Danger() throws an exception, there is nothing set up to call Dispose() on the something instance now. And if this contains a thread or another limited resource, then it is game over! Again, this is not exception safe.

So whilst the C++ code is much less verbose, it also ensures that the memory is *always* cleaned up whatever you do with it.
In C++, the destructor does not get called if there is an exception in it's constructor... However I have gotten round this in my previous solution by wrapping the something instance in a smart pointer which *does* call delete on it's contained data when it pops off the stack.

Edit: I have marked you up for your code example, since it certainly helped what I was trying to describe. Edited by Karsten_
-2

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347398811' post='4979060']
So whilst the C++ code is much less verbose, it also ensures that the memory is *always* cleaned up whatever you do with it.[/quote]
You are hardly the [url="http://swiftcoder.wordpress.com/2009/02/18/raii-why-is-it-unique-to-c/"]first person to notice that Garbage Collection and RAII don't play terribly well together[/url] (it has however taken me this long to figure out that was what you were talking about).

And the long and short of it is: who cares? You only need to worry about manually finalising mission-critical resources, which a beginner doesn't need to worry about. And if typing a few extra lines in the critical case gives you the heeby-jeebies, well, one can always stick to x86 assembly [img]http://public.gamedev.net//public/style_emoticons/default/sad.png[/img]
2

Share this post


Link to post
Share on other sites
Well then... as a fellow [url="https://www.ibm.com/developerworks/mydeveloperworks/blogs/karsten/entry/raii_with_c_libraries?lang=en"]RAII[/url] buff, surely you feel the same frustration that a solved and very satisfactory solution to memory management is effectively ignored by the current generation of new developers just because they happen to be using a language which has effectively regressed in this area.

So I guess, I rest my case in the knowledge that the C# language is flawed but "who cares"... C++ just ain't cool.
C and C++ are both a step up from assembly... What the hell is C# a step up from?

So in my opinion, the suggested ease of C# does not outweigh it's design flaw and so I will never recommend it to people in threads such as these :) Edited by Karsten_
-3

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347398811' post='4979060']
The only reason why it appears to be working is that the finalizer is being run for the other classes because the program is terminating. In a game, this is unlikely to be the case unless you run GC.Collect() every loop (though even this wont help finalize some things like out of scope threads).[/quote]

But it still runs when the GC runs. I already acknowledged that it won't be run immediately, but if you can guarantee that the GC will run on that object at some point, the finalizer will run and the object will be disposed, and there won't be a leak. Naturally, this will prove a problem if we want to actually recover from the exception, as the now potentially broken object will hang around until the next GC run. But it was never specified that the example given required whatever exception Danger throws to be recovered from, so I assumed that like almost all cases I've encountered, it wasn't, and the program terminating upon hitting an exception was desirable behaviour, and therefore I could take advantage of it. I personally prefer that my games crash on an exception even in C++, if only because their existence becomes much more apparent, usually causing me to notice them and fix whatever the problem is more quickly.

Understand, I'm not arguing in the slightest with the idea that C# is worse off for lack of full RAII, just that its "pseudo-RAII" is not as bad a situation as you're making it out to be given that a few caveats are observed and a few hoops jumped through. It's a pity stack types can't have destructors in C# - that would bring C#'s "pseudo-RAII" closer to actual RAII.

[quote]Better still, throw an exception just before the end of your program (just after the new ReadLine() position) and (in mono on x86 linux) the other dispose statements will never even be called. (This is an example of not exception safe).[/quote]

This is quite true, and is the purpose of the try/catch in the Main() function of this test program. That try/catch block ensures that the program does not crash before the garbage collector runs for the final time. Again, if that is not desirable or possible, this all falls down at our feet, as I once again acknowledge. I've not seen many cases in C# where it is not desirable, however.

[quote]
The only reason why danger is disposed is because it is in a "using" but unfortunately using only works within a block so can't help ensure "something" is cleaned up.[/quote]

Interestingly, I've found that danger is NOT disposed by the using itself. If I remove the destructor/finalizer from the Danger class, I get this:

[code]
Disposed something
Disposed test
[/code]

So clearly, that using never calls Dispose, because otherwise removing the Dispose() call in the finalizer would not change the output. I guess using blocks don't call Dispose() if an exception occurs in the constructor of whatever's being managed by the using block, which I must admit is a bit disappointing.

[quote]So in my opinion, the suggested ease of C# does not outweigh it's design flaw and so I will never recommend it to people in threads such as these[/quote]

So you discourage beginners from using managed languages in general, I take it?

I wouldn't call it a "design flaw," personally. As has been mentioned, RAII and garbage collection don't really play nice together. If you're using a language with garbage collection, chances are very good that you don't actually care very much about exactly when your resources get released, even in exceptional conditions - you just care that they ARE released, at some point. In fact that's... kind of the whole point of garbage collection. Edited by Oberon_Command
1

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347401477' post='4979085']
So I guess, I rest my case in the knowledge that the C# language is flawed but "who cares"... C++ just ain't cool.
[/quote]

And you are saying C++ doesn't have flaws in it too? That some how it is the 'perfect' language?
Get real.

C++ has more flaws in it than C#, more flaws which will trip up someone who isn't using it AND has less productivity associated with it in situations where that matters more.

And before you bash me as some C# fanboy I should point out that my primary coding enviroment is C++ and will likely continue to be C++ for some time however to dismiss C# over some trivial issue (and it really is) and thus avoiding the benefits of the language and the massive class library which comes with it is frankly idiotic.

It's not a matter of 'what is cool' its a matter of 'what is the right tool for the job'.
Sometimes that tool is C++.
Sometimes that tool is C#.
Sometimes that tool is Python.
Or some other language or method.

If you want to wander around with a set of 'C++ is bestest!' blinkers on then feel free; be prepared to be taken to task over it however because most of us who work for a living with the language know better... apprently much better.. and apprently have a much better appreciation for other languages and tool sets.
1

Share this post


Link to post
Share on other sites
[quote name='Oberon_Command' timestamp='1347403864' post='4979096']
I guess using blocks don't call Dispose() if an exception occurs in the constructor
[/quote]

Yeah, thats where my hacky try / catch Dispose(); throw in it's constructor solution came from in my C# example. It isn't ideal but it seems a safe compromise. :/

[quote name='Oberon_Command' timestamp='1347403864' post='4979096']
So you discourage beginners from using managed languages in general, I take it?
[/quote]

Well, for games, yes. I simply don't feel that garbage collected languages are the correct tool for the job. For a business application or server then yeah, I don't see a problem with managed languages.

I find it quite wrong that so many new games developers on this forum are steered away from C++ when it is infact the perfect language for this task and is much more suited to games and other high performance software than any other language currently in use today.

[quote name='phantom' timestamp='1347405751' post='4979108']
C++ has more flaws in it than C#
[/quote]

Oh? Such as? Until you can give me an actual example(s), this statement is useless. As for thinking correct memory management is a "trivial issue", I think that is an extremely careless oversight on your behalf.

[quote name='phantom' timestamp='1347405751' post='4979108']
the massive class library which comes with it
[/quote]

Class library? What the heck has that to do with anything related to a language? I can easily utilize that from C++/CLI or if I really wanted a 100% native solution, I could use Qt. Certainly not going to cripple my code design for a few "nifty" classes. (many of which arn't even needed in game dev anyway). Edited by Karsten_
-2

Share this post


Link to post
Share on other sites
[quote name='Karsten_' timestamp='1347409107' post='4979118']
[quote name='phantom' timestamp='1347405751' post='4979108']
C++ has more flaws in it than C#
[/quote]

Oh? Such as?[/quote]

How about the ones described in [url="http://yosefk.com/c++fqa/"]http://yosefk.com/c++fqa/[/url]?

[quote]Class library? What the heck has that to do with anything related to a language?[/quote]

A lot, actually. If a class library is so inseparable from a programming language to the point where it's described in the actual [i]language spec[/i] (as is this case for the C# and C++ standard libraries, I believe), then for all intents and purposes, that library is a part of that language. Even if a class library is not sufficiently tied to the language to qualify for being part of the language, if it is widely-enough available it will grow into the common idiom of that language so much as to be inseparable enough that knowing and using it will be inescapable.

Languages (both natural and artificial) are more than syntax. They are also idiom and culture. Class libraries in general contribute to the latter two. Edited by Oberon_Command
2

Share this post


Link to post
Share on other sites
Perhaps read this one...
[url="http://yosefk.com/c++fqa/defective.html#defect-10"]http://yosefk.com/c ....html#defect-10[/url]

<rant>This could be a good idea in some cases if C++ exceptions were any good. They aren't, and can't be - as usual, because of another C++ "feature"... snip</rant>

This guy sounds like a moron.

And this one...
[url="http://yosefk.com/c++fqa/defective.html#defect-13"]http://yosefk.com/c ....html#defect-13[/url]
He goes into detail to explain the issues with manual memory management.. then mentions smart pointers and trails off. He didnt actually care to elaborate on C++'s elegant and unique solution to manual memeory management?

As for a class library being part of a language, when .NET offers the same classes on any of their compatible languages, it certainly isnt a sane argument for choosing C#.

I guess we can resume this discussion in a few years when C# is long forgotten and C++ is still uncool. [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img] Edited by Karsten_
-3

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0